diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 1fbc3e7e07..e821ae4bf7 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -165,6 +165,7 @@ You destroyed City States that were under our protection! = You attacked City States that were under our protection! = You demanded tribute from City States that were under our protection! = You sided with a City State over us = +You returned captured units to us = Demands = Please don't settle new cities near us. = @@ -176,6 +177,9 @@ We asked [civName] for a tribute recently and they gave in.\nYou promised to pro It's come to my attention that I may have attacked [civName], a city-state under your protection.\nWhile it was not my goal to be at odds with your empire, this was deemed a necessary course of action. = I thought you might like to know that I've launched an invasion of one of your little pet states.\nThe lands of [civName] will make a fine addition to my own. = +Return [unitName] to [civName]? = +The [unitName] we liberated originally belonged to [civName]. They will be grateful if we return it to them. = + Enter the amount of gold = # City-States diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 0bd130ff5d..fe98a49214 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -347,17 +347,20 @@ object Battle { private fun postBattleMoveToAttackedTile(attacker: ICombatant, defender: ICombatant, attackedTile: TileInfo) { if (attacker.isMelee() - && (defender.isDefeated() || defender.getCivInfo() == attacker.getCivInfo()) - // This is so that if we attack e.g. a barbarian in enemy territory that we can't enter, we won't enter it - && (attacker as MapUnitCombatant).unit.movement.canMoveTo(attackedTile)) { + && (defender.isDefeated() || defender.getCivInfo() == attacker.getCivInfo())) { // we destroyed an enemy military unit and there was a civilian unit in the same tile as well + // this has to be checked before canMoveTo, otherwise it will return false if (attackedTile.civilianUnit != null && attackedTile.civilianUnit!!.civInfo != attacker.getCivInfo()) captureCivilianUnit(attacker, MapUnitCombatant(attackedTile.civilianUnit!!)) - // Units that can move after attacking are not affected by zone of control if the - // movement is caused by killing a unit. Effectively, this means that attack movements - // are exempt from zone of control, since units that cannot move after attacking already - // lose all remaining movement points anyway. - attacker.unit.movement.moveToTile(attackedTile, considerZoneOfControl = false) + + // This is so that if we attack e.g. a barbarian in enemy territory that we can't enter, we won't enter it + if ((attacker as MapUnitCombatant).unit.movement.canMoveTo(attackedTile)) { + // Units that can move after attacking are not affected by zone of control if the + // movement is caused by killing a unit. Effectively, this means that attack movements + // are exempt from zone of control, since units that cannot move after attacking already + // lose all remaining movement points anyway. + attacker.unit.movement.moveToTile(attackedTile, considerZoneOfControl = false) + } } } @@ -477,14 +480,40 @@ object Battle { defender.getTile().position, attacker.getName(), NotificationIcon.War, defender.getName()) val capturedUnitTile = capturedUnit.getTile() + val originalOwner = if (capturedUnit.originalOwner != null) + capturedUnit.civInfo.gameInfo.getCivilization(capturedUnit.originalOwner!!) + else null + when { // Uncapturable units are destroyed defender.unit.hasUnique("Uncapturable") -> { capturedUnit.destroy() } + // City states can never capture settlers at all + capturedUnit.hasUnique("Founds a new city") && attacker.getCivInfo().isCityState() -> { + capturedUnit.destroy() + } + // Is it our old unit? + attacker.getCivInfo() == originalOwner -> { + // Then it is recaptured without converting settlers to workers + capturedUnit.capturedBy(attacker.getCivInfo()) + } + // Return captured civilian to its original owner? + defender.getCivInfo().isBarbarian() + && originalOwner != null + && !originalOwner.isBarbarian() + && attacker.getCivInfo() != originalOwner + && attacker.getCivInfo().knows(originalOwner) + && originalOwner.isAlive() + && !attacker.getCivInfo().isAtWarWith(originalOwner) + && attacker.getCivInfo().playerType == PlayerType.Human // Only humans get the choice + -> { + capturedUnit.capturedBy(attacker.getCivInfo()) + attacker.getCivInfo().popupAlerts.add(PopupAlert(AlertType.RecapturedCivilian, capturedUnitTile.position.toString())) + } // Captured settlers are converted to workers unless captured by barbarians (so they can be returned later). - capturedUnit.name == Constants.settler && !attacker.getCivInfo().isBarbarian() -> { + capturedUnit.hasUnique("Founds a new city") && !attacker.getCivInfo().isBarbarian() -> { capturedUnit.destroy() // This is so that future checks which check if a unit has been captured are caught give the right answer // For example, in postBattleMoveToAttackedTile @@ -492,14 +521,7 @@ object Battle { attacker.getCivInfo().placeUnitNearTile(capturedUnitTile.position, Constants.worker) } else -> { - capturedUnit.civInfo.removeUnit(capturedUnit) - capturedUnit.assignOwner(attacker.getCivInfo()) - capturedUnit.currentMovement = 0f - // It's possible that the unit can no longer stand on the tile it was captured on. - // For example, because it's embarked and the capturing civ cannot embark units yet. - if (!capturedUnit.movement.canPassThrough(capturedUnitTile)) { - capturedUnit.movement.teleportToClosestMoveableTile() - } + capturedUnit.capturedBy(attacker.getCivInfo()) } } diff --git a/core/src/com/unciv/logic/civilization/PopupAlert.kt b/core/src/com/unciv/logic/civilization/PopupAlert.kt index 9dc7e1fb2a..1f5d7abe4f 100644 --- a/core/src/com/unciv/logic/civilization/PopupAlert.kt +++ b/core/src/com/unciv/logic/civilization/PopupAlert.kt @@ -16,6 +16,7 @@ enum class AlertType { DiplomaticMarriage, BulliedProtectedMinor, AttackedProtectedMinor, + RecapturedCivilian, } class PopupAlert { diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index 456212ff87..09d1e98b6c 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -81,6 +81,7 @@ enum class DiplomaticModifiers { AttackedProtectedMinor, BulliedProtectedMinor, SidedWithProtectedMinor, + ReturnedCapturedUnits, } class DiplomacyManager() { @@ -645,6 +646,7 @@ class DiplomacyManager() { } otherCivDiplomacy.setModifier(DiplomaticModifiers.DeclaredWarOnUs, -20f) + otherCivDiplomacy.removeModifier(DiplomaticModifiers.ReturnedCapturedUnits) if (otherCiv.isCityState()) { otherCivDiplomacy.setInfluence(-60f) civInfo.changeMinorCivsAttacked(1) diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 84b0df12f5..2bb783cd26 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -114,6 +114,9 @@ class MapUnit { /** civName owning the unit */ lateinit var owner: String + /** civName of original owner - relevant for returning captured workers from barbarians */ + var originalOwner: String? = null + /** * Name key of the unit, used for serialization */ @@ -169,6 +172,7 @@ class MapUnit { toReturn.name = name toReturn.civInfo = civInfo toReturn.owner = owner + toReturn.originalOwner = originalOwner toReturn.instanceName = instanceName toReturn.currentMovement = currentMovement toReturn.health = health @@ -916,6 +920,17 @@ class MapUnit { civInfo.addUnit(this, updateCivInfo) } + fun capturedBy(captor: CivilizationInfo) { + civInfo.removeUnit(this) + assignOwner(captor) + currentMovement = 0f + // It's possible that the unit can no longer stand on the tile it was captured on. + // For example, because it's embarked and the capturing civ cannot embark units yet. + if (!movement.canPassThrough(getTile())) { + movement.teleportToClosestMoveableTile() + } + } + fun canIntercept(attackedTile: TileInfo): Boolean { if (!canIntercept()) return false if (currentTile.aerialDistanceTo(attackedTile) > baseUnit.interceptRange) return false diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 5d9b5456e6..c8c02e1e37 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -410,6 +410,8 @@ class TileMap { // both the civ name and actual civ need to be in here in order to calculate the canMoveTo...Darn unit.assignOwner(civInfo, false) + // remember our first owner + unit.originalOwner = civInfo.civName var unitToPlaceTile: TileInfo? = null // try to place at the original point (this is the most probable scenario) diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index 895c5fd6fd..047fb22518 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -759,6 +759,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo): CameraStageBaseScreen() AttackedProtectedMinor -> "You attacked City States that were under our protection!" BulliedProtectedMinor -> "You demanded tribute from City States that were under our protection!" SidedWithProtectedMinor -> "You sided with a City State over us" + ReturnedCapturedUnits -> "You returned captured units to us" } text = text.tr() + " " if (modifier.value > 0) text += "+" diff --git a/core/src/com/unciv/ui/worldscreen/AlertPopup.kt b/core/src/com/unciv/ui/worldscreen/AlertPopup.kt index 958cfb7add..d5a01fc478 100644 --- a/core/src/com/unciv/ui/worldscreen/AlertPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/AlertPopup.kt @@ -1,11 +1,13 @@ package com.unciv.ui.worldscreen +import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.civilization.* +import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.tr @@ -321,6 +323,49 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu cityState.removeProtectorCiv(player, forced = true) }).row() } + AlertType.RecapturedCivilian -> { + val position = Vector2().fromString(popupAlert.value) + val tile = worldScreen.gameInfo.tileMap[position] + val capturedUnit = tile.civilianUnit!! // This has got to be it + val originalOwner = worldScreen.gameInfo.getCivilization(capturedUnit.originalOwner!!) + val captor = worldScreen.viewingCiv + + addGoodSizedLabel("Return [${capturedUnit.name}] to [${originalOwner.civName}]?") + addSeparator() + addGoodSizedLabel("The [${capturedUnit.name}] we liberated originally belonged to [${originalOwner.civName}]. They will be grateful if we return it to them.").row() + val responseTable = Table() + responseTable.defaults().pad(0f, 30f) // Small buttons, plenty of pad so we don't fat-finger it + responseTable.add(getCloseButton("Yes", 'y') { + // Return it to original owner + val unitName = capturedUnit.baseUnit.name + capturedUnit.destroy() + val closestCity = originalOwner.cities.minByOrNull { it.getCenterTile().aerialDistanceTo(tile) } + if (closestCity != null) { + // Attempt to place the unit near their nearest city + originalOwner.placeUnitNearTile(closestCity.location, unitName) + } + + if (originalOwner.isCityState()) { + originalOwner.getDiplomacyManager(captor).addInfluence(45f) + } else if (originalOwner.isMajorCiv()) { + // No extra bonus from doing it several times + originalOwner.getDiplomacyManager(captor).setModifier(DiplomaticModifiers.ReturnedCapturedUnits, 20f) + } + }) + responseTable.add(getCloseButton("No", 'n') { + // Take it for ourselves + // Settlers become workers at this point + if (capturedUnit.hasUnique("Founds a new city")) { + capturedUnit.destroy() + // This is so that future checks which check if a unit has been captured are caught give the right answer + // For example, in postBattleMoveToAttackedTile + capturedUnit.civInfo = captor + captor.placeUnitNearTile(tile.position, Constants.worker) + } else + capturedUnit.capturedBy(captor) + }).row() + add(responseTable) + } } }