mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
Connect roads automation (#10631)
* Start on road connect feature. * Rough UI and tile highlighting - Highlight visible tiles for selected unit red -- Maybe change this to all explored tiles - Move action firing inside WorldMapHolder - Set begin and end tiles * Serialize Vector2 instead of Tile * Add road icon * Much better UI handling - Tile highlights go away after choosing a tile - Added restrictions to allowed tile destination choices. - Explored - Land - Passable - Added two-tap button * Refactor part of `onTileClicked` for readability * Band-aid fix null pointer error * Add RoadConnection icon * Tentatively working connect road feature * AStar search implementation * AStar connect road automation * Fix worker getting stuck in city tiles * Heuristic should be between tiles * Add heuristic to road connect, remove maxSize limit * Fix predicates * Cancel automation when worker is force moved off path * Change valid/highlighted tiles to be friendly or neutral * Put log back the way it was * Fix behavior when kicked off path * Worker no longer wastes movement points * Workers will progress multiple tiles at a time towards the next build destination. * Respect civs with certain tiles as roads * Refractor ForceAutomateRoadConnection -> AutomateRoadConnection * Connect road UI button only shows for units with UniqueType.BuildImprovements * Connect road UI button only show when road tech is unlocked * Add wagon sound * Fix destination icon, add KeyboardBinding to 'c' * UI highlight connect road path tiles orange * Downsample wagon.mp3 * Apply migration patch, idiomatic sequence processing * Add notifications on success and failure * Extract movement cost function to be reusable * Refactor road pathfinding into MapPathing.kt * Make pathing calls more general for future extendability * Add UI road connection tile path preview * Keep road path highlighting when routing to a city tile * Adjust road pathing cost function * Path includes pillaged roads * Repair pillaged roads along path * Valid road path tiles now include all passable tiles (open borders)
This commit is contained in:
BIN
android/Images.Construction/UnitActionIcons/RoadConnection.png
Normal file
BIN
android/Images.Construction/UnitActionIcons/RoadConnection.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
android/ImagesToNotAddToGame/RoadConnection.xcf
Normal file
BIN
android/ImagesToNotAddToGame/RoadConnection.xcf
Normal file
Binary file not shown.
@ -622,245 +622,245 @@ BuildingIcons/Research Lab
|
||||
index: -1
|
||||
BuildingIcons/Satrap's Court
|
||||
rotate: false
|
||||
xy: 1486, 970
|
||||
xy: 1594, 1084
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Seaport
|
||||
rotate: false
|
||||
xy: 1810, 1294
|
||||
xy: 1270, 646
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Shrine
|
||||
rotate: false
|
||||
xy: 1594, 976
|
||||
xy: 1702, 1078
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Sistine Chapel
|
||||
rotate: false
|
||||
xy: 1810, 1186
|
||||
xy: 1378, 646
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Solar Plant
|
||||
rotate: false
|
||||
xy: 1702, 970
|
||||
xy: 1810, 1078
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Spaceship Factory
|
||||
rotate: false
|
||||
xy: 1810, 1078
|
||||
xy: 1486, 646
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Stable
|
||||
rotate: false
|
||||
xy: 1594, 760
|
||||
xy: 1702, 862
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Stadium
|
||||
rotate: false
|
||||
xy: 1702, 862
|
||||
xy: 1810, 970
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Statue of Liberty
|
||||
rotate: false
|
||||
xy: 1594, 652
|
||||
xy: 1702, 754
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Statue of Zeus
|
||||
rotate: false
|
||||
xy: 1702, 754
|
||||
xy: 1810, 862
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Stele
|
||||
rotate: false
|
||||
xy: 1810, 754
|
||||
xy: 1162, 538
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Stock Exchange
|
||||
rotate: false
|
||||
xy: 1162, 538
|
||||
xy: 1270, 538
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Stone Works
|
||||
rotate: false
|
||||
xy: 1270, 538
|
||||
xy: 1378, 538
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Stonehenge
|
||||
rotate: false
|
||||
xy: 1378, 538
|
||||
xy: 1486, 538
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Sydney Opera House
|
||||
rotate: false
|
||||
xy: 1702, 538
|
||||
xy: 1810, 538
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Taj Mahal
|
||||
rotate: false
|
||||
xy: 1810, 538
|
||||
xy: 730, 430
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Temple
|
||||
rotate: false
|
||||
xy: 838, 430
|
||||
xy: 946, 431
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Temple of Artemis
|
||||
rotate: false
|
||||
xy: 946, 431
|
||||
xy: 1054, 436
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Terracotta Army
|
||||
rotate: false
|
||||
xy: 1162, 430
|
||||
xy: 1270, 430
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/The Great Library
|
||||
rotate: false
|
||||
xy: 1270, 430
|
||||
xy: 1378, 430
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/The Great Lighthouse
|
||||
rotate: false
|
||||
xy: 1378, 430
|
||||
xy: 1486, 430
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/The Louvre
|
||||
rotate: false
|
||||
xy: 1486, 430
|
||||
xy: 1594, 436
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/The Oracle
|
||||
rotate: false
|
||||
xy: 1594, 436
|
||||
xy: 1702, 430
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/The Pyramids
|
||||
rotate: false
|
||||
xy: 1702, 430
|
||||
xy: 1810, 430
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Theatre
|
||||
rotate: false
|
||||
xy: 1810, 430
|
||||
xy: 1940, 1618
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/United Nations
|
||||
rotate: false
|
||||
xy: 1918, 1185
|
||||
xy: 1918, 1077
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/University
|
||||
rotate: false
|
||||
xy: 1918, 1077
|
||||
xy: 1918, 969
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Utopia Project
|
||||
rotate: false
|
||||
xy: 1918, 969
|
||||
xy: 1918, 861
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Walls
|
||||
rotate: false
|
||||
xy: 1918, 429
|
||||
xy: 760, 322
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Walls of Babylon
|
||||
rotate: false
|
||||
xy: 760, 322
|
||||
xy: 760, 214
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Wat
|
||||
rotate: false
|
||||
xy: 868, 214
|
||||
xy: 868, 106
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Water Mill
|
||||
rotate: false
|
||||
xy: 868, 106
|
||||
xy: 976, 323
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Windmill
|
||||
rotate: false
|
||||
xy: 976, 323
|
||||
xy: 976, 215
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
BuildingIcons/Workshop
|
||||
rotate: false
|
||||
xy: 1084, 322
|
||||
xy: 1084, 214
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/0
|
||||
rotate: false
|
||||
xy: 1351, 372
|
||||
xy: 2014, 1934
|
||||
size: 25, 50
|
||||
orig: 25, 50
|
||||
offset: 0, 0
|
||||
@ -874,35 +874,35 @@ MayaCalendar/1
|
||||
index: -1
|
||||
MayaCalendar/10
|
||||
rotate: false
|
||||
xy: 1316, 256
|
||||
xy: 1300, 314
|
||||
size: 21, 50
|
||||
orig: 21, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/11
|
||||
rotate: false
|
||||
xy: 1308, 372
|
||||
xy: 924, 48
|
||||
size: 35, 50
|
||||
orig: 35, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/12
|
||||
rotate: false
|
||||
xy: 924, 48
|
||||
xy: 1464, 372
|
||||
size: 35, 50
|
||||
orig: 35, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/13
|
||||
rotate: false
|
||||
xy: 1240, 256
|
||||
xy: 1507, 372
|
||||
size: 35, 50
|
||||
orig: 35, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/14
|
||||
rotate: false
|
||||
xy: 1298, 314
|
||||
xy: 1550, 372
|
||||
size: 35, 50
|
||||
orig: 35, 50
|
||||
offset: 0, 0
|
||||
@ -923,21 +923,21 @@ MayaCalendar/16
|
||||
index: -1
|
||||
MayaCalendar/17
|
||||
rotate: false
|
||||
xy: 876, 48
|
||||
xy: 1250, 264
|
||||
size: 40, 50
|
||||
orig: 40, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/18
|
||||
rotate: false
|
||||
xy: 1192, 256
|
||||
xy: 876, 48
|
||||
size: 40, 50
|
||||
orig: 40, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/19
|
||||
rotate: false
|
||||
xy: 1250, 314
|
||||
xy: 1416, 372
|
||||
size: 40, 50
|
||||
orig: 40, 50
|
||||
offset: 0, 0
|
||||
@ -951,14 +951,14 @@ MayaCalendar/2
|
||||
index: -1
|
||||
MayaCalendar/3
|
||||
rotate: false
|
||||
xy: 1374, 314
|
||||
xy: 1329, 314
|
||||
size: 13, 50
|
||||
orig: 13, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/4
|
||||
rotate: false
|
||||
xy: 2014, 1876
|
||||
xy: 2026, 1394
|
||||
size: 13, 50
|
||||
orig: 13, 50
|
||||
offset: 0, 0
|
||||
@ -972,28 +972,28 @@ MayaCalendar/5
|
||||
index: -1
|
||||
MayaCalendar/6
|
||||
rotate: false
|
||||
xy: 1283, 256
|
||||
xy: 2014, 1876
|
||||
size: 25, 50
|
||||
orig: 25, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/7
|
||||
rotate: false
|
||||
xy: 1341, 314
|
||||
xy: 1192, 206
|
||||
size: 25, 50
|
||||
orig: 25, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/8
|
||||
rotate: false
|
||||
xy: 2014, 1934
|
||||
xy: 1225, 206
|
||||
size: 25, 50
|
||||
orig: 25, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/9
|
||||
rotate: false
|
||||
xy: 1384, 372
|
||||
xy: 1258, 206
|
||||
size: 25, 50
|
||||
orig: 25, 50
|
||||
offset: 0, 0
|
||||
@ -1007,21 +1007,21 @@ MayaCalendar/Baktun
|
||||
index: -1
|
||||
MayaCalendar/Katun
|
||||
rotate: false
|
||||
xy: 1084, 156
|
||||
xy: 1192, 264
|
||||
size: 50, 50
|
||||
orig: 50, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/Maya
|
||||
rotate: false
|
||||
xy: 818, 48
|
||||
xy: 1300, 372
|
||||
size: 50, 50
|
||||
orig: 50, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
MayaCalendar/Tun
|
||||
rotate: false
|
||||
xy: 1250, 372
|
||||
xy: 1358, 372
|
||||
size: 50, 50
|
||||
orig: 50, 50
|
||||
offset: 0, 0
|
||||
@ -1063,21 +1063,21 @@ OtherIcons/ConvertScience
|
||||
index: -1
|
||||
OtherIcons/WLTK 1
|
||||
rotate: false
|
||||
xy: 1918, 861
|
||||
xy: 1918, 753
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
OtherIcons/WLTK 2
|
||||
rotate: false
|
||||
xy: 1918, 753
|
||||
xy: 1918, 645
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
OtherIcons/WLTK LR
|
||||
rotate: false
|
||||
xy: 1918, 645
|
||||
xy: 1918, 537
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
@ -1182,7 +1182,7 @@ UnitActionIcons/FoundCity
|
||||
index: -1
|
||||
UnitActionIcons/HideMore
|
||||
rotate: false
|
||||
xy: 1192, 372
|
||||
xy: 1084, 156
|
||||
size: 50, 50
|
||||
orig: 50, 50
|
||||
offset: 0, 0
|
||||
@ -1196,14 +1196,14 @@ UnitActionIcons/HurryResearch
|
||||
index: -1
|
||||
UnitActionIcons/MoveTo
|
||||
rotate: false
|
||||
xy: 1192, 314
|
||||
xy: 818, 48
|
||||
size: 50, 50
|
||||
orig: 50, 50
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitActionIcons/ShowMore
|
||||
rotate: false
|
||||
xy: 1192, 314
|
||||
xy: 818, 48
|
||||
size: 50, 50
|
||||
orig: 50, 50
|
||||
offset: 0, 0
|
||||
@ -1250,16 +1250,23 @@ UnitActionIcons/RemoveHeresy
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitActionIcons/RoadConnection
|
||||
rotate: false
|
||||
xy: 1594, 1192
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitActionIcons/SetUp
|
||||
rotate: false
|
||||
xy: 1270, 646
|
||||
xy: 1378, 754
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitActionIcons/Sleep
|
||||
rotate: false
|
||||
xy: 1486, 754
|
||||
xy: 1594, 868
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
@ -1273,35 +1280,28 @@ UnitActionIcons/Star
|
||||
index: -1
|
||||
UnitActionIcons/StartGoldenAge
|
||||
rotate: false
|
||||
xy: 1810, 970
|
||||
xy: 1594, 652
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitActionIcons/Stop
|
||||
rotate: false
|
||||
xy: 1486, 538
|
||||
xy: 1594, 544
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitActionIcons/StopMove
|
||||
rotate: false
|
||||
xy: 1486, 538
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitActionIcons/ShowUnitDestination
|
||||
rotate: false
|
||||
xy: 980, 0
|
||||
xy: 1594, 544
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitActionIcons/Swap
|
||||
rotate: false
|
||||
xy: 1702, 646
|
||||
xy: 1810, 646
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
@ -1315,7 +1315,7 @@ UnitActionIcons/Transform
|
||||
index: -1
|
||||
UnitActionIcons/Wait
|
||||
rotate: false
|
||||
xy: 1918, 537
|
||||
xy: 1918, 429
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
@ -2071,203 +2071,203 @@ UnitIcons/Rifleman
|
||||
index: -1
|
||||
UnitIcons/Rocket Artillery
|
||||
rotate: false
|
||||
xy: 1594, 1192
|
||||
xy: 1702, 1294
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/SS Booster
|
||||
rotate: false
|
||||
xy: 1702, 1294
|
||||
xy: 1810, 1402
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/SS Cockpit
|
||||
rotate: false
|
||||
xy: 1810, 1402
|
||||
xy: 1162, 646
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/SS Engine
|
||||
rotate: false
|
||||
xy: 1162, 646
|
||||
xy: 1270, 754
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/SS Stasis Chamber
|
||||
rotate: false
|
||||
xy: 1270, 754
|
||||
xy: 1378, 862
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Samurai
|
||||
rotate: false
|
||||
xy: 1378, 862
|
||||
xy: 1486, 970
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Scout
|
||||
rotate: false
|
||||
xy: 1594, 1084
|
||||
xy: 1702, 1186
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Sea Beggar
|
||||
rotate: false
|
||||
xy: 1702, 1186
|
||||
xy: 1810, 1294
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Settler
|
||||
rotate: false
|
||||
xy: 1378, 754
|
||||
xy: 1486, 862
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Ship of the Line
|
||||
rotate: false
|
||||
xy: 1486, 862
|
||||
xy: 1594, 976
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Sipahi
|
||||
rotate: false
|
||||
xy: 1702, 1078
|
||||
xy: 1810, 1186
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Skirmisher
|
||||
rotate: false
|
||||
xy: 1378, 646
|
||||
xy: 1486, 754
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Slinger
|
||||
rotate: false
|
||||
xy: 1594, 868
|
||||
xy: 1702, 970
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Spearman
|
||||
rotate: false
|
||||
xy: 1486, 646
|
||||
xy: 1594, 760
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Stealth Bomber
|
||||
rotate: false
|
||||
xy: 1810, 862
|
||||
xy: 1810, 754
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Submarine
|
||||
rotate: false
|
||||
xy: 1594, 544
|
||||
xy: 1702, 646
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Swordsman
|
||||
rotate: false
|
||||
xy: 1810, 646
|
||||
xy: 1702, 538
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Tank
|
||||
rotate: false
|
||||
xy: 730, 430
|
||||
xy: 838, 430
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Tercio
|
||||
rotate: false
|
||||
xy: 1054, 436
|
||||
xy: 1162, 430
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Trebuchet
|
||||
rotate: false
|
||||
xy: 1940, 1618
|
||||
xy: 1940, 1510
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Triplane
|
||||
rotate: false
|
||||
xy: 1940, 1510
|
||||
xy: 1918, 1402
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Trireme
|
||||
rotate: false
|
||||
xy: 1918, 1401
|
||||
xy: 1918, 1293
|
||||
size: 100, 101
|
||||
orig: 100, 101
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Turtle Ship
|
||||
rotate: false
|
||||
xy: 1918, 1293
|
||||
xy: 1918, 1185
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/War Chariot
|
||||
rotate: false
|
||||
xy: 760, 214
|
||||
xy: 760, 106
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/War Elephant
|
||||
rotate: false
|
||||
xy: 760, 106
|
||||
xy: 868, 322
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Warrior
|
||||
rotate: false
|
||||
xy: 868, 322
|
||||
xy: 868, 214
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Work Boats
|
||||
rotate: false
|
||||
xy: 976, 215
|
||||
xy: 976, 107
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Worker
|
||||
rotate: false
|
||||
xy: 976, 107
|
||||
xy: 1084, 322
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
index: -1
|
||||
UnitIcons/Zero
|
||||
rotate: false
|
||||
xy: 1084, 214
|
||||
xy: 1192, 322
|
||||
size: 100, 100
|
||||
orig: 100, 100
|
||||
offset: 0, 0
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1021 KiB After Width: | Height: | Size: 904 KiB |
BIN
android/assets/sounds/wagon.mp3
Normal file
BIN
android/assets/sounds/wagon.mp3
Normal file
Binary file not shown.
@ -34,6 +34,9 @@ object CivilianUnitAutomation {
|
||||
if (unit.hasUnique(UniqueType.FoundCity))
|
||||
return SpecificUnitAutomation.automateSettlerActions(unit, tilesWhereWeWillBeCaptured)
|
||||
|
||||
if(unit.isAutomatingRoadConnection())
|
||||
return unit.civ.getWorkerAutomation().automateConnectRoad(unit, tilesWhereWeWillBeCaptured)
|
||||
|
||||
if (unit.cache.hasUniqueToBuildImprovements)
|
||||
return unit.civ.getWorkerAutomation().automateWorkerAction(unit, tilesWhereWeWillBeCaptured)
|
||||
|
||||
|
@ -10,8 +10,10 @@ import com.unciv.logic.automation.unit.UnitAutomation.wander
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.NotificationCategory
|
||||
import com.unciv.logic.civilization.NotificationIcon
|
||||
import com.unciv.logic.map.BFS
|
||||
import com.unciv.logic.map.HexMath
|
||||
import com.unciv.logic.map.MapPathing
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.RoadStatus
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
@ -55,6 +57,9 @@ class WorkerAutomation(
|
||||
RoadStatus.None
|
||||
else civInfo.tech.getBestRoadAvailable()
|
||||
|
||||
/** Same as above, but ignores the option */
|
||||
private val actualBestRoadAvailable: RoadStatus = civInfo.tech.getBestRoadAvailable()
|
||||
|
||||
/** Civ-wide list of unconnected Cities, sorted by closest to capital first */
|
||||
private val citiesThatNeedConnecting: List<City> by lazy {
|
||||
val result = civInfo.cities.asSequence()
|
||||
@ -115,6 +120,132 @@ class WorkerAutomation(
|
||||
|
||||
|
||||
///////////////////////////////////////// Methods /////////////////////////////////////////
|
||||
|
||||
|
||||
/**
|
||||
* Automate the process of connecting a road between two points.
|
||||
* Current thoughts:
|
||||
* Will be a special case of MapUnit.automated property
|
||||
* Unit has new attributes startTile endTile
|
||||
* - We will progress towards the end path sequentially, taking absolute least distance w/o regard for movement cost
|
||||
* - Cancel upon risk of capture
|
||||
* - Cancel upon blocked
|
||||
* - End automation upon finish
|
||||
*/
|
||||
// TODO: Caching
|
||||
// TODO: Hide the automate road button if road is not unlocked
|
||||
fun automateConnectRoad(unit: MapUnit, tilesWhereWeWillBeCaptured: Set<Tile>){
|
||||
if (actualBestRoadAvailable == RoadStatus.None) return
|
||||
|
||||
var currentTile = unit.getTile()
|
||||
|
||||
/** Reset side effects from automation, return worker to non-automated state*/
|
||||
fun stopAndCleanAutomation(){
|
||||
unit.automated = false
|
||||
unit.action = null
|
||||
unit.automatedRoadConnectionDestination = null
|
||||
unit.automatedRoadConnectionPath = null
|
||||
currentTile.stopWorkingOnImprovement()
|
||||
}
|
||||
|
||||
/** Conditions for whether it is acceptable to build a road on this tile */
|
||||
fun shouldBuildRoadOnTile(tile: Tile): Boolean {
|
||||
return !tile.isCityCenter() // Can't build road on city tiles
|
||||
// Special case for civs that treat forest/jungles as roads (inside their territory). We shouldn't build if railroads aren't unlocked.
|
||||
&& !(tile.hasConnection(unit.civ) && actualBestRoadAvailable == RoadStatus.Road)
|
||||
// Build (upgrade) if possible
|
||||
&& tile.roadStatus != actualBestRoadAvailable
|
||||
// Build if the road is pillaged
|
||||
|| tile.roadIsPillaged
|
||||
}
|
||||
|
||||
val destinationTile = unit.civ.gameInfo.tileMap[unit.automatedRoadConnectionDestination!!]
|
||||
|
||||
var pathToDest: List<Vector2>? = unit.automatedRoadConnectionPath
|
||||
|
||||
// The path does not exist, create it
|
||||
if (pathToDest == null) {
|
||||
val foundPath: List<Tile>? = MapPathing.getRoadPath(unit, currentTile, destinationTile)
|
||||
if (foundPath == null) {
|
||||
Log.debug("WorkerAutomation: ${unit.label()} -> connect road failed")
|
||||
stopAndCleanAutomation()
|
||||
unit.civ.addNotification("Connect road failed!", currentTile.position, NotificationCategory.Units, NotificationIcon.Construction)
|
||||
return
|
||||
} else {
|
||||
pathToDest = foundPath // Convert to a list of positions for serialization
|
||||
.map { it.position }
|
||||
|
||||
unit.automatedRoadConnectionPath = pathToDest
|
||||
debug("WorkerAutomation: ${unit.label()} -> found connect road path to destination tile: %s, %s", destinationTile, pathToDest)
|
||||
}
|
||||
}
|
||||
|
||||
val currTileIndex = pathToDest.indexOf(currentTile.position)
|
||||
|
||||
// The worker was somehow moved off its path, cancel the action
|
||||
if (currTileIndex == -1) {
|
||||
Log.debug("${unit.label()} -> was moved off its connect road path. Operation cancelled.")
|
||||
stopAndCleanAutomation()
|
||||
unit.civ.addNotification("Connect road cancelled!", currentTile.position, NotificationCategory.Units, unit.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (unit.currentMovement > 0) {
|
||||
/* Can not build a road on this tile, try to move on.
|
||||
* The worker should search for the next furthest tile in the path that:
|
||||
* - It can move to
|
||||
* - Can be improved/upgraded
|
||||
* */
|
||||
if (!shouldBuildRoadOnTile(currentTile)) {
|
||||
when {
|
||||
currTileIndex < pathToDest.size - 1 -> { // Try to move to the next tile in the path
|
||||
val tileMap = unit.civ.gameInfo.tileMap
|
||||
var nextTile: Tile = currentTile
|
||||
|
||||
// Create a new list with tiles where the index is greater than currTileIndex
|
||||
val futureTiles = pathToDest.asSequence()
|
||||
.dropWhile { it != unit.currentTile.position }
|
||||
.drop(1)
|
||||
.map { tileMap[it] }
|
||||
|
||||
for (futureTile in futureTiles){ // Find the furthest tile we can reach in this turn, move to, and does not have a road
|
||||
if (unit.movement.canReachInCurrentTurn(futureTile) && unit.movement.canMoveTo(futureTile)) { // We can at least move to this tile
|
||||
nextTile = futureTile
|
||||
if (shouldBuildRoadOnTile(futureTile)) {
|
||||
break // Stop on this tile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unit.movement.moveToTile(nextTile)
|
||||
currentTile = unit.getTile()
|
||||
}
|
||||
currTileIndex == pathToDest.size - 1 -> { // The last tile in the path is unbuildable or has a road.
|
||||
stopAndCleanAutomation()
|
||||
unit.civ.addNotification("Connect road completed!", currentTile.position, NotificationCategory.Units, unit.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to check current movement again after we've (potentially) moved
|
||||
if (unit.currentMovement > 0) {
|
||||
// Repair pillaged roads first
|
||||
if(currentTile.roadStatus != RoadStatus.None && currentTile.roadIsPillaged){
|
||||
currentTile.setRepaired()
|
||||
return
|
||||
}
|
||||
if (shouldBuildRoadOnTile(currentTile) && currentTile.improvementInProgress != actualBestRoadAvailable.name) {
|
||||
val improvement = actualBestRoadAvailable.improvement(ruleSet)!!
|
||||
currentTile.startWorkingOnImprovement(improvement, civInfo, unit)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Automate one Worker - decide what to do and where, move, start or continue work.
|
||||
*/
|
||||
|
185
core/src/com/unciv/logic/map/AStar.kt
Normal file
185
core/src/com/unciv/logic/map/AStar.kt
Normal file
@ -0,0 +1,185 @@
|
||||
package com.unciv.logic.map
|
||||
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import java.util.PriorityQueue
|
||||
|
||||
|
||||
data class TilePriority(val tile: Tile, val priority: Float)
|
||||
|
||||
/**
|
||||
* AStar is an implementation of the A* search algorithm, commonly used for finding the shortest path
|
||||
* in a weighted graph.
|
||||
*
|
||||
* The algorithm maintains a priority queue of paths while exploring the graph, expanding paths in
|
||||
* order of their estimated total cost from the start node to the goal node, factoring in both the
|
||||
* cost so far and an estimated cost (heuristic) to the goal.
|
||||
*
|
||||
* @param startingPoint The initial tile where the search begins.
|
||||
* @param predicate A function that determines if a tile should be considered for further exploration.
|
||||
* For instance, it might return `true` for passable tiles and `false` for obstacles.
|
||||
* @param cost A function that takes two tiles (fromTile, toTile) as input and returns the cost
|
||||
* of moving from 'fromTile' to 'toTile' as a Float. This allows for flexible cost
|
||||
* calculations based on different criteria, such as distance, terrain, or other
|
||||
* custom logic defined by the user.
|
||||
* @param heuristic A function that estimates the cost from a given tile to the goal. For the A*
|
||||
* algorithm to guarantee the shortest path, this heuristic must be admissible,
|
||||
* meaning it should never overestimate the actual cost to reach the goal.
|
||||
* You can set this to `{ tile -> 0 }` for Djikstra's algorithm.
|
||||
*
|
||||
* Usage Example:
|
||||
* ```
|
||||
* val unit: MapUnit = ...
|
||||
* val aStarSearch = AStar(startTile,
|
||||
* { tile -> tile.isPassable },
|
||||
* { from: Tile, to: Tile -> MovementCost.getMovementCostBetweenAdjacentTiles(unit, from, to)},
|
||||
* { tile -> <custom heuristic> })
|
||||
*
|
||||
* val path = aStarSearch.findPath(goalTile)
|
||||
* ```
|
||||
*/
|
||||
class AStar(
|
||||
val startingPoint: Tile,
|
||||
private val predicate : (Tile) -> Boolean,
|
||||
private val cost: (Tile, Tile) -> Float,
|
||||
private val heuristic : (Tile, Tile) -> Float,
|
||||
) {
|
||||
/** Maximum number of tiles to search */
|
||||
var maxSize = Int.MAX_VALUE
|
||||
|
||||
/** Cache for storing the costs */
|
||||
private val costCache = mutableMapOf<Pair<Tile,Tile>, Float>()
|
||||
|
||||
/**
|
||||
* Retrieves the cost of moving to a given tile, utilizing a cache to improve efficiency.
|
||||
* If the cost for a tile is not already cached, it computes the cost using the provided cost function and stores it in the cache.
|
||||
*
|
||||
* @param from The source tile.
|
||||
* @param to The destination tile.
|
||||
* @return The cost of moving between the tiles.
|
||||
*/
|
||||
private fun getCost(from: Tile, to: Tile): Float {
|
||||
return costCache.getOrPut(Pair(from, to)) { cost(from, to) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for the priority queue used in the A* algorithm.
|
||||
* It compares two `TilePriority` objects based on their priority value,
|
||||
* ensuring that tiles with lower estimated total costs are given precedence in the queue.
|
||||
*/
|
||||
private val tilePriorityComparator = Comparator<TilePriority> { tp1, tp2 ->
|
||||
tp1.priority.compareTo(tp2.priority)
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontier priority queue for managing the tiles to be checked.
|
||||
* Tiles are ordered based on their priority, determined by the cumulative cost so far and the heuristic estimate to the goal.
|
||||
*/
|
||||
private val tilesToCheck = PriorityQueue(27, tilePriorityComparator)
|
||||
|
||||
/**
|
||||
* A map where each tile reached during the search points to its parent tile.
|
||||
* This map is used to reconstruct the path once the destination is reached.
|
||||
*/
|
||||
private val tilesReached = HashMap<Tile, Tile>()
|
||||
|
||||
/**
|
||||
* A map holding the cumulative cost to reach each tile.
|
||||
* This is used to calculate the most efficient path to a tile during the search process.
|
||||
*/
|
||||
private val cumulativeTileCost = HashMap<Tile, Float>()
|
||||
|
||||
init {
|
||||
tilesToCheck.add(TilePriority(startingPoint, 0f))
|
||||
tilesReached[startingPoint] = startingPoint
|
||||
cumulativeTileCost[startingPoint] = 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues the search process until there are no more tiles left to check.
|
||||
*/
|
||||
fun stepToEnd() {
|
||||
while (!hasEnded())
|
||||
nextStep()
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues the search process until either the specified destination is reached or there are no more tiles left to check.
|
||||
*
|
||||
* @param destination The destination tile to reach.
|
||||
* @return This AStar instance, allowing for method chaining.
|
||||
*/
|
||||
fun stepUntilDestination(destination: Tile): AStar {
|
||||
while (!tilesReached.containsKey(destination) && !hasEnded())
|
||||
nextStep()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes one step in the A* algorithm, expanding the search from the current tile to its neighbors.
|
||||
* It updates the search structures accordingly, considering both the cost so far and the heuristic estimate.
|
||||
*
|
||||
* If the maximum size is reached or no more tiles are available, this method will do nothing.
|
||||
*/
|
||||
fun nextStep() {
|
||||
if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return }
|
||||
val currentTile = tilesToCheck.poll()?.tile ?: return
|
||||
for (neighbor in currentTile.neighbors) {
|
||||
val newCost: Float = cumulativeTileCost[currentTile]!! + getCost(currentTile, neighbor)
|
||||
if (predicate(neighbor) &&
|
||||
(!cumulativeTileCost.containsKey(neighbor)
|
||||
|| newCost < (cumulativeTileCost[neighbor] ?: Float.MAX_VALUE))
|
||||
){
|
||||
cumulativeTileCost[neighbor] = newCost
|
||||
val priority: Float = newCost + heuristic(currentTile, neighbor)
|
||||
tilesToCheck.add(TilePriority(neighbor, priority))
|
||||
tilesReached[neighbor] = currentTile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a sequence representing the path from the given destination tile back to the starting point.
|
||||
* If the destination has not been reached, the sequence will be empty.
|
||||
*
|
||||
* @param destination The destination tile to trace the path to.
|
||||
* @return A sequence of tiles representing the path from the destination to the starting point.
|
||||
*/
|
||||
fun getPathTo(destination: Tile): Sequence<Tile> = sequence {
|
||||
var currentNode = destination
|
||||
while (true) {
|
||||
val parent = tilesReached[currentNode] ?: break // destination is not in our path
|
||||
yield(currentNode)
|
||||
if (currentNode == startingPoint) break
|
||||
currentNode = parent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are no more tiles to be checked in the search.
|
||||
*
|
||||
* @return True if the search has ended, otherwise false.
|
||||
*/
|
||||
fun hasEnded() = tilesToCheck.isEmpty()
|
||||
|
||||
/**
|
||||
* Determines if a specific tile has been reached during the search.
|
||||
*
|
||||
* @param tile The tile to check.
|
||||
* @return True if the tile has been reached, otherwise false.
|
||||
*/
|
||||
fun hasReachedTile(tile: Tile) = tilesReached.containsKey(tile)
|
||||
|
||||
/**
|
||||
* Retrieves all tiles that have been reached so far in the search.
|
||||
*
|
||||
* @return A set of tiles that have been reached.
|
||||
*/
|
||||
fun getReachedTiles(): MutableSet<Tile> = tilesReached.keys
|
||||
|
||||
/**
|
||||
* Provides the number of tiles that have been reached so far in the search.
|
||||
*
|
||||
* @return The count of tiles reached.
|
||||
*/
|
||||
fun size() = tilesReached.size
|
||||
}
|
107
core/src/com/unciv/logic/map/MapPathing.kt
Normal file
107
core/src/com/unciv/logic/map/MapPathing.kt
Normal file
@ -0,0 +1,107 @@
|
||||
package com.unciv.logic.map
|
||||
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.RoadStatus
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.utils.Log
|
||||
|
||||
//TODO: Eventually, all path generation in the game should be moved into here.
|
||||
object MapPathing {
|
||||
|
||||
/**
|
||||
* We prefer the worker to prioritize paths connected by existing roads. If a tile has a road, but the civ has the ability
|
||||
* to upgrade it to a railroad, we consider it to be a railroad for pathing since it will be upgraded.
|
||||
* Otherwise, we set every tile to have equal value since building a road on any of them makes the original movement cost irrelevant.
|
||||
*/
|
||||
private fun roadPreferredMovementCost(unit: MapUnit, from: Tile, to: Tile): Float{
|
||||
// hasRoadConnection accounts for civs that treat jungle/forest as roads
|
||||
// Ignore road over river penalties.
|
||||
val areConnectedByRoad = from.hasRoadConnection(unit.civ, mustBeUnpillaged = false) && to.hasRoadConnection(unit.civ, mustBeUnpillaged = false)
|
||||
if (areConnectedByRoad){
|
||||
// If the civ has railroad technology, consider roads as railroads since they will be upgraded
|
||||
if (unit.civ.tech.getBestRoadAvailable() == RoadStatus.Railroad){
|
||||
return RoadStatus.Railroad.movement
|
||||
}else{
|
||||
return unit.civ.tech.movementSpeedOnRoads
|
||||
}
|
||||
}
|
||||
|
||||
val areConnectedByRailroad = from.hasRailroadConnection(mustBeUnpillaged = false) && to.hasRailroadConnection(mustBeUnpillaged = false)
|
||||
if (areConnectedByRailroad)
|
||||
return RoadStatus.Railroad.movement
|
||||
|
||||
return 1f
|
||||
}
|
||||
|
||||
fun isValidRoadPathTile(unit: MapUnit, tile: Tile): Boolean {
|
||||
return tile.isLand
|
||||
&& !tile.isImpassible()
|
||||
&& unit.civ.hasExplored(tile)
|
||||
&& tile.canCivPassThrough(unit.civ)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the path for a road construction between two tiles.
|
||||
*
|
||||
* This function uses the A* search algorithm to find an optimal path for road construction between two specified tiles.
|
||||
*
|
||||
* @param unit The unit that will construct the road.
|
||||
* @param startTile The starting tile of the path.
|
||||
* @param endTile The destination tile of the path.
|
||||
* @return A sequence of tiles representing the path from startTile to endTile, or null if no valid path is found.
|
||||
*/
|
||||
fun getRoadPath(unit: MapUnit, startTile: Tile, endTile: Tile): List<Tile>?{
|
||||
return getPath(unit,
|
||||
startTile,
|
||||
endTile,
|
||||
::isValidRoadPathTile,
|
||||
::roadPreferredMovementCost,
|
||||
{_, _, _ -> 0f}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the path between two tiles.
|
||||
*
|
||||
* This function uses the A* search algorithm to find an optimal path two specified tiles on a game map.
|
||||
*
|
||||
* @param unit The unit for which the path is being calculated.
|
||||
* @param startTile The tile from which the pathfinding begins.
|
||||
* @param endTile The destination tile for the pathfinding.
|
||||
* @param predicate A function that takes a MapUnit and a Tile, returning a Boolean. This function is used to determine whether a tile can be traversed by the unit.
|
||||
* @param cost A function that calculates the cost of moving from one tile to another.
|
||||
* It takes a MapUnit, a 'from' Tile, and a 'to' Tile, returning a Float value representing the cost.
|
||||
* @param heuristic A function that estimates the cost from a given tile to the end tile.
|
||||
* It takes a MapUnit, a 'from' Tile, and a 'to' Tile, returning a Float value representing the heuristic cost estimate.
|
||||
* @return A list of tiles representing the path from the startTile to the endTile. Returns null if no valid path is found.
|
||||
*/
|
||||
private fun getPath(unit: MapUnit,
|
||||
startTile: Tile,
|
||||
endTile: Tile,
|
||||
predicate: (MapUnit, Tile) -> Boolean,
|
||||
cost: (MapUnit, Tile, Tile) -> Float,
|
||||
heuristic: (MapUnit, Tile, Tile) -> Float): List<Tile>? {
|
||||
val astar = AStar(startTile,
|
||||
{ tile -> predicate(unit, tile) },
|
||||
{ from, to -> cost(unit, from, to)},
|
||||
{ from, to -> heuristic(unit, from, to) })
|
||||
while (true) {
|
||||
if (astar.hasEnded()) {
|
||||
// We failed to find a path
|
||||
Log.debug("getRoadPath failed at AStar search size ${astar.size()}")
|
||||
return null
|
||||
}
|
||||
if (!astar.hasReachedTile(endTile)) {
|
||||
astar.nextStep()
|
||||
continue
|
||||
}
|
||||
// Found a path.
|
||||
return astar.getPathTo(endTile)
|
||||
.toList()
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -103,9 +103,14 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
var currentMovement: Float = 0f
|
||||
var health: Int = 100
|
||||
|
||||
var action: String? = null // work, automation, fortifying, I dunno what.
|
||||
// work, automation, fortifying, ...
|
||||
// Connect roads implies automated is true. It is specified by the action type.
|
||||
var action: String? = null
|
||||
var automated: Boolean = false
|
||||
|
||||
var automatedRoadConnectionDestination: Vector2? = null
|
||||
var automatedRoadConnectionPath: List<Vector2>? = null
|
||||
|
||||
@Transient
|
||||
var showAdditionalActions: Boolean = false
|
||||
|
||||
@ -180,6 +185,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
toReturn.health = health
|
||||
toReturn.action = action
|
||||
toReturn.automated = automated
|
||||
toReturn.automatedRoadConnectionDestination = automatedRoadConnectionDestination
|
||||
toReturn.automatedRoadConnectionPath = automatedRoadConnectionPath
|
||||
toReturn.attacksThisTurn = attacksThisTurn
|
||||
toReturn.turnsFortified = turnsFortified
|
||||
toReturn.promotions = promotions.clone()
|
||||
@ -381,6 +388,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
fun isMoving() = action?.startsWith("moveTo") == true
|
||||
|
||||
fun isAutomated() = automated
|
||||
|
||||
fun isAutomatingRoadConnection() = action == UnitActionType.ConnectRoad.value
|
||||
fun isExploring() = action == UnitActionType.Explore.value
|
||||
fun isPreparingParadrop() = action == UnitActionType.Paradrop.value
|
||||
fun isPreparingAirSweep() = action == UnitActionType.AirSweep.value
|
||||
|
@ -246,7 +246,7 @@ class UnitMovement(val unit: MapUnit) {
|
||||
return getShortestPath(destination).any()
|
||||
}
|
||||
|
||||
private fun canReachInCurrentTurn(destination: Tile): Boolean {
|
||||
fun canReachInCurrentTurn(destination: Tile): Boolean {
|
||||
if (unit.cache.cannotMove) return destination == unit.getTile()
|
||||
if (unit.baseUnit.movesLikeAirUnits())
|
||||
return unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits()
|
||||
|
@ -671,7 +671,19 @@ open class Tile : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
fun hasConnection(civInfo: Civilization) =
|
||||
getUnpillagedRoad() != RoadStatus.None || forestOrJungleAreRoads(civInfo)
|
||||
getUnpillagedRoad() != RoadStatus.None || forestOrJungleAreRoads(civInfo)
|
||||
|
||||
fun hasRoadConnection(civInfo: Civilization, mustBeUnpillaged: Boolean) =
|
||||
if (mustBeUnpillaged)
|
||||
(getUnpillagedRoad() == RoadStatus.Road) || forestOrJungleAreRoads(civInfo)
|
||||
else
|
||||
roadStatus == RoadStatus.Road || forestOrJungleAreRoads(civInfo)
|
||||
|
||||
fun hasRailroadConnection(mustBeUnpillaged: Boolean) =
|
||||
if (mustBeUnpillaged)
|
||||
getUnpillagedRoad() == RoadStatus.Railroad
|
||||
else
|
||||
roadStatus == RoadStatus.Railroad
|
||||
|
||||
|
||||
private fun forestOrJungleAreRoads(civInfo: Civilization) =
|
||||
|
@ -99,6 +99,8 @@ enum class UnitActionType(
|
||||
{ ImageGetter.getUnitActionPortrait("Swap") }, false),
|
||||
Automate("Automate",
|
||||
{ ImageGetter.getUnitActionPortrait("Automate") }),
|
||||
ConnectRoad("Connect road",
|
||||
{ ImageGetter.getUnitActionPortrait("RoadConnection") }),
|
||||
StopAutomation("Stop automation",
|
||||
{ ImageGetter.getUnitActionPortrait("Stop") }, false),
|
||||
StopMovement("Stop movement",
|
||||
|
@ -88,6 +88,7 @@ enum class KeyboardBinding(
|
||||
// here as it will not be guaranteed to already be fully initialized.
|
||||
SwapUnits(Category.UnitActions,"Swap units", 'y'),
|
||||
Automate(Category.UnitActions, 'm'),
|
||||
ConnectRoad(Category.UnitActions, "Connect road", 'c'),
|
||||
StopAutomation(Category.UnitActions,"Stop automation", 'm'),
|
||||
StopMovement(Category.UnitActions,"Stop movement", '.'),
|
||||
ShowUnitDestination(Category.UnitActions, "Show unit destination", 'j'),
|
||||
|
@ -15,17 +15,20 @@ import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.automation.unit.CityLocationTileRanker
|
||||
import com.unciv.logic.automation.unit.UnitAutomation
|
||||
import com.unciv.logic.battle.AttackableTile
|
||||
import com.unciv.logic.battle.Battle
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.battle.TargetHelper
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.map.MapPathing
|
||||
import com.unciv.logic.map.TileMap
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.mapunit.movement.UnitMovement
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.UnitActionType
|
||||
import com.unciv.models.ruleset.unique.LocalUniqueCache
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
@ -39,6 +42,7 @@ import com.unciv.ui.components.extensions.surroundWithCircle
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.input.ActivationTypes
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.components.input.KeyboardBinding
|
||||
import com.unciv.ui.components.input.keyShortcuts
|
||||
import com.unciv.ui.components.input.onActivation
|
||||
import com.unciv.ui.components.input.onClick
|
||||
@ -70,6 +74,8 @@ class WorldMapHolder(
|
||||
|
||||
private val unitMovementPaths: HashMap<MapUnit, ArrayList<Tile>> = HashMap()
|
||||
|
||||
private val unitConnectRoadPaths: HashMap<MapUnit, List<Tile>> = HashMap()
|
||||
|
||||
private lateinit var tileGroupMap: TileGroupMap<WorldTileGroup>
|
||||
|
||||
lateinit var currentTileSetStrings: TileSetStrings
|
||||
@ -112,6 +118,10 @@ class WorldMapHolder(
|
||||
// Contains the data required to draw a "swap with" button
|
||||
class SwapWithButtonDto(val unit: MapUnit, val tile: Tile) : ButtonDto
|
||||
|
||||
// Contains the data required to draw a "connect road" button
|
||||
class ConnectRoadButtonDto(val unit: MapUnit, val tile: Tile) : ButtonDto
|
||||
|
||||
|
||||
internal fun addTiles() {
|
||||
val tileSetStrings = TileSetStrings()
|
||||
currentTileSetStrings = tileSetStrings
|
||||
@ -153,38 +163,46 @@ class WorldMapHolder(
|
||||
removeUnitActionOverlay()
|
||||
selectedTile = tile
|
||||
unitMovementPaths.clear()
|
||||
unitConnectRoadPaths.clear()
|
||||
|
||||
val unitTable = worldScreen.bottomUnitTable
|
||||
val previousSelectedUnits = unitTable.selectedUnits.toList() // create copy
|
||||
val previousSelectedCity = unitTable.selectedCity
|
||||
val previousSelectedUnitIsSwapping = unitTable.selectedUnitIsSwapping
|
||||
val previousSelectedUnitIsConnectingRoad = unitTable.selectedUnitIsConnectingRoad
|
||||
unitTable.tileSelected(tile)
|
||||
val newSelectedUnit = unitTable.selectedUnit
|
||||
|
||||
if (previousSelectedCity != null && tile != previousSelectedCity.getCenterTile())
|
||||
tileGroups[previousSelectedCity.getCenterTile()]!!.layerCityButton.moveUp()
|
||||
|
||||
if (previousSelectedUnits.isNotEmpty() && previousSelectedUnits.any { it.getTile() != tile }
|
||||
&& worldScreen.isPlayersTurn
|
||||
&& (
|
||||
if (previousSelectedUnitIsSwapping)
|
||||
previousSelectedUnits.first().movement.canUnitSwapTo(tile)
|
||||
else
|
||||
previousSelectedUnits.any {
|
||||
it.movement.canMoveTo(tile) ||
|
||||
it.movement.isUnknownTileWeShouldAssumeToBePassable(tile) && !it.baseUnit.movesLikeAirUnits()
|
||||
}
|
||||
) && previousSelectedUnits.any { !it.isPreparingAirSweep()}) {
|
||||
if (previousSelectedUnitIsSwapping) {
|
||||
addTileOverlaysWithUnitSwapping(previousSelectedUnits.first(), tile)
|
||||
}
|
||||
else {
|
||||
// this can take a long time, because of the unit-to-tile calculation needed, so we put it in a different thread
|
||||
addTileOverlaysWithUnitMovement(previousSelectedUnits, tile)
|
||||
if (previousSelectedUnits.isNotEmpty()) {
|
||||
val isTileDifferent = previousSelectedUnits.any { it.getTile() != tile }
|
||||
val isPlayerTurn = worldScreen.isPlayersTurn
|
||||
val existsUnitNotPreparingAirSweep = previousSelectedUnits.any { !it.isPreparingAirSweep() }
|
||||
|
||||
// Todo: valid tiles for actions should be handled internally, not here.
|
||||
val canPerformActionsOnTile = if (previousSelectedUnitIsSwapping) {
|
||||
previousSelectedUnits.first().movement.canUnitSwapTo(tile)
|
||||
} else if(previousSelectedUnitIsConnectingRoad) {
|
||||
true
|
||||
} else {
|
||||
previousSelectedUnits.any {
|
||||
it.movement.canMoveTo(tile) ||
|
||||
(it.movement.isUnknownTileWeShouldAssumeToBePassable(tile) && !it.baseUnit.movesLikeAirUnits())
|
||||
}
|
||||
}
|
||||
} else addTileOverlays(tile) // no unit movement but display the units in the tile etc.
|
||||
|
||||
if (isTileDifferent && isPlayerTurn && canPerformActionsOnTile && existsUnitNotPreparingAirSweep) {
|
||||
when {
|
||||
previousSelectedUnitIsSwapping -> addTileOverlaysWithUnitSwapping(previousSelectedUnits.first(), tile)
|
||||
previousSelectedUnitIsConnectingRoad -> addTileOverlaysWithUnitRoadConnecting(previousSelectedUnits.first(), tile)
|
||||
else -> addTileOverlaysWithUnitMovement(previousSelectedUnits, tile) // Long-running task
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addTileOverlays(tile) // no unit movement but display the units in the tile etc.
|
||||
}
|
||||
|
||||
if (newSelectedUnit == null || newSelectedUnit.isCivilian()) {
|
||||
val unitsInTile = selectedTile!!.getUnits()
|
||||
@ -204,6 +222,7 @@ class WorldMapHolder(
|
||||
removeUnitActionOverlay()
|
||||
selectedTile = tile
|
||||
unitMovementPaths.clear()
|
||||
unitConnectRoadPaths.clear()
|
||||
if (!worldScreen.canChangeState) return
|
||||
|
||||
// Concurrency might open up a race condition window - if worldScreen.shouldUpdate is on too
|
||||
@ -327,6 +346,24 @@ class WorldMapHolder(
|
||||
removeUnitActionOverlay()
|
||||
}
|
||||
|
||||
private fun connectRoadToTargetTile(selectedUnit: MapUnit, targetTile: Tile) {
|
||||
selectedUnit.automatedRoadConnectionDestination = targetTile.position
|
||||
selectedUnit.automatedRoadConnectionPath = null
|
||||
selectedUnit.action = UnitActionType.ConnectRoad.value
|
||||
selectedUnit.automated = true
|
||||
UnitAutomation.automateUnitMoves(selectedUnit)
|
||||
|
||||
SoundPlayer.play(UncivSound("wagon"))
|
||||
|
||||
worldScreen.shouldUpdate = true
|
||||
removeUnitActionOverlay()
|
||||
|
||||
// Make highlighting go away
|
||||
worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad = false
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun addTileOverlaysWithUnitMovement(selectedUnits: List<MapUnit>, tile: Tile) {
|
||||
Concurrency.run("TurnsToGetThere") {
|
||||
/** LibGdx sometimes has these weird errors when you try to edit the UI layout from 2 separate threads.
|
||||
@ -397,6 +434,27 @@ class WorldMapHolder(
|
||||
worldScreen.shouldUpdate = true
|
||||
}
|
||||
|
||||
private fun addTileOverlaysWithUnitRoadConnecting(selectedUnit: MapUnit, tile: Tile){
|
||||
Concurrency.run("ConnectRoad") {
|
||||
val validTile = tile.isLand &&
|
||||
!tile.isImpassible() &&
|
||||
selectedUnit.civ.hasExplored(tile)
|
||||
if (validTile) {
|
||||
val roadPath: List<Tile>? = MapPathing.getRoadPath(selectedUnit, selectedUnit.currentTile, tile)
|
||||
launchOnGLThread {
|
||||
if (roadPath == null) { // give the regular tile overlays with no road connection
|
||||
addTileOverlays(tile)
|
||||
worldScreen.shouldUpdate = true
|
||||
return@launchOnGLThread
|
||||
}
|
||||
unitConnectRoadPaths[selectedUnit] = roadPath
|
||||
val connectRoadButtonDto = ConnectRoadButtonDto(selectedUnit, tile)
|
||||
addTileOverlays(tile, connectRoadButtonDto)
|
||||
worldScreen.shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun addTileOverlays(tile: Tile, buttonDto: ButtonDto? = null) {
|
||||
val table = Table().apply { defaults().pad(10f) }
|
||||
if (buttonDto != null && worldScreen.canChangeState)
|
||||
@ -404,6 +462,7 @@ class WorldMapHolder(
|
||||
when (buttonDto) {
|
||||
is MoveHereButtonDto -> getMoveHereButton(buttonDto)
|
||||
is SwapWithButtonDto -> getSwapWithButton(buttonDto)
|
||||
is ConnectRoadButtonDto -> getConnectRoadButton(buttonDto)
|
||||
else -> null
|
||||
}
|
||||
)
|
||||
@ -495,6 +554,26 @@ class WorldMapHolder(
|
||||
return swapWithButton
|
||||
}
|
||||
|
||||
private fun getConnectRoadButton(dto: ConnectRoadButtonDto): Group {
|
||||
val connectRoadButton = Group().apply { width = buttonSize;height = buttonSize; }
|
||||
connectRoadButton.addActor(ImageGetter.getUnitActionPortrait("RoadConnection", buttonSize * 0.8f).apply {
|
||||
center(connectRoadButton)
|
||||
}
|
||||
)
|
||||
|
||||
val unitIcon = UnitGroup(dto.unit, smallerCircleSizes)
|
||||
unitIcon.y = buttonSize - unitIcon.height
|
||||
connectRoadButton.addActor(unitIcon)
|
||||
|
||||
connectRoadButton.onActivation(UncivSound.Silent) {
|
||||
connectRoadToTargetTile(dto.unit, dto.tile)
|
||||
}
|
||||
|
||||
connectRoadButton.keyShortcuts.add(KeyboardBinding.ConnectRoad)
|
||||
|
||||
return connectRoadButton
|
||||
}
|
||||
|
||||
|
||||
fun addOverlayOnTileGroup(group: TileGroup, actor: Actor) {
|
||||
|
||||
@ -575,6 +654,10 @@ class WorldMapHolder(
|
||||
unitTable.selectedCity != null -> {
|
||||
val city = unitTable.selectedCity!!
|
||||
updateBombardableTilesForSelectedCity(city)
|
||||
// We still want to show road paths to the selected city if they are present
|
||||
if (unitTable.selectedUnitIsConnectingRoad){
|
||||
updateTilesForSelectedUnit(unitTable.selectedUnits[0])
|
||||
}
|
||||
}
|
||||
unitTable.selectedUnit != null -> {
|
||||
for (unit in unitTable.selectedUnits) {
|
||||
@ -617,6 +700,7 @@ class WorldMapHolder(
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 0
|
||||
// Highlight suitable tiles in swapping-mode
|
||||
if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) {
|
||||
val unitSwappableTiles = unit.movement.getUnitSwappableTiles()
|
||||
@ -625,7 +709,29 @@ class WorldMapHolder(
|
||||
tileGroups[tile]!!.layerOverlay.showHighlight(swapUnitsTileOverlayColor,
|
||||
if (UncivGame.Current.settings.singleTapMove) 0.7f else 0.3f)
|
||||
}
|
||||
// In swapping-mode don't want to show other overlays
|
||||
// In swapping-mode we don't want to show other overlays
|
||||
return
|
||||
}
|
||||
|
||||
// Z-Layer: 0
|
||||
// Highlight suitable tiles in road connecting mode
|
||||
if (worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad){
|
||||
val validTiles = unit.civ.gameInfo.tileMap.tileList.filter {
|
||||
MapPathing.isValidRoadPathTile(unit, it)
|
||||
}
|
||||
unit.civ.gameInfo.civilizations
|
||||
val connectRoadTileOverlayColor = Color.RED
|
||||
for (tile in validTiles) {
|
||||
tileGroups[tile]!!.layerOverlay.showHighlight(connectRoadTileOverlayColor, 0.3f)
|
||||
}
|
||||
|
||||
if (unitConnectRoadPaths.containsKey(unit)) {
|
||||
for (tile in unitConnectRoadPaths[unit]!!) {
|
||||
tileGroups[tile]!!.layerOverlay.showHighlight(Color.ORANGE, 0.8f)
|
||||
}
|
||||
}
|
||||
|
||||
// In road connecting mode we don't want to show other overlays
|
||||
return
|
||||
}
|
||||
|
||||
@ -636,6 +742,7 @@ class WorldMapHolder(
|
||||
val nukeBlastRadius = if (unit.baseUnit.isNuclearWeapon() && selectedTile != null && selectedTile != unit.getTile())
|
||||
unit.getNukeBlastRadius() else -1
|
||||
|
||||
// Z-Layer: 1
|
||||
// Highlight tiles within movement range
|
||||
for (tile in tilesInMoveRange) {
|
||||
val group = tileGroups[tile]!!
|
||||
@ -663,6 +770,7 @@ class WorldMapHolder(
|
||||
|
||||
}
|
||||
|
||||
// Z-Layer: 2
|
||||
// Add back in the red markers for Air Unit Attack range since they can't move, but can still attack
|
||||
if (unit.cache.cannotMove && isAirUnit && !unit.isPreparingAirSweep()) {
|
||||
val tilesInAttackRange = unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getRange()))
|
||||
@ -672,6 +780,7 @@ class WorldMapHolder(
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 3
|
||||
// Movement paths
|
||||
if (unitMovementPaths.containsKey(unit)) {
|
||||
for (tile in unitMovementPaths[unit]!!) {
|
||||
@ -679,11 +788,29 @@ class WorldMapHolder(
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 4
|
||||
// Highlight road path for workers currently connecting roads
|
||||
if (unit.isAutomatingRoadConnection()) {
|
||||
val currTileIndex = unit.automatedRoadConnectionPath!!.indexOf(unit.currentTile.position)
|
||||
if (currTileIndex != -1) {
|
||||
val futureTiles = unit.automatedRoadConnectionPath!!.filterIndexed { index, _ ->
|
||||
index > currTileIndex
|
||||
}.map{tilePos ->
|
||||
tileMap[tilePos]
|
||||
}
|
||||
for (tile in futureTiles){
|
||||
tileGroups[tile]!!.layerOverlay.showHighlight(Color.ORANGE, if (UncivGame.Current.settings.singleTapMove) 0.7f else 0.3f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 5
|
||||
// Highlight movement destination tile
|
||||
if (unit.isMoving()) {
|
||||
tileGroups[unit.getMovementDestination()]!!.layerOverlay.showHighlight(Color.WHITE, 0.7f)
|
||||
}
|
||||
|
||||
// Z-Layer: 6
|
||||
// Highlight attackable tiles
|
||||
if (unit.isMilitary()) {
|
||||
|
||||
@ -711,6 +838,7 @@ class WorldMapHolder(
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 7
|
||||
// Highlight best tiles for city founding
|
||||
if (unit.hasUnique(UniqueType.FoundCity)
|
||||
&& UncivGame.Current.settings.showSettlersSuggestedCityLocations) {
|
||||
|
@ -46,6 +46,9 @@ class UnitTable(val worldScreen: WorldScreen) : Table() {
|
||||
// Whether the (first) selected unit is in unit-swapping mode
|
||||
var selectedUnitIsSwapping = false
|
||||
|
||||
// Whether the (first) selected unit is in road-connecting mode
|
||||
var selectedUnitIsConnectingRoad = false
|
||||
|
||||
/** Sending no unit clears the selected units entirely */
|
||||
fun selectUnit(unit: MapUnit?=null, append:Boolean=false) {
|
||||
if (!append) selectedUnits.clear()
|
||||
@ -55,6 +58,7 @@ class UnitTable(val worldScreen: WorldScreen) : Table() {
|
||||
unit.actionsOnDeselect()
|
||||
}
|
||||
selectedUnitIsSwapping = false
|
||||
selectedUnitIsConnectingRoad = false
|
||||
}
|
||||
|
||||
var selectedCity : City? = null
|
||||
@ -292,7 +296,13 @@ class UnitTable(val worldScreen: WorldScreen) : Table() {
|
||||
}
|
||||
|
||||
fun citySelected(city: City) : Boolean {
|
||||
selectUnit()
|
||||
// If the last selected unit connecting a road, keep it selected. Otherwise, clear.
|
||||
if(selectedUnitIsConnectingRoad){
|
||||
selectUnit(selectedUnits[0])
|
||||
selectedUnitIsConnectingRoad = true // selectUnit resets this
|
||||
}else{
|
||||
selectUnit()
|
||||
}
|
||||
if (city == selectedCity) return false
|
||||
selectedCity = city
|
||||
selectedUnitHasChanged = true
|
||||
|
@ -39,6 +39,7 @@ object UnitActions {
|
||||
UnitActionType.SetUp to UnitActionsFromUniques::getSetupActions,
|
||||
UnitActionType.FoundCity to UnitActionsFromUniques::getFoundCityActions,
|
||||
UnitActionType.ConstructImprovement to UnitActionsFromUniques::getBuildingImprovementsActions,
|
||||
UnitActionType.ConnectRoad to UnitActionsFromUniques::getConnectRoadActions,
|
||||
UnitActionType.Repair to UnitActionsFromUniques::getRepairActions,
|
||||
UnitActionType.HurryResearch to UnitActionsGreatPerson::getHurryResearchActions,
|
||||
UnitActionType.HurryWonder to UnitActionsGreatPerson::getHurryWonderActions,
|
||||
@ -302,7 +303,6 @@ object UnitActions {
|
||||
return
|
||||
|
||||
if (unit.isAutomated()) return
|
||||
|
||||
actionList += UnitAction(UnitActionType.Automate,
|
||||
isCurrentAction = unit.isAutomated(),
|
||||
action = {
|
||||
|
@ -7,6 +7,7 @@ import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.RoadStatus
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.Counter
|
||||
import com.unciv.models.UncivSound
|
||||
@ -274,6 +275,20 @@ object UnitActionsFromUniques {
|
||||
return finalActions
|
||||
}
|
||||
|
||||
fun getConnectRoadActions(unit: MapUnit, tile: Tile) = sequence {
|
||||
if (!unit.hasUnique(UniqueType.BuildImprovements)) return@sequence
|
||||
if (unit.civ.tech.getBestRoadAvailable() == RoadStatus.None) return@sequence
|
||||
val worldScreen = GUI.getWorldScreen()
|
||||
yield(UnitAction(UnitActionType.ConnectRoad,
|
||||
isCurrentAction = unit.isAutomatingRoadConnection(),
|
||||
action = {
|
||||
worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad =
|
||||
!worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad
|
||||
worldScreen.shouldUpdate = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}.asIterable()
|
||||
|
||||
fun getTransformActions(
|
||||
unit: MapUnit, tile: Tile
|
||||
|
@ -665,6 +665,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
|
||||
- [Circle](https://thenounproject.com/term/circle/1841891/) By Aybige
|
||||
- [Arrow](https://thenounproject.com/term/arrow/18123/) By Joe Mortell for movement
|
||||
- [Swap](https://thenounproject.com/search/?q=swap&i=1259600) By iconomania for swapping units
|
||||
- [Road](https://thenounproject.com/icon/road-224428/) By Gábor István Karaba for connect road automation
|
||||
- [Connection](https://thenounproject.com/search/?q=connection&i=1521886) By Travis Avery
|
||||
- [Skull](https://thenounproject.com/search/?q=Skull&i=1030702) By Vladimir Belochkin for disbanding units
|
||||
- [Crosshair](https://thenounproject.com/search/?q=crosshairs&i=916030) By Bakunetsu Kaito for selecting enemies to attack
|
||||
@ -770,6 +771,7 @@ Sounds are from FreeSound.org unless otherwise noted and are either Creative Com
|
||||
- [uzzi_full_single](https://freesound.org/people/Deganoth/sounds/348685/) By Deganoth as 'shot' for bullet attacks
|
||||
- [Grenade Launcher 2](https://soundbible.com/2140-Grenade-Launcher-2.html) By Daniel Simon as city bombard sound (CC Attribution 3.0 license)
|
||||
- [Woosh](https://soundbible.com/2068-Woosh.html) by Mark DiAngelo as 'slider' sound (CC Attribution 3.0 license)
|
||||
- [Large wooden wagon](https://freesound.org/people/craigsmith/sounds/675230/) by Craig Smith as 'connect road' sound
|
||||
- [Tornado-Siren-II](https://soundbible.com/1937-Tornado-Siren-II.html) by Delilah as part of 'nuke' sound (CC Attribution 3.0 license)
|
||||
- [Explosion-Ultra-Bass](https://soundbible.com/1807-Explosion-Ultra-Bass.html) by Mark DiAngelo as part of 'nuke' sound (CC Attribution 3.0 license)
|
||||
- [Short Choir](https://freesound.org/people/Breviceps/sounds/444491/) by Breviceps as 'choir' for free great person pick
|
||||
|
Reference in New Issue
Block a user