MediterraneanMapPanel.qml 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. import QtQuick 2.15
  2. import QtQuick.Controls 2.15
  3. import QtQuick.Layouts 1.15
  4. import StandardOfIron 1.0
  5. Rectangle {
  6. id: root
  7. property var selected_mission: null
  8. property string active_region_id: selected_mission && selected_mission.world_region_id ? selected_mission.world_region_id : ""
  9. property real map_orbit_yaw: 180
  10. property real map_orbit_pitch: 90
  11. property real map_orbit_distance: 1.2
  12. property real map_pan_u: 0
  13. property real map_pan_v: 0
  14. property real terrain_height_scale: 0.15
  15. property bool show_province_fills: true
  16. property string hover_province_name: ""
  17. property string hover_province_owner: ""
  18. property real hover_mouse_x: 0
  19. property real hover_mouse_y: 0
  20. property var province_labels: []
  21. property int label_refresh: 0
  22. property var campaign_state: null
  23. property var campaign_state_sources: ["assets/campaign_map/campaign_state.json", "qrc:/assets/campaign_map/campaign_state.json", "qrc:/StandardOfIron/assets/campaign_map/campaign_state.json", "qrc:/qt/qml/StandardOfIron/assets/campaign_map/campaign_state.json"]
  24. property var owner_color_map: ({
  25. "rome": [0.82, 0.12, 0.1, 0.45],
  26. "carthage": [0.8, 0.56, 0.28, 0.45],
  27. "neutral": [0.25, 0.25, 0.25, 0.25]
  28. })
  29. property var region_camera_positions: ({
  30. "transalpine_gaul": {
  31. "yaw": 200,
  32. "pitch": 50,
  33. "distance": 2
  34. },
  35. "cisalpine_gaul": {
  36. "yaw": 185,
  37. "pitch": 48,
  38. "distance": 1.9
  39. },
  40. "etruria": {
  41. "yaw": 180,
  42. "pitch": 52,
  43. "distance": 1.8
  44. },
  45. "southern_italy": {
  46. "yaw": 175,
  47. "pitch": 50,
  48. "distance": 1.9
  49. },
  50. "carthage_core": {
  51. "yaw": 170,
  52. "pitch": 55,
  53. "distance": 2.2
  54. }
  55. })
  56. signal regionSelected(string region_id)
  57. function focus_on_region(region_id) {
  58. if (!region_id || region_id === "")
  59. return ;
  60. var camera_pos = region_camera_positions[region_id];
  61. if (camera_pos) {
  62. map_orbit_yaw = camera_pos.yaw;
  63. map_orbit_pitch = camera_pos.pitch;
  64. map_orbit_distance = camera_pos.distance;
  65. map_pan_u = 0;
  66. map_pan_v = 0;
  67. }
  68. }
  69. function load_provinces() {
  70. if (campaignMapLoader.item) {
  71. var labels = campaignMapLoader.item.province_labels;
  72. if (labels && labels.length > 0) {
  73. province_labels = labels;
  74. label_refresh += 1;
  75. apply_campaign_state();
  76. }
  77. }
  78. }
  79. function load_campaign_state() {
  80. if (campaign_state)
  81. return ;
  82. load_campaign_state_from(0);
  83. }
  84. function load_campaign_state_from(index) {
  85. if (index >= campaign_state_sources.length)
  86. return ;
  87. var xhr = new XMLHttpRequest();
  88. xhr.open("GET", campaign_state_sources[index]);
  89. xhr.onreadystatechange = function() {
  90. if (xhr.readyState !== XMLHttpRequest.DONE)
  91. return ;
  92. if (xhr.status !== 200 && xhr.status !== 0) {
  93. load_campaign_state_from(index + 1);
  94. return ;
  95. }
  96. try {
  97. var data = JSON.parse(xhr.responseText);
  98. if (data && data.provinces && data.provinces.length > 0) {
  99. if (!campaign_state) {
  100. campaign_state = data;
  101. apply_campaign_state();
  102. }
  103. }
  104. } catch (e) {
  105. load_campaign_state_from(index + 1);
  106. }
  107. };
  108. xhr.send();
  109. }
  110. function owner_color_for(owner) {
  111. var key = owner ? owner.toLowerCase() : "neutral";
  112. if (owner_color_map[key])
  113. return owner_color_map[key];
  114. return owner_color_map.neutral;
  115. }
  116. function apply_campaign_state() {
  117. if (!campaignMapLoader.item)
  118. return ;
  119. if (!campaign_state || !campaign_state.provinces)
  120. return ;
  121. var owner_by_id = {
  122. };
  123. for (var i = 0; i < campaign_state.provinces.length; i++) {
  124. var entry = campaign_state.provinces[i];
  125. if (entry && entry.id)
  126. owner_by_id[entry.id] = entry.owner || "neutral";
  127. }
  128. var entries = [];
  129. if (province_labels && province_labels.length > 0) {
  130. for (var j = 0; j < province_labels.length; j++) {
  131. var prov = province_labels[j];
  132. if (!prov || !prov.id)
  133. continue;
  134. var owner = owner_by_id[prov.id] || prov.owner || "neutral";
  135. var color = owner_color_for(owner);
  136. entries.push({
  137. "id": prov.id,
  138. "owner": owner,
  139. "color": color
  140. });
  141. }
  142. } else {
  143. for (var k = 0; k < campaign_state.provinces.length; k++) {
  144. var state_prov = campaign_state.provinces[k];
  145. if (!state_prov || !state_prov.id)
  146. continue;
  147. var fallback_owner = state_prov.owner || "neutral";
  148. var fallback_color = owner_color_for(fallback_owner);
  149. entries.push({
  150. "id": state_prov.id,
  151. "owner": fallback_owner,
  152. "color": fallback_color
  153. });
  154. }
  155. }
  156. if (entries.length > 0)
  157. campaignMapLoader.item.apply_province_state(entries);
  158. }
  159. function reset_view() {
  160. if (selected_mission && selected_mission.world_region_id) {
  161. focus_on_region(selected_mission.world_region_id);
  162. return ;
  163. }
  164. map_orbit_yaw = 180;
  165. map_orbit_pitch = 90;
  166. map_orbit_distance = 1.2;
  167. map_pan_u = 0;
  168. map_pan_v = 0;
  169. }
  170. function label_uv_for(prov) {
  171. if (prov && prov.label_uv && prov.label_uv.length === 2)
  172. return prov.label_uv;
  173. if (!prov || !prov.triangles || prov.triangles.length === 0)
  174. return null;
  175. var sum_u = 0;
  176. var sum_v = 0;
  177. var count = 0;
  178. var step = Math.max(1, Math.floor(prov.triangles.length / 200));
  179. for (var i = 0; i < prov.triangles.length; i += step) {
  180. var pt = prov.triangles[i];
  181. if (!pt || pt.length < 2)
  182. continue;
  183. sum_u += pt[0];
  184. sum_v += pt[1];
  185. count += 1;
  186. }
  187. if (count === 0)
  188. return null;
  189. return [sum_u / count, sum_v / count];
  190. }
  191. color: "#28445C"
  192. radius: Theme.radiusMedium
  193. Component.onCompleted: {
  194. load_provinces();
  195. load_campaign_state();
  196. }
  197. onSelected_missionChanged: {
  198. if (selected_mission && selected_mission.world_region_id) {
  199. focus_on_region(selected_mission.world_region_id);
  200. } else {
  201. map_orbit_yaw = 180;
  202. map_orbit_pitch = 90;
  203. map_orbit_distance = 1.2;
  204. map_pan_u = 0;
  205. map_pan_v = 0;
  206. }
  207. }
  208. onCampaign_stateChanged: {
  209. apply_campaign_state();
  210. }
  211. Item {
  212. id: mapViewport
  213. anchors.fill: parent
  214. anchors.margins: Theme.spacingSmall
  215. clip: true
  216. Loader {
  217. id: campaignMapLoader
  218. anchors.fill: parent
  219. active: root.visible && (typeof mainWindow === 'undefined' || !mainWindow.gameStarted)
  220. onStatusChanged: {
  221. if (status === Loader.Ready) {
  222. root.load_provinces();
  223. root.apply_campaign_state();
  224. }
  225. }
  226. sourceComponent: Component {
  227. CampaignMapView {
  228. id: campaign_map
  229. anchors.fill: parent
  230. orbit_yaw: root.map_orbit_yaw
  231. orbit_pitch: root.map_orbit_pitch
  232. orbit_distance: root.map_orbit_distance
  233. pan_u: root.map_pan_u
  234. pan_v: root.map_pan_v
  235. terrain_height_scale: root.terrain_height_scale
  236. show_province_fills: root.show_province_fills
  237. current_mission: root.selected_mission && root.selected_mission.order_index !== undefined ? root.selected_mission.order_index : 7
  238. hover_province_id: {
  239. if (root.active_region_id !== "")
  240. return root.active_region_id;
  241. var info = province_info_at_screen(root.hover_mouse_x, root.hover_mouse_y);
  242. return info && info.id ? info.id : "";
  243. }
  244. onOrbit_yaw_changed: root.label_refresh += 1
  245. onOrbit_pitch_changed: root.label_refresh += 1
  246. onOrbit_distance_changed: root.label_refresh += 1
  247. onPan_u_changed: root.label_refresh += 1
  248. onPan_v_changed: root.label_refresh += 1
  249. onCurrent_mission_changed: root.label_refresh += 1
  250. onWidthChanged: root.label_refresh += 1
  251. onHeightChanged: root.label_refresh += 1
  252. Behavior on orbit_yaw {
  253. NumberAnimation {
  254. duration: 600
  255. easing.type: Easing.InOutQuad
  256. }
  257. }
  258. Behavior on orbit_pitch {
  259. NumberAnimation {
  260. duration: 600
  261. easing.type: Easing.InOutQuad
  262. }
  263. }
  264. Behavior on orbit_distance {
  265. NumberAnimation {
  266. duration: 600
  267. easing.type: Easing.InOutQuad
  268. }
  269. }
  270. }
  271. }
  272. }
  273. Row {
  274. id: mapControlRow
  275. anchors.top: parent.top
  276. anchors.right: parent.right
  277. anchors.margins: Theme.spacingMedium
  278. spacing: Theme.spacingSmall
  279. z: 8
  280. visible: campaignMapLoader.item
  281. Rectangle {
  282. id: pitchDownButton
  283. height: 24
  284. width: pitchDownLabel.implicitWidth + 12
  285. radius: 4
  286. color: "#f5f0e6"
  287. border.color: "#8b7355"
  288. border.width: 1
  289. opacity: pitchDownArea.containsMouse ? 1 : 0.9
  290. Label {
  291. id: pitchDownLabel
  292. anchors.centerIn: parent
  293. text: qsTr("Tilt -")
  294. color: "#2d241c"
  295. font.pointSize: Theme.fontSizeTiny
  296. font.bold: true
  297. }
  298. MouseArea {
  299. id: pitchDownArea
  300. anchors.fill: parent
  301. hoverEnabled: true
  302. cursorShape: Qt.PointingHandCursor
  303. onClicked: root.map_orbit_pitch = Math.max(5, root.map_orbit_pitch - 5)
  304. }
  305. }
  306. Rectangle {
  307. id: pitchUpButton
  308. height: 24
  309. width: pitchUpLabel.implicitWidth + 12
  310. radius: 4
  311. color: "#f5f0e6"
  312. border.color: "#8b7355"
  313. border.width: 1
  314. opacity: pitchUpArea.containsMouse ? 1 : 0.9
  315. Label {
  316. id: pitchUpLabel
  317. anchors.centerIn: parent
  318. text: qsTr("Tilt +")
  319. color: "#2d241c"
  320. font.pointSize: Theme.fontSizeTiny
  321. font.bold: true
  322. }
  323. MouseArea {
  324. id: pitchUpArea
  325. anchors.fill: parent
  326. hoverEnabled: true
  327. cursorShape: Qt.PointingHandCursor
  328. onClicked: root.map_orbit_pitch = Math.min(90, root.map_orbit_pitch + 5)
  329. }
  330. }
  331. Rectangle {
  332. id: resetViewButton
  333. height: 24
  334. width: resetViewLabel.implicitWidth + 16
  335. radius: 4
  336. color: "#f5f0e6"
  337. border.color: "#8b7355"
  338. border.width: 1
  339. opacity: resetViewArea.containsMouse ? 1 : 0.9
  340. Label {
  341. id: resetViewLabel
  342. anchors.centerIn: parent
  343. text: qsTr("Reset view")
  344. color: "#2d241c"
  345. font.pointSize: Theme.fontSizeTiny
  346. font.bold: true
  347. }
  348. MouseArea {
  349. id: resetViewArea
  350. anchors.fill: parent
  351. hoverEnabled: true
  352. cursorShape: Qt.PointingHandCursor
  353. onClicked: root.reset_view()
  354. }
  355. }
  356. }
  357. MouseArea {
  358. property real last_x: 0
  359. property real last_y: 0
  360. property real drag_distance: 0
  361. anchors.fill: parent
  362. hoverEnabled: true
  363. acceptedButtons: Qt.LeftButton | Qt.RightButton
  364. onPressed: function(mouse) {
  365. last_x = mouse.x;
  366. last_y = mouse.y;
  367. drag_distance = 0;
  368. }
  369. onPositionChanged: function(mouse) {
  370. var dx = mouse.x - last_x;
  371. var dy = mouse.y - last_y;
  372. if ((mouse.buttons & Qt.RightButton) || (mouse.buttons & Qt.LeftButton && (mouse.modifiers & Qt.ShiftModifier))) {
  373. var pan_scale = 0.0015 * root.map_orbit_distance;
  374. root.map_pan_u -= dx * pan_scale;
  375. root.map_pan_v += dy * pan_scale;
  376. } else if (mouse.buttons & Qt.LeftButton) {
  377. root.map_orbit_yaw += dx * 0.4;
  378. root.map_orbit_pitch = Math.max(5, Math.min(90, root.map_orbit_pitch + dy * 0.4));
  379. }
  380. drag_distance += Math.abs(dx) + Math.abs(dy);
  381. last_x = mouse.x;
  382. last_y = mouse.y;
  383. root.hover_mouse_x = mouse.x;
  384. root.hover_mouse_y = mouse.y;
  385. if (root.active_region_id === "" && campaignMapLoader.item) {
  386. var info = campaignMapLoader.item.province_info_at_screen(mouse.x, mouse.y);
  387. var id = info && info.id ? info.id : "";
  388. root.hover_province_name = info && info.name ? info.name : "";
  389. root.hover_province_owner = info && info.owner ? info.owner : "";
  390. }
  391. }
  392. onExited: {
  393. if (root.active_region_id === "") {
  394. root.hover_province_name = "";
  395. root.hover_province_owner = "";
  396. }
  397. }
  398. onReleased: function(mouse) {
  399. if (mouse.button !== Qt.LeftButton)
  400. return ;
  401. if (drag_distance > 6)
  402. return ;
  403. if (!campaignMapLoader.item)
  404. return ;
  405. var info = campaignMapLoader.item.province_info_at_screen(mouse.x, mouse.y);
  406. var id = info && info.id ? info.id : "";
  407. if (id !== "")
  408. root.regionSelected(id);
  409. }
  410. onWheel: function(wheel) {
  411. var step = wheel.angleDelta.y > 0 ? 0.9 : 1.1;
  412. var next_distance = root.map_orbit_distance * step;
  413. if (campaignMapLoader.item)
  414. root.map_orbit_distance = Math.min(campaignMapLoader.item.max_orbit_distance, Math.max(campaignMapLoader.item.min_orbit_distance, next_distance));
  415. wheel.accepted = true;
  416. }
  417. }
  418. Repeater {
  419. model: root.province_labels
  420. delegate: Repeater {
  421. property var _cities: (modelData && modelData.cities) ? modelData.cities : []
  422. model: _cities
  423. delegate: Item {
  424. property var city_data: modelData
  425. property var _city_uv: city_data.uv && city_data.uv.length === 2 ? city_data.uv : null
  426. property int _refresh: root.label_refresh
  427. property var _pos: (_city_uv !== null && _refresh >= 0 && campaignMapLoader.item) ? campaignMapLoader.item.screen_pos_for_uv(_city_uv[0], _city_uv[1]) : Qt.point(0, 0)
  428. visible: _city_uv !== null && city_data.name && city_data.name.length > 0
  429. z: 4
  430. x: _pos.x
  431. y: _pos.y
  432. Rectangle {
  433. width: 6
  434. height: 6
  435. radius: 3
  436. color: "#f2e6c8"
  437. border.color: "#2d241c"
  438. border.width: 1
  439. x: -width / 2
  440. y: -height / 2
  441. }
  442. Text {
  443. text: city_data.name
  444. color: "#111111"
  445. font.pointSize: Theme.fontSizeTiny
  446. font.bold: true
  447. style: Text.Outline
  448. styleColor: "#f2e6c8"
  449. x: 6
  450. y: -height / 2
  451. }
  452. }
  453. }
  454. }
  455. Repeater {
  456. id: missionMarkerRepeater
  457. property var mission_region_map: ({
  458. "transalpine_gaul": {
  459. "uv": [0.28, 0.35],
  460. "name": "Rhône"
  461. },
  462. "cisalpine_gaul": {
  463. "uv": [0.42, 0.38],
  464. "name": "N. Italy"
  465. },
  466. "etruria": {
  467. "uv": [0.44, 0.48],
  468. "name": "Trasimene"
  469. },
  470. "southern_italy": {
  471. "uv": [0.5, 0.53],
  472. "name": "Cannae"
  473. },
  474. "carthage_core": {
  475. "uv": [0.4, 0.78],
  476. "name": "Zama"
  477. }
  478. })
  479. model: root.selected_mission ? 1 : 0
  480. delegate: Item {
  481. property var region_info: missionMarkerRepeater.mission_region_map[root.active_region_id] || null
  482. property var marker_uv: region_info ? region_info.uv : null
  483. property int _refresh: root.label_refresh
  484. property var _pos: (marker_uv !== null && _refresh >= 0 && campaignMapLoader.item) ? campaignMapLoader.item.screen_pos_for_uv(marker_uv[0], marker_uv[1]) : Qt.point(0, 0)
  485. visible: marker_uv !== null && root.active_region_id !== ""
  486. z: 6
  487. x: _pos.x
  488. y: _pos.y
  489. Rectangle {
  490. width: 24
  491. height: 24
  492. radius: 12
  493. color: "#cc8f47"
  494. border.color: "#ffffff"
  495. border.width: 2
  496. x: -width / 2
  497. y: -height / 2
  498. opacity: 0.9
  499. Text {
  500. anchors.centerIn: parent
  501. text: "⚔"
  502. color: "#ffffff"
  503. font.pointSize: Theme.fontSizeSmall
  504. font.bold: true
  505. }
  506. SequentialAnimation on scale {
  507. loops: Animation.Infinite
  508. running: visible
  509. NumberAnimation {
  510. from: 1
  511. to: 1.15
  512. duration: 800
  513. easing.type: Easing.InOutQuad
  514. }
  515. NumberAnimation {
  516. from: 1.15
  517. to: 1
  518. duration: 800
  519. easing.type: Easing.InOutQuad
  520. }
  521. }
  522. }
  523. Text {
  524. anchors.horizontalCenter: parent.horizontalCenter
  525. anchors.top: parent.top
  526. anchors.topMargin: -24
  527. text: region_info ? region_info.name : ""
  528. color: "#ffffff"
  529. font.pointSize: Theme.fontSizeSmall
  530. font.bold: true
  531. style: Text.Outline
  532. styleColor: "#000000"
  533. }
  534. }
  535. }
  536. Item {
  537. id: hannibalIcon
  538. property int _refresh: root.label_refresh
  539. property var _pos: (_refresh >= 0 && campaignMapLoader.item) ? campaignMapLoader.item.hannibal_icon_position() : Qt.point(0, 0)
  540. 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"]
  541. property int _iconIndex: 0
  542. visible: campaignMapLoader.item && _pos.x > 0 && _pos.y > 0 && root.selected_mission
  543. z: 10
  544. x: _pos.x
  545. y: _pos.y
  546. Rectangle {
  547. width: 44
  548. height: 44
  549. x: -width / 2
  550. y: -height / 2
  551. radius: 6
  552. color: "#2a1f1a"
  553. border.color: "#d4a857"
  554. border.width: 2
  555. opacity: 0.95
  556. Rectangle {
  557. anchors.fill: parent
  558. anchors.margins: 2
  559. radius: 4
  560. color: "transparent"
  561. border.color: "#6b4423"
  562. border.width: 1
  563. }
  564. }
  565. Image {
  566. source: hannibalIcon._iconSources[hannibalIcon._iconIndex]
  567. width: 36
  568. height: 36
  569. x: -width / 2
  570. y: -height / 2
  571. smooth: true
  572. mipmap: true
  573. fillMode: Image.PreserveAspectFit
  574. cache: true
  575. asynchronous: false
  576. onStatusChanged: {
  577. if (status === Image.Error && hannibalIcon._iconIndex + 1 < hannibalIcon._iconSources.length) {
  578. hannibalIcon._iconIndex += 1;
  579. source = hannibalIcon._iconSources[hannibalIcon._iconIndex];
  580. }
  581. }
  582. }
  583. Rectangle {
  584. width: 50
  585. height: 50
  586. x: -width / 2
  587. y: -height / 2
  588. radius: width / 2
  589. color: "transparent"
  590. border.color: "#d4a857"
  591. border.width: 2
  592. opacity: 0.4
  593. SequentialAnimation on opacity {
  594. loops: Animation.Infinite
  595. running: hannibalIcon.visible
  596. NumberAnimation {
  597. from: 0.4
  598. to: 0
  599. duration: 1500
  600. easing.type: Easing.OutCubic
  601. }
  602. PauseAnimation {
  603. duration: 500
  604. }
  605. }
  606. SequentialAnimation on scale {
  607. loops: Animation.Infinite
  608. running: hannibalIcon.visible
  609. NumberAnimation {
  610. from: 1
  611. to: 1.3
  612. duration: 1500
  613. easing.type: Easing.OutCubic
  614. }
  615. NumberAnimation {
  616. from: 1.3
  617. to: 1
  618. duration: 0
  619. }
  620. PauseAnimation {
  621. duration: 500
  622. }
  623. }
  624. }
  625. }
  626. Rectangle {
  627. id: hover_tooltip
  628. visible: (root.active_region_id !== "" || (campaignMapLoader.item && campaignMapLoader.item.hover_province_id !== "" && root.hover_province_name !== "")) && root.active_region_id === ""
  629. x: Math.min(parent.width - width - Theme.spacingSmall, Math.max(Theme.spacingSmall, root.hover_mouse_x + 12))
  630. y: Math.min(parent.height - height - Theme.spacingSmall, Math.max(Theme.spacingSmall, root.hover_mouse_y + 12))
  631. width: tooltip_layout.implicitWidth + 16
  632. height: tooltip_layout.implicitHeight + 16
  633. radius: 4
  634. color: "#f5f0e6"
  635. border.color: "#8b7355"
  636. border.width: 2
  637. opacity: 0.95
  638. z: 10
  639. ColumnLayout {
  640. id: tooltip_layout
  641. anchors.centerIn: parent
  642. spacing: 2
  643. Label {
  644. text: root.hover_province_name
  645. color: "#2d241c"
  646. font.bold: true
  647. font.pointSize: Theme.fontSizeSmall
  648. }
  649. Label {
  650. text: qsTr("Control: ") + root.hover_province_owner
  651. color: "#4a3f32"
  652. font.pointSize: Theme.fontSizeTiny
  653. }
  654. }
  655. }
  656. Rectangle {
  657. anchors.left: parent.left
  658. anchors.bottom: parent.bottom
  659. anchors.margins: Theme.spacingMedium
  660. width: legend_layout.implicitWidth + 16
  661. height: legend_layout.implicitHeight + 16
  662. radius: 4
  663. color: "#f5f0e6"
  664. border.color: "#8b7355"
  665. border.width: 2
  666. opacity: 0.95
  667. ColumnLayout {
  668. id: legend_layout
  669. anchors.centerIn: parent
  670. spacing: Theme.spacingTiny
  671. Label {
  672. text: qsTr("Legend")
  673. color: "#2d241c"
  674. font.pointSize: Theme.fontSizeSmall
  675. font.bold: true
  676. }
  677. Repeater {
  678. model: [{
  679. "name": qsTr("Rome"),
  680. "color": "#d01f1a"
  681. }, {
  682. "name": qsTr("Carthage"),
  683. "color": "#cc8f47"
  684. }, {
  685. "name": qsTr("Neutral"),
  686. "color": "#3a3a3a"
  687. }]
  688. delegate: RowLayout {
  689. spacing: Theme.spacingTiny
  690. Rectangle {
  691. width: 12
  692. height: 12
  693. radius: 2
  694. color: modelData.color
  695. border.color: "#5a4a3a"
  696. border.width: 1
  697. }
  698. Label {
  699. text: modelData.name
  700. color: "#4a3f32"
  701. font.pointSize: Theme.fontSizeTiny
  702. }
  703. }
  704. }
  705. }
  706. }
  707. Label {
  708. anchors.right: parent.right
  709. anchors.bottom: parent.bottom
  710. anchors.margins: Theme.spacingMedium
  711. text: qsTr("🖱️ Drag to rotate • Shift/Right-drag to pan • Scroll to zoom")
  712. color: "#4a3f32"
  713. font.pointSize: Theme.fontSizeTiny
  714. style: Text.Outline
  715. styleColor: "#f5f0e6"
  716. }
  717. Rectangle {
  718. visible: root.active_region_id !== ""
  719. anchors.right: parent.right
  720. anchors.top: parent.top
  721. anchors.margins: Theme.spacingMedium
  722. width: active_region_label.implicitWidth + 16
  723. height: active_region_label.implicitHeight + 12
  724. radius: 4
  725. color: "#cc8f47"
  726. border.color: "#8b6332"
  727. border.width: 2
  728. opacity: 0.95
  729. z: 10
  730. Label {
  731. id: active_region_label
  732. anchors.centerIn: parent
  733. text: qsTr("📍 Mission Region")
  734. color: "#2d241c"
  735. font.pointSize: Theme.fontSizeSmall
  736. font.bold: true
  737. }
  738. }
  739. }
  740. }