diff --git a/android/Images.ConstructionIcons/UnitActionIcons/Guard.png b/android/Images.ConstructionIcons/UnitActionIcons/Guard.png new file mode 100644 index 0000000000..27f1773a3c Binary files /dev/null and b/android/Images.ConstructionIcons/UnitActionIcons/Guard.png differ diff --git a/android/assets/jsons/Tutorials.json b/android/assets/jsons/Tutorials.json index 1d112187e7..e7b86d4b10 100644 --- a/android/assets/jsons/Tutorials.json +++ b/android/assets/jsons/Tutorials.json @@ -507,6 +507,14 @@ "If the Interceptor is an Air Unit, the two units will damage each other in a straight fight with no Interception bonuses. And only the Attacking Air Sweep Unit gets any Air Sweep strength bonuses." ] }, + { + "name": "Fortify, Guard, Withdraw", + "civilopediaText": [ + {"text": "Most Land units have the ability to Fortify. As they spend time without moving, they will slowly gain a Fortification Bonus to their Combat Strength representing digging in and preparing in case of an attack."}, + {"text":"Some units have the ability to Withdraw before a melee attack is struck. However, perhaps you wish for them to stand their ground to protect a particular tile. If you do, then give them the Guard command and they will stay fixed until they die or you give them fresh orders."}, + {"text":"This is NOT the same as Fortify but they will still gain the Fortification Bonus. They will simply not utilize their Withdraw ability. Fortified units with the Withdraw ability will try to flee when attacked."} + ] + }, { "name": "City Tile Blockade", "steps": [ diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 51b59c278b..e8ab7ee79b 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1178,6 +1178,8 @@ Construct road = Fortify = Fortify until healed = Fortification = +Guard = +Guarding = Sleep = Sleep until healed = Moving = diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index b97783493c..e0fbfb4e6a 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -485,7 +485,7 @@ object Battle { if (!attacker.unit.baseUnit.movesLikeAirUnits && !(attacker.isMelee() && defender.isDefeated())) unit.useMovementPoints(1f) } else unit.currentMovement = 0f - if (unit.isFortified() || unit.isSleeping()) + if (unit.isFortified() || unit.isSleeping() || unit.isGuarding()) attacker.unit.action = null // but not, for instance, if it's Set Up - then it should definitely keep the action! } else if (attacker is CityCombatant) { attacker.city.attackedThisTurn = true @@ -640,7 +640,8 @@ object Battle { if (defender.unit.isEmbarked()) return false if (defender.unit.cache.cannotMove) return false if (defender.unit.isEscorting()) return false // running away and leaving the escorted unit defeats the purpose of escorting - + if (defender.unit.isGuarding()) return false // guarding this post and will fight to the death! + // Promotions have no effect as per what I could find in available documentation val fromTile = defender.getTile() val attackerTile = attacker.getTile() diff --git a/core/src/com/unciv/logic/battle/BattleDamage.kt b/core/src/com/unciv/logic/battle/BattleDamage.kt index 32d8a1e406..f811e3d444 100644 --- a/core/src/com/unciv/logic/battle/BattleDamage.kt +++ b/core/src/com/unciv/logic/battle/BattleDamage.kt @@ -226,7 +226,7 @@ object BattleDamage { modifiers["Tile"] = (tileDefenceBonus * 100).toInt() - if (defender.unit.isFortified()) + if (defender.unit.isFortified() || defender.unit.isGuarding()) modifiers["Fortification"] = BattleConstants.FORTIFICATION_BONUS * defender.unit.getFortificationTurns() } diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index 483ffcaf18..cb99d6c821 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -237,9 +237,10 @@ class MapUnit : IsPartOfGameInfoSerialization { fun isActionUntilHealed() = action?.endsWith("until healed") == true fun isFortified() = action?.startsWith(UnitActionType.Fortify.value) == true + fun isGuarding() = action?.equals(UnitActionType.Guard.value) == true fun isFortifyingUntilHealed() = isFortified() && isActionUntilHealed() fun getFortificationTurns(): Int { - if (!isFortified()) return 0 + if (!(isFortified() || isGuarding())) return 0 return turnsFortified } @@ -273,7 +274,7 @@ class MapUnit : IsPartOfGameInfoSerialization { !tile.isMarkedForCreatesOneImprovement() ) return false if (includeOtherEscortUnit && isEscorting() && !getOtherEscortUnit()!!.isIdle(false)) return false - return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving()) + return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving() || isGuarding()) } fun getUniques(): Sequence = tempUniquesMap.getAllUniques() diff --git a/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt b/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt index 977f5296b7..4427c8d5a0 100644 --- a/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt +++ b/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt @@ -24,10 +24,12 @@ class UnitTurnManager(val unit: MapUnit) { tile.getCity()?.shouldReassignPopulation = true } - if (!unit.hasUnitMovedThisTurn() && unit.isFortified() && unit.turnsFortified < 2) { + if (!unit.hasUnitMovedThisTurn() + && (unit.isFortified() || (unit.isGuarding() && unit.canFortify())) + && unit.turnsFortified < 2) { unit.turnsFortified++ } - if (!unit.isFortified()) + if (!unit.isFortified() && !unit.isGuarding()) unit.turnsFortified = 0 if (!unit.hasUnitMovedThisTurn() || unit.hasUnique(UniqueType.HealsEvenAfterAction)) diff --git a/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt b/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt index e70fd346d4..b5d357fbde 100644 --- a/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt +++ b/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt @@ -105,5 +105,8 @@ class UnitUpgradeManager(val unit: MapUnit) { // wake up if lost ability to fortify if (newUnit.isFortified() && !newUnit.canFortify(ignoreAlreadyFortified = true)) newUnit.action = null + // wake up from Guarding if can't Withdraw + if (newUnit.isGuarding() && !newUnit.hasUnique(UniqueType.WithdrawsBeforeMeleeCombat)) + newUnit.action = null } } diff --git a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt index 86f6b7e88c..c278e5fb70 100644 --- a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt +++ b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt @@ -376,7 +376,7 @@ class UnitMovement(val unit: MapUnit) { unit.removeFromTile() // we "teleport" them away unit.putInTile(allowedTile) // Cancel sleep or fortification if forcibly displaced - for now, leave movement / auto / explore orders - if (unit.isSleeping() || unit.isFortified()) + if (unit.isSleeping() || unit.isFortified() || unit.isGuarding()) unit.action = null unit.mostRecentMoveType = UnitMovementMemoryType.UnitTeleported @@ -435,7 +435,7 @@ class UnitMovement(val unit: MapUnit) { unit.mostRecentMoveType = UnitMovementMemoryType.UnitMoved val pathToLastReachableTile = distanceToTiles.getPathToTile(lastReachableTile) - if (unit.isFortified() || unit.isSetUpForSiege() || unit.isSleeping()) + if (unit.isFortified() || unit.isGuarding() || unit.isSetUpForSiege() || unit.isSleeping()) unit.action = null // un-fortify/un-setup/un-sleep after moving // If this unit is a carrier, keep record of its air payload whereabouts. diff --git a/core/src/com/unciv/models/UnitAction.kt b/core/src/com/unciv/models/UnitAction.kt index 559180c685..027ffcce82 100644 --- a/core/src/com/unciv/models/UnitAction.kt +++ b/core/src/com/unciv/models/UnitAction.kt @@ -126,6 +126,8 @@ enum class UnitActionType( { ImageGetter.getUnitActionPortrait("Fortify") }, UncivSound.Fortify), FortifyUntilHealed("Fortify until healed", { ImageGetter.getUnitActionPortrait("FortifyUntilHealed") }, UncivSound.Fortify), + Guard("Guard", + { ImageGetter.getUnitActionPortrait("Guard") }, UncivSound.Fortify, defaultPage = 0), Explore("Explore", { ImageGetter.getUnitActionPortrait("Explore") }), StopExploration("Stop exploration", diff --git a/core/src/com/unciv/ui/components/widgets/UnitIconGroup.kt b/core/src/com/unciv/ui/components/widgets/UnitIconGroup.kt index 8add5c9cd0..2bffcab1e3 100644 --- a/core/src/com/unciv/ui/components/widgets/UnitIconGroup.kt +++ b/core/src/com/unciv/ui/components/widgets/UnitIconGroup.kt @@ -138,6 +138,7 @@ class UnitIconGroup(val unit: MapUnit, val size: Float) : Group() { return when { unit.isEmbarked() -> ImageGetter.getDrawable("UnitFlagIcons/UnitFlagEmbark") unit.isFortified() -> ImageGetter.getDrawable("UnitFlagIcons/UnitFlagFortify") + unit.isGuarding() -> ImageGetter.getDrawable("UnitFlagIcons/UnitFlagFortify") unit.isCivilian() -> ImageGetter.getDrawable("UnitFlagIcons/UnitFlagCivilian") else -> ImageGetter.getDrawable("UnitFlagIcons/UnitFlag") } @@ -147,6 +148,7 @@ class UnitIconGroup(val unit: MapUnit, val size: Float) : Group() { return when { unit.isEmbarked() -> ImageGetter.getDrawableOrNull("UnitFlagIcons/UnitFlagEmbarkInner") unit.isFortified() -> ImageGetter.getDrawableOrNull("UnitFlagIcons/UnitFlagFortifyInner") + unit.isGuarding() -> ImageGetter.getDrawableOrNull("UnitFlagIcons/UnitFlagFortifyInner") unit.isCivilian() -> ImageGetter.getDrawableOrNull("UnitFlagIcons/UnitFlagCivilianInner") else -> ImageGetter.getDrawableOrNull("UnitFlagIcons/UnitFlagInner") } @@ -157,6 +159,7 @@ class UnitIconGroup(val unit: MapUnit, val size: Float) : Group() { val filename = when { unit.isEmbarked() -> "UnitFlagIcons/UnitFlagMaskEmbark" unit.isFortified() -> "UnitFlagIcons/UnitFlagMaskFortify" + unit.isGuarding() -> "UnitFlagIcons/UnitFlagMaskFortify" unit.isCivilian() -> "UnitFlagIcons/UnitFlagMaskCivilian" else -> "UnitFlagIcons/UnitFlagMask" } @@ -170,6 +173,7 @@ class UnitIconGroup(val unit: MapUnit, val size: Float) : Group() { return when { unit.isEmbarked() -> ImageGetter.getImage("UnitFlagIcons/UnitFlagSelectionEmbark") unit.isFortified() -> ImageGetter.getImage("UnitFlagIcons/UnitFlagSelectionFortify") + unit.isGuarding() -> ImageGetter.getImage("UnitFlagIcons/UnitFlagSelectionFortify") unit.isCivilian() -> ImageGetter.getImage("UnitFlagIcons/UnitFlagSelectionCivilian") else -> ImageGetter.getImage("UnitFlagIcons/UnitFlagSelection") } diff --git a/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTabHelpers.kt b/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTabHelpers.kt index bf6f51f01e..011248e009 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTabHelpers.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTabHelpers.kt @@ -57,6 +57,7 @@ open class UnitOverviewTabHelpers { return when { unit.action == null -> workerText unit.isFortified() -> UnitActionType.Fortify.value + unit.isGuarding() -> UnitActionType.Guard.value unit.isMoving() -> "Moving" unit.isAutomated() && workerText != null -> "[$workerText] ${Fonts.automate}" else -> unit.action diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt index cabe29c52a..afb1b33630 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt @@ -78,6 +78,7 @@ object UnitActions { UnitActionType.Paradrop to UnitActionsFromUniques::getParadropActions, UnitActionType.AirSweep to UnitActionsFromUniques::getAirSweepActions, UnitActionType.SetUp to UnitActionsFromUniques::getSetupActions, + UnitActionType.Guard to UnitActionsFromUniques::getGuardActions, UnitActionType.FoundCity to UnitActionsFromUniques::getFoundCityActions, UnitActionType.ConstructImprovement to UnitActionsFromUniques::getBuildingImprovementsActions, UnitActionType.ConnectRoad to UnitActionsFromUniques::getConnectRoadActions, @@ -295,7 +296,7 @@ object UnitActions { } private suspend fun SequenceScope.addSleepActions(unit: MapUnit, tile: Tile) { - if (unit.isFortified() || unit.canFortify() || !unit.hasMovement()) return + if (unit.isFortified() || unit.canFortify() || unit.isGuarding() || !unit.hasMovement()) return if (tile.hasImprovementInProgress() && unit.canBuildImprovement(tile.getTileImprovementInProgress()!!)) return yield(UnitAction(UnitActionType.Sleep, diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt index dad155c463..6a9c78ebe1 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt @@ -165,6 +165,30 @@ object UnitActionsFromUniques { )) } + // Instead of Withdrawing, stand your ground! + // Different than Fortify + internal fun getGuardActions(unit: MapUnit, tile: Tile): Sequence { + if (!unit.hasUnique(UniqueType.WithdrawsBeforeMeleeCombat)) return emptySequence() + + if (unit.isGuarding()) { + val title = if (unit.canFortify()) "${"Guarding".tr()} ${unit.getFortificationTurns() * 20}%" else "Guarding".tr() + return sequenceOf(UnitAction(UnitActionType.Guard, + useFrequency = 0f, + isCurrentAction = true, + title = title + )) + } + + if (!unit.hasMovement()) return emptySequence() + + return sequenceOf(UnitAction(UnitActionType.Guard, + useFrequency = 0f, + action = { + unit.action = UnitActionType.Guard.value + }.takeIf { !unit.isGuarding() }) + ) + } + internal fun getTriggerUniqueActions(unit: MapUnit, tile: Tile) = sequence { for (unique in unit.getUniques()) { // not a unit action