CampaignMenu.qml 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906
  1. import QtQuick 2.15
  2. import QtQuick.Controls 2.15
  3. import QtQuick.Layouts 1.15
  4. import StandardOfIron 1.0
  5. Item {
  6. id: root
  7. property var campaigns: []
  8. signal missionSelected(string campaignId)
  9. signal cancelled()
  10. function refreshCampaigns() {
  11. if (typeof game !== "undefined" && game.available_campaigns)
  12. campaigns = game.available_campaigns;
  13. }
  14. onVisibleChanged: {
  15. if (visible && typeof game !== "undefined" && game.load_campaigns) {
  16. game.load_campaigns();
  17. refreshCampaigns();
  18. }
  19. }
  20. anchors.fill: parent
  21. focus: true
  22. Keys.onPressed: function(event) {
  23. if (event.key === Qt.Key_Escape) {
  24. root.cancelled();
  25. event.accepted = true;
  26. }
  27. }
  28. Connections {
  29. function onAvailable_campaigns_changed() {
  30. refreshCampaigns();
  31. }
  32. target: (typeof game !== "undefined") ? game : null
  33. }
  34. Rectangle {
  35. anchors.fill: parent
  36. color: Theme.dim
  37. }
  38. Rectangle {
  39. id: container
  40. width: Math.min(parent.width * 0.85, 1200)
  41. height: Math.min(parent.height * 0.85, 800)
  42. anchors.centerIn: parent
  43. radius: Theme.radiusPanel
  44. color: Theme.panelBase
  45. border.color: Theme.panelBr
  46. border.width: 1
  47. opacity: 0.98
  48. ColumnLayout {
  49. anchors.fill: parent
  50. anchors.margins: Theme.spacingXLarge
  51. spacing: Theme.spacingLarge
  52. RowLayout {
  53. Layout.fillWidth: true
  54. spacing: Theme.spacingMedium
  55. Label {
  56. text: qsTr("Campaign Missions")
  57. color: Theme.textMain
  58. font.pointSize: Theme.fontSizeHero
  59. font.bold: true
  60. Layout.fillWidth: true
  61. }
  62. StyledButton {
  63. text: qsTr("← Back")
  64. onClicked: root.cancelled()
  65. }
  66. }
  67. Rectangle {
  68. Layout.fillWidth: true
  69. Layout.preferredHeight: 1
  70. color: Theme.border
  71. }
  72. ScrollView {
  73. Layout.fillWidth: true
  74. Layout.fillHeight: true
  75. clip: true
  76. ListView {
  77. id: listView
  78. model: root.campaigns
  79. spacing: Theme.spacingMedium
  80. delegate: Rectangle {
  81. width: listView.width
  82. height: 120
  83. radius: Theme.radiusLarge
  84. color: mouseArea.containsMouse ? Theme.hoverBg : Theme.cardBase
  85. border.color: mouseArea.containsMouse ? Theme.selectedBr : Theme.cardBorder
  86. border.width: 1
  87. MouseArea {
  88. id: mouseArea
  89. anchors.fill: parent
  90. hoverEnabled: true
  91. cursorShape: Qt.PointingHandCursor
  92. onClicked: {
  93. missionDetailPanel.visible = true;
  94. missionDetailPanel.campaignData = modelData;
  95. }
  96. }
  97. RowLayout {
  98. anchors.fill: parent
  99. anchors.margins: Theme.spacingMedium
  100. spacing: Theme.spacingMedium
  101. ColumnLayout {
  102. Layout.fillWidth: true
  103. spacing: Theme.spacingSmall
  104. RowLayout {
  105. Layout.fillWidth: true
  106. spacing: Theme.spacingSmall
  107. Label {
  108. text: modelData.title || ""
  109. color: Theme.textMain
  110. font.pointSize: Theme.fontSizeTitle
  111. font.bold: true
  112. Layout.fillWidth: true
  113. }
  114. Rectangle {
  115. visible: modelData.completed || false
  116. Layout.preferredWidth: 100
  117. Layout.preferredHeight: 24
  118. radius: Theme.radiusSmall
  119. color: Theme.successBg
  120. border.color: Theme.successBr
  121. border.width: 1
  122. Label {
  123. anchors.centerIn: parent
  124. text: qsTr("✓ Completed")
  125. color: Theme.successText
  126. font.pointSize: Theme.fontSizeSmall
  127. font.bold: true
  128. }
  129. }
  130. Rectangle {
  131. visible: !(modelData.unlocked || false)
  132. Layout.preferredWidth: 80
  133. Layout.preferredHeight: 24
  134. radius: Theme.radiusSmall
  135. color: Theme.disabledBg
  136. border.color: Theme.border
  137. border.width: 1
  138. Label {
  139. anchors.centerIn: parent
  140. text: qsTr("🔒 Locked")
  141. color: Theme.textDim
  142. font.pointSize: Theme.fontSizeSmall
  143. }
  144. }
  145. }
  146. Label {
  147. text: modelData.description || ""
  148. color: Theme.textSubLite
  149. wrapMode: Text.WordWrap
  150. maximumLineCount: 2
  151. elide: Text.ElideRight
  152. Layout.fillWidth: true
  153. font.pointSize: Theme.fontSizeMedium
  154. }
  155. }
  156. Text {
  157. text: "›"
  158. font.pointSize: Theme.fontSizeHero
  159. color: Theme.textHint
  160. }
  161. }
  162. Behavior on color {
  163. ColorAnimation {
  164. duration: Theme.animNormal
  165. }
  166. }
  167. Behavior on border.color {
  168. ColorAnimation {
  169. duration: Theme.animNormal
  170. }
  171. }
  172. }
  173. }
  174. }
  175. Label {
  176. visible: root.campaigns.length === 0
  177. text: qsTr("No campaign missions available")
  178. color: Theme.textDim
  179. font.pointSize: Theme.fontSizeMedium
  180. horizontalAlignment: Text.AlignHCenter
  181. Layout.fillWidth: true
  182. Layout.fillHeight: true
  183. }
  184. }
  185. }
  186. Rectangle {
  187. id: missionDetailPanel
  188. property var campaignData: null
  189. property real map_orbit_yaw: 180
  190. property real map_orbit_pitch: 90
  191. property real map_orbit_distance: 1.2
  192. property var province_labels: []
  193. property string hoverProvinceName: ""
  194. property string hoverProvinceOwner: ""
  195. property real hoverMouseX: 0
  196. property real hoverMouseY: 0
  197. property var ownerLegend: [{
  198. "name": qsTr("Rome"),
  199. "color": "#d01f1a"
  200. }, {
  201. "name": qsTr("Carthage"),
  202. "color": "#cc8f47"
  203. }, {
  204. "name": qsTr("Neutral"),
  205. "color": "#3a3a3a"
  206. }]
  207. property var provinceSources: ["assets/campaign_map/provinces.json", "qrc:/assets/campaign_map/provinces.json", "qrc:/StandardOfIron/assets/campaign_map/provinces.json", "qrc:/qt/qml/StandardOfIron/assets/campaign_map/provinces.json"]
  208. property int label_refresh: 0
  209. property int current_mission_index: 7
  210. function loadProvinces() {
  211. loadProvincesFrom(0);
  212. }
  213. function labelUvFor(prov) {
  214. if (prov && prov.label_uv && prov.label_uv.length === 2)
  215. return prov.label_uv;
  216. if (!prov || !prov.triangles || prov.triangles.length === 0)
  217. return null;
  218. var sumU = 0;
  219. var sumV = 0;
  220. var count = 0;
  221. var step = Math.max(1, Math.floor(prov.triangles.length / 200));
  222. for (var i = 0; i < prov.triangles.length; i += step) {
  223. var pt = prov.triangles[i];
  224. if (!pt || pt.length < 2)
  225. continue;
  226. sumU += pt[0];
  227. sumV += pt[1];
  228. count += 1;
  229. }
  230. if (count === 0)
  231. return null;
  232. return [sumU / count, sumV / count];
  233. }
  234. function provinceInfoFor(id) {
  235. if (!id)
  236. return null;
  237. for (var i = 0; i < province_labels.length; i++) {
  238. var prov = province_labels[i];
  239. if (prov && prov.id === id)
  240. return prov;
  241. }
  242. return null;
  243. }
  244. function loadProvincesFrom(index) {
  245. if (index >= provinceSources.length)
  246. return ;
  247. var xhr = new XMLHttpRequest();
  248. xhr.open("GET", provinceSources[index]);
  249. xhr.onreadystatechange = function() {
  250. if (xhr.readyState !== XMLHttpRequest.DONE)
  251. return ;
  252. if (xhr.status !== 200 && xhr.status !== 0) {
  253. loadProvincesFrom(index + 1);
  254. return ;
  255. }
  256. try {
  257. var data = JSON.parse(xhr.responseText);
  258. if (data && data.provinces) {
  259. var hasCities = false;
  260. for (var i = 0; i < data.provinces.length; i++) {
  261. var prov = data.provinces[i];
  262. if (prov && prov.cities && prov.cities.length > 0) {
  263. hasCities = true;
  264. break;
  265. }
  266. }
  267. if (!hasCities) {
  268. loadProvincesFrom(index + 1);
  269. return ;
  270. }
  271. missionDetailPanel.province_labels = data.provinces;
  272. missionDetailPanel.label_refresh += 1;
  273. }
  274. } catch (e) {
  275. loadProvincesFrom(index + 1);
  276. }
  277. };
  278. xhr.send();
  279. }
  280. onProvince_labels_changed: label_refresh += 1
  281. Component.onCompleted: {
  282. refreshCampaigns();
  283. if (campaignMapLoader.item) {
  284. var labels = campaignMapLoader.item.province_labels;
  285. if (labels && labels.length > 0) {
  286. missionDetailPanel.province_labels = labels;
  287. missionDetailPanel.label_refresh += 1;
  288. } else {
  289. loadProvinces();
  290. }
  291. } else {
  292. loadProvinces();
  293. }
  294. }
  295. onCampaignDataChanged: {
  296. map_orbit_yaw = 180;
  297. map_orbit_pitch = 90;
  298. map_orbit_distance = 1.2;
  299. }
  300. visible: false
  301. anchors.fill: parent
  302. color: Theme.dim
  303. z: 100
  304. MouseArea {
  305. anchors.fill: parent
  306. onClicked: missionDetailPanel.visible = false
  307. }
  308. Rectangle {
  309. width: Math.min(parent.width * 0.7, 900)
  310. height: Math.min(parent.height * 0.8, 700)
  311. anchors.centerIn: parent
  312. radius: Theme.radiusPanel
  313. color: Theme.panelBase
  314. border.color: Theme.panelBr
  315. border.width: 2
  316. MouseArea {
  317. anchors.fill: parent
  318. onClicked: {
  319. }
  320. }
  321. ColumnLayout {
  322. anchors.fill: parent
  323. anchors.margins: Theme.spacingXLarge
  324. spacing: Theme.spacingLarge
  325. Label {
  326. text: missionDetailPanel.campaignData ? (missionDetailPanel.campaignData.title || "") : ""
  327. color: Theme.textMain
  328. font.pointSize: Theme.fontSizeHero
  329. font.bold: true
  330. Layout.fillWidth: true
  331. }
  332. Label {
  333. text: missionDetailPanel.campaignData ? (missionDetailPanel.campaignData.description || "") : ""
  334. color: Theme.textSubLite
  335. wrapMode: Text.WordWrap
  336. Layout.fillWidth: true
  337. font.pointSize: Theme.fontSizeMedium
  338. }
  339. Rectangle {
  340. id: mapFrame
  341. Layout.fillWidth: true
  342. Layout.preferredHeight: 240
  343. radius: Theme.radiusMedium
  344. color: Theme.cardBase
  345. border.color: Theme.cardBorder
  346. border.width: 1
  347. Loader {
  348. id: campaignMapLoader
  349. anchors.fill: parent
  350. anchors.margins: Theme.spacingSmall
  351. active: root.visible && (typeof mainWindow === 'undefined' || !mainWindow.gameStarted)
  352. sourceComponent: Component {
  353. CampaignMapView {
  354. id: campaignMap
  355. anchors.fill: parent
  356. orbit_yaw: missionDetailPanel.map_orbit_yaw
  357. orbit_pitch: missionDetailPanel.map_orbit_pitch
  358. orbit_distance: missionDetailPanel.map_orbit_distance
  359. current_mission: missionDetailPanel.current_mission_index
  360. onOrbit_yaw_changed: missionDetailPanel.label_refresh += 1
  361. onOrbit_pitch_changed: missionDetailPanel.label_refresh += 1
  362. onOrbit_distance_changed: missionDetailPanel.label_refresh += 1
  363. onCurrent_mission_changed: missionDetailPanel.label_refresh += 1
  364. onWidthChanged: missionDetailPanel.label_refresh += 1
  365. onHeightChanged: missionDetailPanel.label_refresh += 1
  366. }
  367. }
  368. }
  369. MouseArea {
  370. property real lastX: 0
  371. property real lastY: 0
  372. anchors.fill: parent
  373. hoverEnabled: true
  374. acceptedButtons: Qt.LeftButton
  375. onPressed: function(mouse) {
  376. lastX = mouse.x;
  377. lastY = mouse.y;
  378. }
  379. onPositionChanged: function(mouse) {
  380. if (!(mouse.buttons & Qt.LeftButton))
  381. return ;
  382. var dx = mouse.x - lastX;
  383. var dy = mouse.y - lastY;
  384. lastX = mouse.x;
  385. lastY = mouse.y;
  386. missionDetailPanel.map_orbit_yaw += dx * 0.4;
  387. missionDetailPanel.map_orbit_pitch = Math.max(5, Math.min(90, missionDetailPanel.map_orbit_pitch + dy * 0.4));
  388. }
  389. onMouseXChanged: function() {
  390. if (!hoverEnabled || !campaignMapLoader.item)
  391. return ;
  392. missionDetailPanel.hoverMouseX = mouseX;
  393. missionDetailPanel.hoverMouseY = mouseY;
  394. var info = campaignMapLoader.item.province_info_at_screen(mouseX, mouseY);
  395. var id = info && info.id ? info.id : "";
  396. campaignMapLoader.item.hover_province_id = id;
  397. missionDetailPanel.hoverProvinceName = info && info.name ? info.name : "";
  398. missionDetailPanel.hoverProvinceOwner = info && info.owner ? info.owner : "";
  399. }
  400. onMouseYChanged: function() {
  401. if (!hoverEnabled || !campaignMapLoader.item)
  402. return ;
  403. missionDetailPanel.hoverMouseX = mouseX;
  404. missionDetailPanel.hoverMouseY = mouseY;
  405. var info = campaignMapLoader.item.province_info_at_screen(mouseX, mouseY);
  406. var id = info && info.id ? info.id : "";
  407. campaignMapLoader.item.hover_province_id = id;
  408. missionDetailPanel.hoverProvinceName = info && info.name ? info.name : "";
  409. missionDetailPanel.hoverProvinceOwner = info && info.owner ? info.owner : "";
  410. }
  411. onExited: {
  412. if (campaignMapLoader.item)
  413. campaignMapLoader.item.hover_province_id = "";
  414. missionDetailPanel.hoverProvinceName = "";
  415. missionDetailPanel.hoverProvinceOwner = "";
  416. }
  417. onWheel: function(wheel) {
  418. var step = wheel.angleDelta.y > 0 ? 0.9 : 1.1;
  419. var nextDistance = missionDetailPanel.map_orbit_distance * step;
  420. if (campaignMapLoader.item)
  421. missionDetailPanel.map_orbit_distance = Math.min(campaignMapLoader.item.max_orbit_distance, Math.max(campaignMapLoader.item.min_orbit_distance, nextDistance));
  422. wheel.accepted = true;
  423. }
  424. }
  425. Item {
  426. id: hannibalIcon
  427. property int _refresh: missionDetailPanel.label_refresh
  428. property var _pos: (_refresh >= 0 && campaignMapLoader.item) ? campaignMapLoader.item.hannibal_icon_position() : Qt.point(0, 0)
  429. property var _iconSources: ["qrc:/StandardOfIron/assets/visuals/hannibal.png", "qrc:/assets/visuals/hannibal.png", "assets/visuals/hannibal.png", "qrc:/qt/qml/StandardOfIron/assets/visuals/hannibal.png"]
  430. property int _iconIndex: 0
  431. visible: campaignMapLoader.item && _pos.x > 0 && _pos.y > 0
  432. z: 10
  433. x: _pos.x
  434. y: _pos.y
  435. Rectangle {
  436. width: 44
  437. height: 44
  438. x: -width / 2
  439. y: -height / 2
  440. radius: 6
  441. color: "#2a1f1a"
  442. border.color: "#d4a857"
  443. border.width: 2
  444. opacity: 0.95
  445. Rectangle {
  446. anchors.fill: parent
  447. anchors.margins: 2
  448. radius: 4
  449. color: "transparent"
  450. border.color: "#6b4423"
  451. border.width: 1
  452. }
  453. }
  454. Image {
  455. source: hannibalIcon._iconSources[hannibalIcon._iconIndex]
  456. width: 36
  457. height: 36
  458. x: -width / 2
  459. y: -height / 2
  460. smooth: true
  461. mipmap: true
  462. fillMode: Image.PreserveAspectFit
  463. cache: true
  464. asynchronous: false
  465. onStatusChanged: {
  466. if (status === Image.Error && hannibalIcon._iconIndex + 1 < hannibalIcon._iconSources.length) {
  467. hannibalIcon._iconIndex += 1;
  468. source = hannibalIcon._iconSources[hannibalIcon._iconIndex];
  469. }
  470. }
  471. }
  472. Rectangle {
  473. width: 50
  474. height: 50
  475. x: -width / 2
  476. y: -height / 2
  477. radius: width / 2
  478. color: "transparent"
  479. border.color: "#d4a857"
  480. border.width: 2
  481. opacity: 0.4
  482. SequentialAnimation on opacity {
  483. loops: Animation.Infinite
  484. running: hannibalIcon.visible
  485. NumberAnimation {
  486. from: 0.4
  487. to: 0
  488. duration: 1500
  489. easing.type: Easing.OutCubic
  490. }
  491. PauseAnimation {
  492. duration: 500
  493. }
  494. }
  495. SequentialAnimation on scale {
  496. loops: Animation.Infinite
  497. running: hannibalIcon.visible
  498. NumberAnimation {
  499. from: 1
  500. to: 1.3
  501. duration: 1500
  502. easing.type: Easing.OutCubic
  503. }
  504. NumberAnimation {
  505. from: 1.3
  506. to: 1
  507. duration: 0
  508. }
  509. PauseAnimation {
  510. duration: 500
  511. }
  512. }
  513. }
  514. }
  515. Repeater {
  516. model: missionDetailPanel.province_labels
  517. delegate: Text {
  518. property var _labelUv: missionDetailPanel.labelUvFor(modelData)
  519. property int _refresh: missionDetailPanel.label_refresh
  520. property var _pos: (_labelUv !== null && _refresh >= 0 && campaignMapLoader.item) ? campaignMapLoader.item.screen_pos_for_uv(_labelUv[0], _labelUv[1]) : Qt.point(0, 0)
  521. visible: false
  522. text: modelData.name
  523. color: (campaignMapLoader.item && campaignMapLoader.item.hover_province_id === modelData.id) ? Theme.accent : Theme.textMain
  524. font.pointSize: Theme.fontSizeSmall
  525. font.bold: true
  526. style: Text.Outline
  527. styleColor: "#101010"
  528. opacity: (campaignMapLoader.item && campaignMapLoader.item.hover_province_id === modelData.id) ? 1 : 0.85
  529. z: (campaignMapLoader.item && campaignMapLoader.item.hover_province_id === modelData.id) ? 3 : 2
  530. x: _pos.x - width / 2
  531. y: _pos.y - height / 2
  532. }
  533. }
  534. Repeater {
  535. model: missionDetailPanel.province_labels
  536. delegate: Repeater {
  537. property var _cities: (modelData && modelData.cities) ? modelData.cities : []
  538. model: _cities
  539. delegate: Item {
  540. property var cityData: modelData
  541. property var _cityUv: cityData.uv && cityData.uv.length === 2 ? cityData.uv : null
  542. property int _refresh: missionDetailPanel.label_refresh
  543. property var _pos: (_cityUv !== null && _refresh >= 0 && campaignMapLoader.item) ? campaignMapLoader.item.screen_pos_for_uv(_cityUv[0], _cityUv[1]) : Qt.point(0, 0)
  544. visible: _cityUv !== null && cityData.name && cityData.name.length > 0
  545. z: 4
  546. x: _pos.x
  547. y: _pos.y
  548. Rectangle {
  549. width: 6
  550. height: 6
  551. radius: 3
  552. color: "#f2e6c8"
  553. border.color: "#2d241c"
  554. border.width: 1
  555. x: -width / 2
  556. y: -height / 2
  557. }
  558. Text {
  559. text: cityData.name
  560. color: "#111111"
  561. font.pointSize: Theme.fontSizeTiny
  562. font.bold: true
  563. style: Text.Outline
  564. styleColor: "#f2e6c8"
  565. x: 6
  566. y: -height / 2
  567. }
  568. }
  569. }
  570. }
  571. Rectangle {
  572. id: hoverTooltip
  573. visible: campaignMap.hover_province_id !== "" && missionDetailPanel.hoverProvinceName !== ""
  574. x: Math.min(parent.width - width - Theme.spacingSmall, Math.max(Theme.spacingSmall, missionDetailPanel.hoverMouseX + 12))
  575. y: Math.min(parent.height - height - Theme.spacingSmall, Math.max(Theme.spacingSmall, missionDetailPanel.hoverMouseY + 12))
  576. radius: 6
  577. color: "#1a1a1a"
  578. border.color: "#2c2c2c"
  579. border.width: 1
  580. opacity: 0.95
  581. z: 5
  582. ColumnLayout {
  583. anchors.margins: 8
  584. anchors.fill: parent
  585. spacing: 2
  586. Label {
  587. text: missionDetailPanel.hoverProvinceName
  588. color: "#ffffff"
  589. style: Text.Outline
  590. styleColor: "#000000"
  591. font.bold: true
  592. font.pointSize: Theme.fontSizeSmall
  593. }
  594. Label {
  595. text: qsTr("Control: ") + missionDetailPanel.hoverProvinceOwner
  596. color: "#ffffff"
  597. style: Text.Outline
  598. styleColor: "#000000"
  599. font.pointSize: Theme.fontSizeTiny
  600. }
  601. }
  602. }
  603. }
  604. RowLayout {
  605. Layout.fillWidth: true
  606. spacing: Theme.spacingMedium
  607. Label {
  608. text: qsTr("Legend")
  609. color: Theme.textMain
  610. font.pointSize: Theme.fontSizeSmall
  611. font.bold: true
  612. }
  613. Repeater {
  614. model: missionDetailPanel.ownerLegend
  615. delegate: RowLayout {
  616. spacing: Theme.spacingTiny
  617. Rectangle {
  618. width: 12
  619. height: 12
  620. radius: 2
  621. color: modelData.color
  622. border.color: Theme.border
  623. border.width: 1
  624. }
  625. Label {
  626. text: modelData.name
  627. color: Theme.textSubLite
  628. font.pointSize: Theme.fontSizeSmall
  629. }
  630. }
  631. }
  632. }
  633. Rectangle {
  634. Layout.fillWidth: true
  635. Layout.preferredHeight: 1
  636. color: Theme.border
  637. }
  638. Label {
  639. text: qsTr("Select a Mission")
  640. color: Theme.textMain
  641. font.pointSize: Theme.fontSizeTitle
  642. font.bold: true
  643. Layout.fillWidth: true
  644. }
  645. ScrollView {
  646. Layout.fillWidth: true
  647. Layout.fillHeight: true
  648. clip: true
  649. ListView {
  650. id: missionListView
  651. model: missionDetailPanel.campaignData ? (missionDetailPanel.campaignData.missions || []) : []
  652. spacing: Theme.spacingMedium
  653. delegate: Rectangle {
  654. width: missionListView.width
  655. height: 80
  656. radius: Theme.radiusMedium
  657. color: missionMouseArea.containsMouse ? Theme.hoverBg : Theme.cardBase
  658. border.color: missionMouseArea.containsMouse ? Theme.selectedBr : Theme.cardBorder
  659. border.width: 1
  660. MouseArea {
  661. id: missionMouseArea
  662. anchors.fill: parent
  663. hoverEnabled: true
  664. cursorShape: Qt.PointingHandCursor
  665. onClicked: {
  666. if (missionDetailPanel.campaignData && modelData.mission_id)
  667. root.missionSelected(missionDetailPanel.campaignData.id + "/" + modelData.mission_id);
  668. }
  669. onContainsMouseChanged: {
  670. if (containsMouse && modelData.order_index !== undefined)
  671. missionDetailPanel.current_mission_index = modelData.order_index;
  672. else if (!containsMouse)
  673. missionDetailPanel.current_mission_index = 7;
  674. }
  675. }
  676. RowLayout {
  677. anchors.fill: parent
  678. anchors.margins: Theme.spacingMedium
  679. spacing: Theme.spacingMedium
  680. Label {
  681. text: (modelData.order_index + 1).toString()
  682. color: Theme.accent
  683. font.pointSize: Theme.fontSizeTitle
  684. font.bold: true
  685. Layout.preferredWidth: 40
  686. }
  687. ColumnLayout {
  688. Layout.fillWidth: true
  689. spacing: Theme.spacingTiny
  690. Label {
  691. text: modelData.mission_id || ""
  692. color: Theme.textMain
  693. font.pointSize: Theme.fontSizeLarge
  694. font.bold: true
  695. Layout.fillWidth: true
  696. }
  697. Label {
  698. text: modelData.intro_text || qsTr("Mission briefing...")
  699. color: Theme.textSubLite
  700. wrapMode: Text.WordWrap
  701. maximumLineCount: 2
  702. elide: Text.ElideRight
  703. Layout.fillWidth: true
  704. font.pointSize: Theme.fontSizeSmall
  705. }
  706. }
  707. Text {
  708. text: "›"
  709. font.pointSize: Theme.fontSizeTitle
  710. color: Theme.textHint
  711. }
  712. }
  713. Behavior on color {
  714. ColorAnimation {
  715. duration: Theme.animNormal
  716. }
  717. }
  718. Behavior on border.color {
  719. ColorAnimation {
  720. duration: Theme.animNormal
  721. }
  722. }
  723. }
  724. }
  725. }
  726. RowLayout {
  727. Layout.fillWidth: true
  728. spacing: Theme.spacingMedium
  729. Item {
  730. Layout.fillWidth: true
  731. }
  732. StyledButton {
  733. text: qsTr("Cancel")
  734. onClicked: missionDetailPanel.visible = false
  735. }
  736. }
  737. }
  738. }
  739. }
  740. }