diff --git a/android/assets/jsons/Translations/Other.json b/android/assets/jsons/Translations/Other.json index f8295ed2..8b4cbfcc 100644 --- a/android/assets/jsons/Translations/Other.json +++ b/android/assets/jsons/Translations/Other.json @@ -227,6 +227,10 @@ Japanese:"自動化を停止" } + "Construct road": { + "German": "Straße bauen" + } + "Fortify":{ Italian:"Fortifica" Russian:"Укрепить" diff --git a/core/src/com/unciv/logic/automation/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/WorkerAutomation.kt index 7167720e..4784e983 100644 --- a/core/src/com/unciv/logic/automation/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/WorkerAutomation.kt @@ -49,23 +49,20 @@ class WorkerAutomation(val unit: MapUnit) { - fun tryConnectingCities():Boolean{ // returns whether we actually did anything - val techEnablingRailroad = GameBasics.TileImprovements["Railroad"]!!.techRequired!! - val canBuildRailroad = unit.civInfo.tech.isResearched(techEnablingRailroad) + fun tryConnectingCities():Boolean { // returns whether we actually did anything - val targetRoadName = if (canBuildRailroad) "Railroad" else "Road" - val targetStatus = if (canBuildRailroad) RoadStatus.Railroad else RoadStatus.Road + val targetRoad = unit.civInfo.tech.getBestRoadAvailable() val citiesThatNeedConnecting = unit.civInfo.cities .filter { it.population.population>3 && !it.isCapital() - && !it.cityStats.isConnectedToCapital(targetStatus) } + && !it.cityStats.isConnectedToCapital(targetRoad) } if(citiesThatNeedConnecting.isEmpty()) return false // do nothing. val citiesThatNeedConnectingBfs = citiesThatNeedConnecting .map { city -> BFS(city.getCenterTile()){it.isLand && unit.canPassThrough(it)} } .toMutableList() - val connectedCities = unit.civInfo.cities.filter { it.isCapital() || it.cityStats.isConnectedToCapital(targetStatus) } + val connectedCities = unit.civInfo.cities.filter { it.isCapital() || it.cityStats.isConnectedToCapital(targetRoad) } .map { it.getCenterTile() } while(citiesThatNeedConnectingBfs.any()){ @@ -78,7 +75,7 @@ class WorkerAutomation(val unit: MapUnit) { for(city in connectedCities) if(bfs.tilesToCheck.contains(city)) { // we have a winner! val pathToCity = bfs.getPathTo(city) - val roadableTiles = pathToCity.filter { it.roadStatus < targetStatus } + val roadableTiles = pathToCity.filter { it.roadStatus < targetRoad } val tileToConstructRoadOn :TileInfo if(unit.currentTile in roadableTiles) tileToConstructRoadOn = unit.currentTile else{ @@ -88,8 +85,8 @@ class WorkerAutomation(val unit: MapUnit) { unit.movementAlgs().headTowards(tileToConstructRoadOn) } if(unit.currentMovement>0 && unit.currentTile==tileToConstructRoadOn - && unit.currentTile.improvementInProgress!=targetRoadName) - tileToConstructRoadOn.startWorkingOnImprovement(GameBasics.TileImprovements[targetRoadName]!!,unit.civInfo) + && unit.currentTile.improvementInProgress!=targetRoad.name) + tileToConstructRoadOn.startWorkingOnImprovement(targetRoad.improvement()!!,unit.civInfo) return true } } diff --git a/core/src/com/unciv/logic/civilization/TechManager.kt b/core/src/com/unciv/logic/civilization/TechManager.kt index f3562314..1630794b 100644 --- a/core/src/com/unciv/logic/civilization/TechManager.kt +++ b/core/src/com/unciv/logic/civilization/TechManager.kt @@ -2,6 +2,7 @@ package com.unciv.logic.civilization import com.badlogic.gdx.graphics.Color +import com.unciv.logic.map.RoadStatus import com.unciv.models.gamebasics.GameBasics import com.unciv.models.gamebasics.tech.Technology import com.unciv.models.gamebasics.tr @@ -196,4 +197,13 @@ class TechManager { if(researchedTechUniques.contains("Enables embarked units to enter ocean tiles")) embarkedUnitsCanEnterOcean=true if(researchedTechUniques.contains("Improves movement speed on roads")) movementSpeedOnRoadsImproved = true } + + fun getBestRoadAvailable(): RoadStatus { + if (!isResearched(RoadStatus.Road.improvement()!!.techRequired!!)) return RoadStatus.None + + val techEnablingRailroad = RoadStatus.Railroad.improvement()!!.techRequired!! + val canBuildRailroad = isResearched(techEnablingRailroad) + + return if (canBuildRailroad) RoadStatus.Railroad else RoadStatus.Road + } } \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/BFS.kt b/core/src/com/unciv/logic/map/BFS.kt index c3b23097..f0875591 100644 --- a/core/src/com/unciv/logic/map/BFS.kt +++ b/core/src/com/unciv/logic/map/BFS.kt @@ -18,9 +18,10 @@ class BFS(val startingPoint: TileInfo, val predicate : (TileInfo) -> Boolean){ nextStep() } - fun stepUntilDestination(destination: TileInfo){ + fun stepUntilDestination(destination: TileInfo): BFS { while(!tilesReached.containsKey(destination) && tilesToCheck.isNotEmpty()) nextStep() + return this } fun nextStep(){ @@ -39,8 +40,10 @@ class BFS(val startingPoint: TileInfo, val predicate : (TileInfo) -> Boolean){ path.add(destination) var currentNode = destination while(currentNode != startingPoint){ - currentNode = tilesReached[currentNode]!! - path.add(currentNode) + tilesReached[currentNode]?.let { + currentNode = it + path.add(currentNode) + } ?: return ArrayList() // destination is not in our path } return path } diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 8f52ef37..2ad3794c 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -6,6 +6,7 @@ import com.unciv.Constants import com.unciv.logic.automation.UnitAutomation import com.unciv.logic.automation.WorkerAutomation import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.map.action.MapUnitAction import com.unciv.models.gamebasics.GameBasics import com.unciv.models.gamebasics.tech.TechEra import com.unciv.models.gamebasics.tile.TerrainType @@ -300,10 +301,13 @@ class MapUnit { val enemyUnitsInWalkingDistance = getDistanceToTiles().keys .filter { it.militaryUnit!=null && civInfo.isAtWarWith(it.militaryUnit!!.civInfo)} if(enemyUnitsInWalkingDistance.isNotEmpty()) { - if (action != null && action!!.startsWith("moveTo")) action=null + if (mapUnitAction?.shouldStopOnEnemyInSight()==true) + mapUnitAction=null return // Don't you dare move. } + mapUnitAction?.doPreTurnAction() + if (action != null && action!!.startsWith("moveTo")) { val destination = action!!.replace("moveTo ", "").split(",").dropLastWhile { it.isEmpty() }.toTypedArray() val destinationVector = Vector2(Integer.parseInt(destination[0]).toFloat(), Integer.parseInt(destination[1]).toFloat()) @@ -383,14 +387,13 @@ class MapUnit { if(otherTile==getTile()) return // already here! val distanceToTiles = getDistanceToTiles() - class YouCantGetThereFromHereException : Exception() + class YouCantGetThereFromHereException(msg: String) : Exception(msg) if (!distanceToTiles.containsKey(otherTile)) + throw YouCantGetThereFromHereException("$this can't get from ${currentTile.position} to ${otherTile.position}.") - throw YouCantGetThereFromHereException() - - class CantEnterThisTileException : Exception() + class CantEnterThisTileException(msg: String) : Exception(msg) if(!canMoveTo(otherTile)) - throw CantEnterThisTileException() + throw CantEnterThisTileException("$this can't enter $otherTile") if(otherTile.isCityCenter() && otherTile.getOwner()!=civInfo) throw Exception("This is an enemy city, you can't go here!") currentMovement -= distanceToTiles[otherTile]!! diff --git a/core/src/com/unciv/logic/map/MapUnitAction.kt b/core/src/com/unciv/logic/map/MapUnitAction.kt deleted file mode 100644 index 8d7d7dfc..00000000 --- a/core/src/com/unciv/logic/map/MapUnitAction.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.unciv.logic.map - -open class MapUnitAction( - @Transient var unit: MapUnit = MapUnit(), - var name: String = "" -) - - -class BuildLongRoadAction( - mapUnit: MapUnit = MapUnit() -) : MapUnitAction(mapUnit, "Build Long Road") { - - - -} \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/RoadStatus.kt b/core/src/com/unciv/logic/map/RoadStatus.kt index 5d42a471..774eef4a 100644 --- a/core/src/com/unciv/logic/map/RoadStatus.kt +++ b/core/src/com/unciv/logic/map/RoadStatus.kt @@ -1,7 +1,18 @@ package com.unciv.logic.map +import com.unciv.models.gamebasics.GameBasics + +/** + * You can use RoadStatus.name to identify [Road] and [Railroad] + * in string-based identification, as done in [improvement]. + */ enum class RoadStatus { + None, Road, - Railroad + Railroad; + + /** returns null for [None] */ + fun improvement() = GameBasics.TileImprovements[this.name] + } diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index a1cb7463..50ecd868 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -137,7 +137,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { */ fun headTowards(destination: TileInfo): TileInfo { val currentTile = unit.getTile() - if(currentTile==destination) return currentTile + if (currentTile == destination) return currentTile val distanceToTiles = unit.getDistanceToTiles() @@ -152,7 +152,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { return currentTile val reachableDestinationNeighbors = destinationNeighbors - .filter { distanceToTiles.containsKey(it) && unit.canMoveTo(it)} + .filter { distanceToTiles.containsKey(it) && unit.canMoveTo(it) } if (reachableDestinationNeighbors.isEmpty()) // We can't get closer... return currentTile @@ -160,8 +160,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) { } } else { // If the tile is far away, we need to build a path how to get there, and then take the first step val path = getShortestPath(destination) - class UnreachableDestinationException:Exception() - if(path.isEmpty()) throw UnreachableDestinationException() + class UnreachableDestinationException : Exception() + if (path.isEmpty()) throw UnreachableDestinationException() destinationTileThisTurn = path.first() } diff --git a/core/src/com/unciv/logic/map/action/BuildLongRoadAction.kt b/core/src/com/unciv/logic/map/action/BuildLongRoadAction.kt new file mode 100644 index 00000000..f22370f5 --- /dev/null +++ b/core/src/com/unciv/logic/map/action/BuildLongRoadAction.kt @@ -0,0 +1,89 @@ +package com.unciv.logic.map.action + +import com.unciv.logic.map.BFS +import com.unciv.logic.map.MapUnit +import com.unciv.logic.map.TileInfo + +class BuildLongRoadAction( + mapUnit: MapUnit = MapUnit(), + val target: TileInfo = TileInfo() +) : MapUnitAction(mapUnit, "Build Long Road") { + + override fun shouldStopOnEnemyInSight(): Boolean = true + + override fun isAvailable(): Boolean + = unit.hasUnique("Can build improvements on tiles") + && getPath(target).isNotEmpty() + + override fun doPreTurnAction() { + + // we're working! + if (unit.currentTile.improvementInProgress != null) + return + else if (startWorking()) { + return + } + + // we reached our target? And road is finished? + if (unit.currentTile.position == target.position + && isRoadFinished(unit.currentTile)) { + unit.action = null + return + } + + // move one step forward - and start building + if (stepForward(target)) { + startWorking() + } else if (unit.currentMovement > 1f) { + unit.action = null + return + } + + } + + // because the unit is building a road, we need to use a shortest path that is + // independent of movement costs, but should respect impassable terrain like water and enemy territory + private fun stepForward(destination: TileInfo): Boolean { + var success = false + for (step in getPath(destination).drop(1)) { + if (unit.currentMovement > 0f && unit.canMoveTo(step)) { + unit.moveToTile(step) + success = true + // if there is a road already, take multiple steps, otherwise break + if (!isRoadFinished(step)) { + break + } + } else break + } + return success + } + + private fun isRoadFinished(tile: TileInfo): Boolean { + return tile.roadStatus == unit.civInfo.tech.getBestRoadAvailable() + } + + private fun getPath(destination: TileInfo): List { + // BFS is not very efficient + return BFS(unit.currentTile) { isRoadableTile(it) } + .stepUntilDestination(destination) + .getPathTo(destination).reversed() + } + + private fun isRoadableTile(it: TileInfo) = it.isLand && unit.canPassThrough(it) + + private fun startWorking(): Boolean { + val tile = unit.currentTile + if (unit.currentMovement > 0 && isRoadableTile(tile)) { + val roadToBuild = unit.civInfo.tech.getBestRoadAvailable() + roadToBuild.improvement()?.let { improvement -> + if (tile.roadStatus != roadToBuild && tile.improvementInProgress != improvement.name) { + tile.startWorkingOnImprovement(improvement, unit.civInfo) + return true + } + } + } + return false + } + + +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/action/MapUnitAction.kt b/core/src/com/unciv/logic/map/action/MapUnitAction.kt new file mode 100644 index 00000000..0b29d421 --- /dev/null +++ b/core/src/com/unciv/logic/map/action/MapUnitAction.kt @@ -0,0 +1,15 @@ +package com.unciv.logic.map.action + +import com.unciv.logic.map.MapUnit + +open class MapUnitAction( + @Transient var unit: MapUnit = MapUnit(), + var name: String = "" +) { + /** return true if this action is possible in the given conditions */ + open fun isAvailable(): Boolean = true + open fun doPreTurnAction() {} + open fun shouldStopOnEnemyInSight(): Boolean = name.startsWith("moveTo") +} + + diff --git a/core/src/com/unciv/ui/pickerscreens/GreatPersonPickerScreen.kt b/core/src/com/unciv/ui/pickerscreens/GreatPersonPickerScreen.kt index f243914d..783846d8 100644 --- a/core/src/com/unciv/ui/pickerscreens/GreatPersonPickerScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/GreatPersonPickerScreen.kt @@ -30,7 +30,7 @@ class GreatPersonPickerScreen : PickerScreen() { pick("Get " +unit.name) descriptionLabel.setText(unit.uniques.joinToString()) } - topTable.add(button).pad(10f) + topTable.add(button).pad(10f).row() } rightSideButton.onClick("choir") { diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitContextMenu.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitContextMenu.kt index 87d10c06..973145cf 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitContextMenu.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitContextMenu.kt @@ -10,7 +10,8 @@ import com.unciv.ui.utils.ImageGetter import com.unciv.ui.utils.Sounds import com.unciv.ui.utils.onClick import com.unciv.ui.worldscreen.TileMapHolder -import com.unciv.logic.map.BuildLongRoadAction +import com.unciv.logic.map.action.BuildLongRoadAction +import com.unciv.logic.map.action.MapUnitAction import kotlin.concurrent.thread class UnitContextMenu(val tileMapHolder: TileMapHolder, val selectedUnit: MapUnit, val targetTile: TileInfo) : VerticalGroup() { @@ -19,17 +20,31 @@ class UnitContextMenu(val tileMapHolder: TileMapHolder, val selectedUnit: MapUni space(10f) - addButton(ImageGetter.getStatIcon("Movement"), "Move here") { + addButton(ImageGetter.getStatIcon("Movement"), "Move unit") { onMoveButtonClick() } - addButton(ImageGetter.getImprovementIcon("Road"), "Construct Road this way") { - onConstructRoadButtonClick() - } + + addButton( + ImageGetter.getImprovementIcon("Road"), + "Construct road", + BuildLongRoadAction(selectedUnit, targetTile) + ) pack() } + fun addButton(icon: Actor, label: String, action: MapUnitAction) { + if (action.isAvailable()) { + addButton(icon, label) { + selectedUnit.mapUnitAction = action + selectedUnit.mapUnitAction?.doPreTurnAction() + tileMapHolder.removeUnitActionOverlay = true + tileMapHolder.worldScreen.shouldUpdate = true + } + } + } + fun addButton(icon: Actor, label: String, action: () -> Unit) { val skin = CameraStageBaseScreen.skin val button = Button(skin) @@ -67,7 +82,4 @@ class UnitContextMenu(val tileMapHolder: TileMapHolder, val selectedUnit: MapUni } } - private fun onConstructRoadButtonClick() { - selectedUnit.mapUnitAction = BuildLongRoadAction() - } } \ No newline at end of file