CampaignScreen.qml 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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 current_campaign: null
  8. property var campaigns: []
  9. property int selected_mission_index: -1
  10. property var campaign_map_state: null
  11. signal mission_selected(string campaign_id, string mission_id)
  12. signal cancelled()
  13. function refresh_campaigns() {
  14. if (typeof game !== "undefined" && game.available_campaigns) {
  15. campaigns = game.available_campaigns;
  16. if (campaigns.length > 0 && !current_campaign)
  17. current_campaign = campaigns[0];
  18. }
  19. build_campaign_state();
  20. ensure_mission_selection();
  21. }
  22. function select_next_unlocked_mission() {
  23. if (!current_campaign || !current_campaign.missions)
  24. return ;
  25. var all_completed = current_campaign.missions.length > 0;
  26. for (var i = 0; i < current_campaign.missions.length; i++) {
  27. var mission = current_campaign.missions[i];
  28. if (mission && mission.unlocked && !mission.completed) {
  29. selected_mission_index = i;
  30. return ;
  31. }
  32. if (!mission || !mission.completed)
  33. all_completed = false;
  34. }
  35. if (all_completed) {
  36. var last_index = current_campaign.missions.length - 1;
  37. selected_mission_index = last_index;
  38. }
  39. }
  40. function ensure_mission_selection() {
  41. if (!current_campaign || !current_campaign.missions || current_campaign.missions.length === 0) {
  42. selected_mission_index = -1;
  43. return ;
  44. }
  45. if (selected_mission_index >= 0 && selected_mission_index < current_campaign.missions.length)
  46. return ;
  47. select_next_unlocked_mission();
  48. }
  49. function select_mission_by_region(region_id) {
  50. if (!current_campaign || !current_campaign.missions || !region_id)
  51. return ;
  52. for (var i = 0; i < current_campaign.missions.length; i++) {
  53. var mission = current_campaign.missions[i];
  54. if (mission.world_region_id === region_id) {
  55. selected_mission_index = i;
  56. return ;
  57. }
  58. }
  59. }
  60. function build_campaign_state() {
  61. if (!current_campaign || !current_campaign.missions) {
  62. campaign_map_state = null;
  63. return ;
  64. }
  65. var region_stats = {
  66. };
  67. for (var i = 0; i < current_campaign.missions.length; i++) {
  68. var mission = current_campaign.missions[i];
  69. if (!mission || !mission.world_region_id)
  70. continue;
  71. var region_id = mission.world_region_id;
  72. if (!region_stats[region_id])
  73. region_stats[region_id] = {
  74. "completed": false,
  75. "unlocked": false
  76. };
  77. if (mission.completed)
  78. region_stats[region_id].completed = true;
  79. if (mission.unlocked)
  80. region_stats[region_id].unlocked = true;
  81. }
  82. var provinces = [];
  83. for (var key in region_stats) {
  84. if (!region_stats.hasOwnProperty(key))
  85. continue;
  86. var state = region_stats[key];
  87. var owner = state.completed ? "carthage" : (state.unlocked ? "neutral" : "rome");
  88. provinces.push({
  89. "id": key,
  90. "owner": owner
  91. });
  92. }
  93. campaign_map_state = {
  94. "provinces": provinces
  95. };
  96. }
  97. onVisibleChanged: {
  98. if (visible && typeof game !== "undefined" && game.load_campaigns) {
  99. game.load_campaigns();
  100. refresh_campaigns();
  101. }
  102. }
  103. onCurrent_campaignChanged: {
  104. build_campaign_state();
  105. ensure_mission_selection();
  106. }
  107. anchors.fill: parent
  108. focus: true
  109. Keys.onPressed: function(event) {
  110. if (event.key === Qt.Key_Escape) {
  111. root.cancelled();
  112. event.accepted = true;
  113. }
  114. }
  115. Connections {
  116. function onAvailable_campaigns_changed() {
  117. refresh_campaigns();
  118. }
  119. target: (typeof game !== "undefined") ? game : null
  120. }
  121. Rectangle {
  122. anchors.fill: parent
  123. color: Theme.dim
  124. }
  125. Rectangle {
  126. id: container
  127. width: Math.min(parent.width * 0.95, 1600)
  128. height: Math.min(parent.height * 0.95, 1000)
  129. anchors.centerIn: parent
  130. radius: Theme.radiusPanel
  131. color: Theme.panelBase
  132. border.color: Theme.panelBr
  133. border.width: 1
  134. opacity: 0.98
  135. ColumnLayout {
  136. anchors.fill: parent
  137. anchors.margins: Theme.spacingXLarge
  138. spacing: Theme.spacingLarge
  139. RowLayout {
  140. Layout.fillWidth: true
  141. spacing: Theme.spacingMedium
  142. ColumnLayout {
  143. Layout.fillWidth: true
  144. spacing: Theme.spacingTiny
  145. Label {
  146. text: current_campaign ? current_campaign.title : qsTr("Campaign")
  147. color: Theme.textMain
  148. font.pointSize: Theme.fontSizeHero
  149. font.bold: true
  150. Layout.fillWidth: true
  151. }
  152. Label {
  153. text: current_campaign ? current_campaign.description : ""
  154. color: Theme.textSubLite
  155. font.pointSize: Theme.fontSizeMedium
  156. wrapMode: Text.WordWrap
  157. Layout.fillWidth: true
  158. maximumLineCount: 2
  159. elide: Text.ElideRight
  160. }
  161. }
  162. StyledButton {
  163. text: qsTr("← Back")
  164. onClicked: root.cancelled()
  165. }
  166. }
  167. RowLayout {
  168. Layout.fillWidth: true
  169. Layout.fillHeight: true
  170. spacing: Theme.spacingLarge
  171. Rectangle {
  172. Layout.fillWidth: true
  173. Layout.fillHeight: true
  174. Layout.preferredWidth: parent.width * 0.45
  175. radius: Theme.radiusMedium
  176. color: Theme.cardBase
  177. border.color: Theme.cardBorder
  178. border.width: 1
  179. ColumnLayout {
  180. anchors.fill: parent
  181. anchors.margins: Theme.spacingMedium
  182. spacing: Theme.spacingMedium
  183. Label {
  184. text: qsTr("Missions")
  185. color: Theme.textMain
  186. font.pointSize: Theme.fontSizeTitle
  187. font.bold: true
  188. }
  189. Rectangle {
  190. Layout.fillWidth: true
  191. implicitHeight: progress_layout.implicitHeight + Theme.spacingSmall * 2
  192. Layout.preferredHeight: implicitHeight
  193. radius: Theme.radiusSmall
  194. color: Theme.cardBase
  195. border.color: Theme.cardBorder
  196. border.width: 1
  197. ColumnLayout {
  198. id: progress_layout
  199. anchors.fill: parent
  200. anchors.margins: Theme.spacingSmall
  201. spacing: Theme.spacingSmall
  202. RowLayout {
  203. Layout.fillWidth: true
  204. spacing: Theme.spacingSmall
  205. Label {
  206. text: qsTr("Progress:")
  207. color: Theme.textMain
  208. font.pointSize: Theme.fontSizeSmall
  209. font.bold: true
  210. }
  211. Rectangle {
  212. Layout.fillWidth: true
  213. Layout.preferredHeight: 16
  214. radius: 8
  215. color: Theme.disabledBg
  216. border.color: Theme.border
  217. border.width: 1
  218. Rectangle {
  219. property int completed_count: {
  220. if (!current_campaign || !current_campaign.missions)
  221. return 0;
  222. var count = 0;
  223. for (var i = 0; i < current_campaign.missions.length; i++) {
  224. if (current_campaign.missions[i].completed)
  225. count++;
  226. }
  227. return count;
  228. }
  229. property int total_count: current_campaign && current_campaign.missions ? current_campaign.missions.length : 0
  230. property real progress_ratio: total_count > 0 ? completed_count / total_count : 0
  231. width: parent.width * progress_ratio
  232. height: parent.height
  233. radius: parent.radius
  234. color: Theme.successBg
  235. border.color: Theme.successBr
  236. border.width: 1
  237. Behavior on width {
  238. NumberAnimation {
  239. duration: Theme.animNormal
  240. }
  241. }
  242. }
  243. }
  244. Label {
  245. property int completed_count: {
  246. if (!current_campaign || !current_campaign.missions)
  247. return 0;
  248. var count = 0;
  249. for (var i = 0; i < current_campaign.missions.length; i++) {
  250. if (current_campaign.missions[i].completed)
  251. count++;
  252. }
  253. return count;
  254. }
  255. property int total_count: current_campaign && current_campaign.missions ? current_campaign.missions.length : 0
  256. text: completed_count + " / " + total_count
  257. color: Theme.textMain
  258. font.pointSize: Theme.fontSizeSmall
  259. font.bold: true
  260. }
  261. }
  262. }
  263. }
  264. ScrollView {
  265. Layout.fillWidth: true
  266. Layout.fillHeight: true
  267. clip: true
  268. ListView {
  269. id: mission_list_view
  270. model: current_campaign ? current_campaign.missions : []
  271. spacing: Theme.spacingSmall
  272. currentIndex: selected_mission_index
  273. delegate: MissionListItem {
  274. width: mission_list_view.width - Theme.spacingSmall
  275. mission_data: modelData
  276. is_selected: root.selected_mission_index === index
  277. onClicked: {
  278. selected_mission_index = index;
  279. }
  280. }
  281. }
  282. }
  283. }
  284. }
  285. Rectangle {
  286. Layout.fillWidth: true
  287. Layout.fillHeight: true
  288. Layout.preferredWidth: parent.width * 0.55
  289. radius: Theme.radiusMedium
  290. color: Theme.cardBase
  291. border.color: Theme.cardBorder
  292. border.width: 1
  293. ColumnLayout {
  294. anchors.fill: parent
  295. anchors.margins: Theme.spacingMedium
  296. spacing: Theme.spacingMedium
  297. Label {
  298. text: qsTr("Mediterranean Strategic Map")
  299. color: Theme.textMain
  300. font.pointSize: Theme.fontSizeTitle
  301. font.bold: true
  302. }
  303. MediterraneanMapPanel {
  304. Layout.fillWidth: true
  305. Layout.fillHeight: true
  306. selected_mission: selected_mission_index >= 0 && current_campaign && current_campaign.missions ? current_campaign.missions[selected_mission_index] : null
  307. campaign_state: root.campaign_map_state
  308. onRegionSelected: function(region_id) {
  309. select_mission_by_region(region_id);
  310. }
  311. }
  312. }
  313. }
  314. }
  315. MissionDetailPanel {
  316. id: mission_detail_panel
  317. Layout.fillWidth: true
  318. Layout.preferredHeight: visible ? 180 : 0
  319. visible: selected_mission_index >= 0
  320. mission_data: selected_mission_index >= 0 && current_campaign && current_campaign.missions ? current_campaign.missions[selected_mission_index] : null
  321. campaign_id: current_campaign ? current_campaign.id : ""
  322. onStart_mission_clicked: {
  323. if (current_campaign && mission_data && mission_data.mission_id)
  324. root.mission_selected(current_campaign.id, mission_data.mission_id);
  325. }
  326. Behavior on Layout.preferredHeight {
  327. NumberAnimation {
  328. duration: Theme.animNormal
  329. }
  330. }
  331. }
  332. }
  333. }
  334. }