CampaignMenu.qml 35 KB

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