From 77a63ce365e763afd3c9b4a7cd413a7061d87ce3 Mon Sep 17 00:00:00 2001 From: Jack Rainy Date: Sat, 18 Apr 2020 21:25:48 +0300 Subject: [PATCH] Citadel improvements + improved AI for the forts (#2453) * Allow building the citadels either next to or within friendly tiles only * Citadel acquire the tiles around it * AI uses the citadels to get the tiles back + improved AI for the forts --- .../jsons/translations/template.properties | 4 ++ .../automation/SpecificUnitAutomation.kt | 41 ++++++++++++++++--- .../logic/automation/WorkerAutomation.kt | 31 +++++++------- .../diplomacy/DiplomacyManager.kt | 1 + .../src/com/unciv/ui/trade/DiplomacyScreen.kt | 1 + .../unciv/ui/worldscreen/unit/UnitActions.kt | 31 +++++++++++++- 6 files changed, 89 insertions(+), 20 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 4916ef25fe..43800b4eec 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -109,6 +109,7 @@ You fulfilled your promise to stop settling cities near us! = You refused to stop settling cities near us = Your arrogant demands are in bad taste = Your use of nuclear weapons is disgusting! = +You have stolen our lands! = Demands = Please don't settle new cities near us. = @@ -240,6 +241,8 @@ Mongol Terror = Units ignore terrain costs when moving into any tile with Hills. No maintenance costs for improvements in Hills; half cost elsewhere. = Great Andean Road = + ++1 Movement to all embarked units, units pay only 1 movement point to embark and disembark. Melee units pay no movement cost to pillage. = Viking Fury = # New game screen @@ -484,6 +487,7 @@ Our proposed trade request is no longer relevant! = [defender] could not withdraw from a [attacker] - blocked. = [defender] withdrew from a [attacker] = [building] has provided [amount] Gold! = +[civName] has stolen your territory! = # World Screen UI diff --git a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt index 1b59fc99d7..5f81aea1ef 100644 --- a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt @@ -1,9 +1,10 @@ -package com.unciv.logic.automation +package com.unciv.logic.automation import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.GreatPersonManager import com.unciv.logic.civilization.diplomacy.DiplomacyFlags +import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo import com.unciv.models.ruleset.tile.ResourceType @@ -59,9 +60,37 @@ object SpecificUnitAutomation { return } + // try to revenge and capture their tiles + val enemyCities = unit.civInfo.gameInfo.civilizations + .filter { unit.civInfo.knows(it) && + unit.civInfo.getDiplomacyManager(it).hasModifier(DiplomaticModifiers.StealingTerritory) } + .flatMap { it.cities }.asSequence() + // find the suitable tiles (or their neigbours) + val tileToSteal = enemyCities.flatMap { it.getTiles() }.flatMap { it.neighbors.asSequence() } + .filter { it in unit.civInfo.viewableTiles // we can see them + && unit.movement.canReach(it) // we can reach it + && it.neighbors.any { tile -> tile.getOwner() == unit.civInfo} } // they are close to our borders + .sortedBy { + // get closest tiles + val distance = it.aerialDistanceTo(unit.currentTile) + // ...also get priorities to steal the most valuable for them + val owner = it.getOwner() + if (owner != null) + distance - WorkerAutomation(unit).getPriority(it, owner) + else distance }.firstOrNull() + // if there is a good tile to steal - go there + if (tileToSteal != null) { + unit.movement.headTowards(tileToSteal) + if (unit.currentMovement > 0 && unit.currentTile == tileToSteal) + UnitActions.getBuildImprovementAction(unit)?.action?.invoke() + return + } + // try to build a citadel - if (WorkerAutomation(unit).evaluateFortPlacement(unit.currentTile, unit.civInfo)) + if (WorkerAutomation(unit).evaluateFortPlacement(unit.currentTile, unit.civInfo, true)) { UnitActions.getBuildImprovementAction(unit)?.action?.invoke() + return + } //if no unit to follow, take refuge in city or build citadel there. val reachableTest : (TileInfo) -> Boolean = {it.civilianUnit == null && @@ -75,10 +104,12 @@ object SpecificUnitAutomation { // try to find a good place for citadel nearby val potentialTilesNearCity = cityToGarrison.getTilesInDistanceRange(3..4) val tileForCitadel = potentialTilesNearCity.firstOrNull { reachableTest(it) && - WorkerAutomation(unit).evaluateFortPlacement(it, unit.civInfo) } - if (tileForCitadel != null) + WorkerAutomation(unit).evaluateFortPlacement(it, unit.civInfo, true) } + if (tileForCitadel != null) { unit.movement.headTowards(tileForCitadel) - else + if (unit.currentMovement > 0 && unit.currentTile == tileForCitadel) + UnitActions.getBuildImprovementAction(unit)?.action?.invoke() + } else unit.movement.headTowards(cityToGarrison) return } diff --git a/core/src/com/unciv/logic/automation/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/WorkerAutomation.kt index 31d44c593a..17d3f25bea 100644 --- a/core/src/com/unciv/logic/automation/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/WorkerAutomation.kt @@ -160,7 +160,7 @@ class WorkerAutomation(val unit: MapUnit) { return false // cou;dn't find anything to construct here } - private fun getPriority(tileInfo: TileInfo, civInfo: CivilizationInfo): Int { + fun getPriority(tileInfo: TileInfo, civInfo: CivilizationInfo): Int { var priority = 0 if (tileInfo.getOwner() == civInfo){ priority += 2 @@ -195,7 +195,7 @@ class WorkerAutomation(val unit: MapUnit) { // Defence is more important that civilian improvements // While AI sucks in strategical placement of forts, allow a human does it manually - !civInfo.isPlayerCivilization() && evaluateFortPlacement(tile,civInfo) -> Constants.fort + !civInfo.isPlayerCivilization() && evaluateFortPlacement(tile,civInfo,false) -> Constants.fort // I think we can assume that the unique improvement is better uniqueImprovement!=null && tile.canBuildImprovement(uniqueImprovement,civInfo) -> uniqueImprovement.name @@ -226,21 +226,21 @@ class WorkerAutomation(val unit: MapUnit) { private fun isAcceptableTileForFort(tile: TileInfo, civInfo: CivilizationInfo): Boolean { - // don't build fort in the city - if (tile.isCityCenter()) return false - // don't build fort if it is already here - if (tile.improvement == Constants.fort) return false - // don't build on resource tiles - if (tile.hasViewableResource(civInfo)) return false - // don't build on great improvements - if (tile.containsGreatImprovement() || tile.containsUnfinishedGreatImprovement()) return false + if (tile.isCityCenter() // don't build fort in the city + || !tile.isLand // don't build fort in the water + || tile.improvement == Constants.fort // don't build fort if it is already here + || tile.hasViewableResource(civInfo) // don't build on resource tiles + || tile.containsGreatImprovement() // don't build on great improvements (including citadel) + || tile.containsUnfinishedGreatImprovement()) return false return true } - fun evaluateFortPlacement(tile: TileInfo, civInfo: CivilizationInfo): Boolean { + fun evaluateFortPlacement(tile: TileInfo, civInfo: CivilizationInfo, isCitadel: Boolean): Boolean { // build on our land only - if ((tile.owningCity?.civInfo != civInfo) || + if ((tile.owningCity?.civInfo != civInfo && + // except citadel which can be built near-by + (!isCitadel || tile.neighbors.all { it.getOwner() != civInfo })) || !isAcceptableTileForFort(tile, civInfo)) return false val isHills = tile.getBaseTerrain().name == Constants.hill @@ -286,11 +286,14 @@ class WorkerAutomation(val unit: MapUnit) { val distanceToEnemy = tile.aerialDistanceTo(closestEnemyCity) // find closest our city to defend from this enemy city - val closestOurCity = tile.owningCity!!.getCenterTile() + val closestOurCity = civInfo.cities.minBy { it.getCenterTile().aerialDistanceTo(tile) }!!.getCenterTile() + val distanceToOurCity = tile.aerialDistanceTo(closestOurCity) + val distanceBetweenCities = closestEnemyCity.aerialDistanceTo(closestOurCity) // let's build fort on the front line, not behind the city - return distanceBetweenCities > distanceToEnemy + // +2 is a acceptable deviation from the straight line between cities + return distanceBetweenCities + 2 > distanceToEnemy + distanceToOurCity } } \ No newline at end of file diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index e5658f7dcf..b59ab7a3aa 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -49,6 +49,7 @@ enum class DiplomaticModifiers{ BetrayedPromiseToNotSettleCitiesNearUs, UnacceptableDemands, UsedNuclearWeapons, + StealingTerritory, YearsOfPeace, SharedEnemy, diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index 345592bd6a..56731cc446 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -303,6 +303,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { FulfilledPromiseToNotSettleCitiesNearUs -> "You fulfilled your promise to stop settling cities near us!" UnacceptableDemands -> "Your arrogant demands are in bad taste" UsedNuclearWeapons -> "Your use of nuclear weapons is disgusting!" + StealingTerritory -> "You have stolen our lands!" } text = text.tr() + " " if (modifier.value > 0) text += "+" diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index ed38371dc1..67764bb487 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -7,6 +7,7 @@ import com.unciv.UniqueAbility import com.unciv.logic.automation.UnitAutomation import com.unciv.logic.automation.WorkerAutomation import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.map.MapUnit import com.unciv.logic.map.RoadStatus import com.unciv.logic.map.TileInfo @@ -364,6 +365,8 @@ object UnitActions { unitTile.improvement = improvementName unitTile.improvementInProgress = null unitTile.turnsToImprovement = 0 + if (improvementName == Constants.citadel) + takeOverTilesAround(unit) val city = unitTile.getCity() if (city != null) { city.cityStats.update() @@ -373,11 +376,37 @@ object UnitActions { unit.destroy() }.takeIf { unit.currentMovement > 0f && !tile.isWater && !tile.isCityCenter() && !tile.getLastTerrain().impassable && - tile.improvement != improvementName }) + tile.improvement != improvementName && + // citadel can be built only next to or within own borders + (improvementName != Constants.citadel || + tile.neighbors.any { it.getOwner() == unit.civInfo })}) } return null } + private fun takeOverTilesAround(unit: MapUnit) { + // one of the neighbour tile must belong to unit's civ, so nearestCity will be never `null` + val nearestCity = unit.currentTile.neighbors.first { it.getOwner() == unit.civInfo }.getCity() + // capture all tiles which do not belong to unit's civ and are not enemy cities + // we use getTilesInDistance here, not neighbours to include the current tile as well + val tilesToTakeOver = unit.currentTile.getTilesInDistance(1) + .filter { !it.isCityCenter() && it.getOwner() != unit.civInfo } + // make a set of civs to be notified (a set - in order to not repeat notification on each tile) + val notifications = mutableSetOf() + // take over the ownership + for (tile in tilesToTakeOver) { + val otherCiv = tile.getOwner() + if (otherCiv != null) { + // decrease relations for -10 pt/tile + otherCiv.getDiplomacyManager(unit.civInfo).addModifier(DiplomaticModifiers.StealingTerritory, -10f) + notifications.add(otherCiv) + } + nearestCity!!.expansion.takeOwnership(tile) + } + for (otherCiv in notifications) + otherCiv.addNotification("${unit.civInfo} has stolen your territory!", unit.currentTile.position, Color.RED) + } + private fun addGoldPerGreatPersonUsage(civInfo: CivilizationInfo) { val uniqueText = "Provides a sum of gold each time you spend a Great Person" val cityWithMausoleum = civInfo.cities.firstOrNull { it.containsBuildingUnique(uniqueText) }