From d1b2d652e326ca9d4238a1a0be02139a7cea278f Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Thu, 28 Sep 2023 15:30:39 +0300 Subject: [PATCH] reorg: Separated UnitActions into 3 files: - UnitActionsFromUniques - UnitActionModifiers - UnitActions retains actions relevant to all units --- .../automation/unit/SpecificUnitAutomation.kt | 19 +- .../logic/automation/unit/UnitAutomation.kt | 2 +- .../logic/automation/unit/WorkerAutomation.kt | 8 +- .../map/tile/TileInfoImprovementFunctions.kt | 49 +- .../unit/actions/UnitActionModifiers.kt | 84 +++ .../worldscreen/unit/actions/UnitActions.kt | 548 +----------------- .../unit/actions/UnitActionsFromUniques.kt | 414 +++++++++++++ .../unit/actions/UnitActionsPillage.kt | 10 +- .../unit/actions/UnitActionsReligion.kt | 6 +- .../src/com/unciv/uniques/UnitUniquesTests.kt | 8 +- 10 files changed, 603 insertions(+), 545 deletions(-) create mode 100644 core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionModifiers.kt create mode 100644 core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt diff --git a/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt index 3e6ba788aa..3c3cdf14c5 100644 --- a/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt @@ -18,6 +18,7 @@ import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions +import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsReligion object SpecificUnitAutomation { @@ -58,13 +59,13 @@ object SpecificUnitAutomation { if (tileToSteal != null) { unit.movement.headTowards(tileToSteal) if (unit.currentMovement > 0 && unit.currentTile == tileToSteal) - UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke() + UnitActionsFromUniques.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke() return true } // try to build a citadel for defensive purposes if (unit.civ.getWorkerAutomation().evaluateFortPlacement(unit.currentTile, true)) { - UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke() + UnitActionsFromUniques.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke() return true } return false @@ -98,13 +99,13 @@ object SpecificUnitAutomation { } unit.movement.headTowards(tileForCitadel) if (unit.currentMovement > 0 && unit.currentTile == tileForCitadel) - UnitActions.getImprovementConstructionActions(unit, unit.currentTile) + UnitActionsFromUniques.getImprovementConstructionActions(unit, unit.currentTile) .firstOrNull()?.action?.invoke() } fun automateSettlerActions(unit: MapUnit, tilesWhereWeWillBeCaptured: Set) { if (unit.civ.gameInfo.turns == 0) { // Special case, we want AI to settle in place on turn 1. - val foundCityAction = UnitActions.getFoundCityAction(unit, unit.getTile()) + val foundCityAction = UnitActionsFromUniques.getFoundCityAction(unit, unit.getTile()) // Depending on era and difficulty we might start with more than one settler. In that case settle the one with the best location val otherSettlers = unit.civ.units.getCivUnits().filter { it.currentMovement > 0 && it.baseUnit == unit.baseUnit } if (foundCityAction?.action != null && @@ -146,7 +147,7 @@ object SpecificUnitAutomation { return } - val foundCityAction = UnitActions.getFoundCityAction(unit, bestCityLocation) + val foundCityAction = UnitActionsFromUniques.getFoundCityAction(unit, bestCityLocation) if (foundCityAction?.action == null) { // this means either currentMove == 0 or city within 3 tiles if (unit.currentMovement > 0) // therefore, city within 3 tiles throw Exception("City within distance") @@ -215,9 +216,10 @@ object SpecificUnitAutomation { unit.movement.headTowards(chosenTile) if (unit.currentTile == chosenTile) { if (unit.currentTile.isPillaged()) - UnitActions.getRepairAction(unit).invoke() + UnitActions.getUnitActions(unit).firstOrNull { it.type == UnitActionType.Repair } + ?.action?.invoke() else - UnitActions.getImprovementConstructionActions(unit, unit.currentTile) + UnitActionsFromUniques.getImprovementConstructionActions(unit, unit.currentTile) .firstOrNull()?.action?.invoke() return true } @@ -320,8 +322,7 @@ object SpecificUnitAutomation { if (unit.movement.canReach(capitalTile)) unit.movement.headTowards(capitalTile) if (unit.getTile() == capitalTile) { - UnitActions.getAddInCapitalAction(unit, capitalTile).action!!() - return + UnitActionsFromUniques.getAddInCapitalAction(unit, capitalTile).action?.invoke() } } diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index 319a5a8496..31cf999926 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -462,7 +462,7 @@ object UnitAutomation { val unitDistanceToTiles = unit.movement.getDistanceToTiles() val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles .filter { it.value.totalDistance < unit.currentMovement }.keys - .filter { unit.movement.canMoveTo(it) && UnitActions.canPillage(unit, it) + .filter { unit.movement.canMoveTo(it) && UnitActionsPillage.canPillage(unit, it) && (it.canPillageTileImprovement() || (it.canPillageRoad() && it.getRoadOwner() != null && unit.civ.isAtWarWith(it.getRoadOwner()!!)))} diff --git a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt index 3e60e8277c..a0a828a1e5 100644 --- a/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/WorkerAutomation.kt @@ -19,7 +19,7 @@ import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.UniqueType -import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions +import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques import com.unciv.utils.Log import com.unciv.utils.debug @@ -141,7 +141,7 @@ class WorkerAutomation( if (unit.currentMovement > 0 && reachedTile == tileToWork) { if (reachedTile.isPillaged()) { debug("WorkerAutomation: ${unit.label()} -> repairs $reachedTile") - UnitActions.getRepairAction(unit).invoke() + UnitActionsFromUniques.getRepairAction(unit)?.action?.invoke() return } if (reachedTile.improvementInProgress == null && reachedTile.isLand @@ -158,7 +158,7 @@ class WorkerAutomation( if (currentTile.isPillaged()) { debug("WorkerAutomation: ${unit.label()} -> repairs $currentTile") - UnitActions.getRepairAction(unit).invoke() + UnitActionsFromUniques.getRepairAction(unit)?.action?.invoke() return } @@ -587,7 +587,7 @@ class WorkerAutomation( // all conditionals succeed with a current StateForConditionals(civ, unit) // todo: Not necessarily the optimal flow: Be optimistic and head towards, // then when arrived and the conditionals say "no" do something else instead? - val action = UnitActions.getWaterImprovementAction(unit) + val action = UnitActionsFromUniques.getWaterImprovementAction(unit) ?: return false // If action.action is null that means only transient reasons prevent the improvement - diff --git a/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt b/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt index 9e16029da8..674ccc9e9a 100644 --- a/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt +++ b/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt @@ -5,10 +5,10 @@ import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.LocationAction import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationIcon +import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueType -import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions enum class ImprovementBuildingProblem { @@ -203,7 +203,7 @@ class TileInfoImprovementFunctions(val tile: Tile) { if (civToActivateBroaderEffects != null && improvementObject != null && improvementObject.hasUnique(UniqueType.TakesOverAdjacentTiles) ) - UnitActions.takeOverTilesAround(civToActivateBroaderEffects, tile) + takeOverTilesAround(civToActivateBroaderEffects, tile) val city = tile.owningCity if (city != null) { @@ -261,6 +261,51 @@ class TileInfoImprovementFunctions(val tile: Tile) { } } + private fun takeOverTilesAround(civ: Civilization, tile: Tile) { + // This method should only be called for a citadel - therefore one of the neighbour tile + // must belong to unit's civ, so minByOrNull in the nearestCity formula should be never `null`. + // That is, unless a mod does not specify the proper unique - then fallbackNearestCity will take over. + + fun priority(tile: Tile): Int { // helper calculates priority (lower is better): distance plus razing malus + val city = tile.getCity()!! // !! assertion is guaranteed by the outer filter selector. + return city.getCenterTile().aerialDistanceTo(tile) + + (if (city.isBeingRazed) 5 else 0) + } + fun fallbackNearestCity(civ: Civilization, tile: Tile) = + civ.cities.minByOrNull { + it.getCenterTile().aerialDistanceTo(tile) + + (if (it.isBeingRazed) 5 else 0) + }!! + + // In the rare case more than one city owns tiles neighboring the citadel + // this will prioritize the nearest one not being razed + val nearestCity = tile.neighbors + .filter { it.getOwner() == civ } + .minByOrNull { priority(it) }?.getCity() + ?: fallbackNearestCity(civ, tile) + + // 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 = tile.getTilesInDistance(1) + .filter { !it.isCityCenter() && it.getOwner() != civ } + + val civsToNotify = mutableSetOf() + for (tileToTakeOver in tilesToTakeOver) { + val otherCiv = tileToTakeOver.getOwner() + if (otherCiv != null) { + // decrease relations for -10 pt/tile + if (!otherCiv.knows(civ)) otherCiv.diplomacyFunctions.makeCivilizationsMeet(civ) + otherCiv.getDiplomacyManager(civ).addModifier(DiplomaticModifiers.StealingTerritory, -10f) + civsToNotify.add(otherCiv) + } + nearestCity.expansion.takeOwnership(tileToTakeOver) + } + + for (otherCiv in civsToNotify) + otherCiv.addNotification("Your territory has been stolen by [$civ]!", + tile.position, NotificationCategory.Cities, civ.civName, NotificationIcon.War) + } + /** Marks tile as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */ diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionModifiers.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionModifiers.kt new file mode 100644 index 0000000000..b538eca387 --- /dev/null +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionModifiers.kt @@ -0,0 +1,84 @@ +package com.unciv.ui.screens.worldscreen.unit.actions + +import com.unciv.logic.map.mapunit.MapUnit +import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.translations.removeConditionals +import com.unciv.models.translations.tr +import com.unciv.ui.components.Fonts + +object UnitActionModifiers { + + private fun getMovementPointsToUse(actionUnique: Unique): Int { + val movementCost = actionUnique.conditionals + .filter { it.type == UniqueType.UnitActionMovementCost } + .minOfOrNull { it.params[0].toInt() } + if (movementCost != null) return movementCost + return 1 + } + + fun activateSideEffects(unit: MapUnit, actionUnique: Unique){ + val movementCost = getMovementPointsToUse(actionUnique) + unit.useMovementPoints(movementCost.toFloat()) + + for (conditional in actionUnique.conditionals){ + when (conditional.type){ + UniqueType.UnitActionConsumeUnit -> unit.consume() + UniqueType.UnitActionLimitedTimes, UniqueType.UnitActionOnce -> { + if (usagesLeft(unit, actionUnique) == 1 + && actionUnique.conditionals.any { it.type== UniqueType.UnitActionAfterWhichConsumed }) { + unit.consume() + continue + } + val usagesSoFar = unit.abilityToTimesUsed[actionUnique.placeholderText] ?: 0 + unit.abilityToTimesUsed[actionUnique.placeholderText] = usagesSoFar + 1 + } + else -> continue + } + } + } + + /** Returns 'null' if usages are not limited */ + fun usagesLeft(unit: MapUnit, actionUnique: Unique): Int?{ + val usagesTotal = getMaxUsages(unit, actionUnique) ?: return null + val usagesSoFar = unit.abilityToTimesUsed[actionUnique.placeholderText] ?: 0 + return usagesTotal - usagesSoFar + } + + private fun getMaxUsages(unit: MapUnit, actionUnique: Unique): Int? { + val extraTimes = unit.getMatchingUniques(actionUnique.type!!) + .filter { it.text.removeConditionals() == actionUnique.text.removeConditionals() } + .flatMap { unique -> unique.conditionals.filter { it.type == UniqueType.UnitActionExtraLimitedTimes } } + .sumOf { it.params[0].toInt() } + + val times = actionUnique.conditionals + .filter { it.type == UniqueType.UnitActionLimitedTimes } + .maxOfOrNull { it.params[0].toInt() } + if (times != null) return times + extraTimes + if (actionUnique.conditionals.any { it.type == UniqueType.UnitActionOnce }) return 1 + extraTimes + + return null + } + + fun actionTextWithSideEffects(originalText: String, actionUnique: Unique, unit: MapUnit): String { + val sideEffectString = getSideEffectString(unit, actionUnique) + if (sideEffectString == "") return originalText + else return "{$originalText} $sideEffectString" + } + + private fun getSideEffectString(unit: MapUnit, actionUnique: Unique): String { + val effects = ArrayList() + + val maxUsages = getMaxUsages(unit, actionUnique) + if (maxUsages!=null) effects += "${usagesLeft(unit, actionUnique)}/$maxUsages" + + if (actionUnique.conditionals.any { it.type == UniqueType.UnitActionConsumeUnit } + || actionUnique.conditionals.any { it.type == UniqueType.UnitActionAfterWhichConsumed } && usagesLeft(unit, actionUnique) == 1 + ) effects += Fonts.death.toString() + else effects += getMovementPointsToUse(actionUnique).toString() + Fonts.movement + + + return if (effects.isEmpty()) "" + else "(${effects.joinToString { it.tr() }})" + } +} 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 adc9f00ebb..6020f1c778 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 @@ -1,33 +1,17 @@ package com.unciv.ui.screens.worldscreen.unit.actions -import com.unciv.Constants import com.unciv.GUI import com.unciv.UncivGame import com.unciv.logic.automation.unit.UnitAutomation -import com.unciv.logic.civilization.Civilization -import com.unciv.logic.civilization.NotificationCategory -import com.unciv.logic.civilization.NotificationIcon -import com.unciv.logic.civilization.PlayerType -import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.tile.Tile -import com.unciv.models.Counter -import com.unciv.models.UncivSound import com.unciv.models.UnitAction import com.unciv.models.UnitActionType -import com.unciv.models.ruleset.unique.StateForConditionals -import com.unciv.models.ruleset.unique.Unique -import com.unciv.models.ruleset.unique.UniqueTarget -import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType -import com.unciv.models.translations.fillPlaceholders -import com.unciv.models.translations.removeConditionals import com.unciv.models.translations.tr -import com.unciv.ui.components.Fonts import com.unciv.ui.popups.ConfirmPopup import com.unciv.ui.popups.hasOpenPopups -import com.unciv.ui.screens.pickerscreens.ImprovementPickerScreen import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen object UnitActions { @@ -42,24 +26,25 @@ object UnitActions { val actionList = ArrayList() // Determined by unit uniques - addTransformActions(unit, actionList) - addParadropAction(unit, actionList) - addAirSweepAction(unit, actionList) - addSetupAction(unit, actionList) - addFoundCityAction(unit, actionList, tile) - addBuildingImprovementsAction(unit, actionList, tile) - addRepairAction(unit, actionList) - addCreateWaterImprovements(unit, actionList) + UnitActionsFromUniques.addTransformActions(unit, actionList) + UnitActionsFromUniques.addParadropAction(unit, actionList) + UnitActionsFromUniques.addAirSweepAction(unit, actionList) + UnitActionsFromUniques.addSetupAction(unit, actionList) + UnitActionsFromUniques.addFoundCityAction(unit, actionList, tile) + UnitActionsFromUniques.addBuildingImprovementsAction(unit, actionList, tile) + UnitActionsFromUniques.addRepairAction(unit, actionList) + UnitActionsFromUniques.addCreateWaterImprovements(unit, actionList) UnitActionsGreatPerson.addGreatPersonActions(unit, actionList, tile) UnitActionsReligion.addFoundReligionAction(unit, actionList) UnitActionsReligion.addEnhanceReligionAction(unit, actionList) - actionList += getImprovementConstructionActions(unit, tile) + actionList += UnitActionsFromUniques.getImprovementConstructionActions(unit, tile) UnitActionsReligion.addActionsWithLimitedUses(unit, actionList, tile) - addAutomateAction(unit, actionList, true) - addTriggerUniqueActions(unit, actionList) - addAddInCapitalAction(unit, actionList, tile) + UnitActionsFromUniques.addTriggerUniqueActions(unit, actionList) + UnitActionsFromUniques.addAddInCapitalAction(unit, actionList, tile) + // General actions + addAutomateAction(unit, actionList, true) if (unit.isMoving()) { actionList += UnitAction(UnitActionType.StopMovement) { unit.action = null } } @@ -149,108 +134,6 @@ object UnitActions { }.takeIf { unit.currentMovement > 0 }) } - private fun addCreateWaterImprovements(unit: MapUnit, actionList: ArrayList) { - val waterImprovementAction = getWaterImprovementAction(unit) - if (waterImprovementAction != null) actionList += waterImprovementAction - } - - fun getWaterImprovementAction(unit: MapUnit): UnitAction? { - val tile = unit.currentTile - if (!tile.isWater || !unit.hasUnique(UniqueType.CreateWaterImprovements) || tile.resource == null) return null - - val improvementName = tile.tileResource.getImprovingImprovement(tile, unit.civ) ?: return null - val improvement = tile.ruleset.tileImprovements[improvementName] ?: return null - if (!tile.improvementFunctions.canBuildImprovement(improvement, unit.civ)) return null - - return UnitAction(UnitActionType.Create, "Create [$improvementName]", - action = { - tile.changeImprovement(improvementName, unit.civ) - unit.destroy() // Modders may wish for a nondestructive way, but that should be another Unique - }.takeIf { unit.currentMovement > 0 }) - } - - - private fun addFoundCityAction(unit: MapUnit, actionList: ArrayList, tile: Tile) { - val getFoundCityAction = getFoundCityAction(unit, tile) - if (getFoundCityAction != null) actionList += getFoundCityAction - } - - /** Produce a [UnitAction] for founding a city. - * @param unit The unit to do the founding. - * @param tile The tile to found a city on. - * @return null if impossible (the unit lacks the ability to found), - * or else a [UnitAction] 'defining' the founding. - * The [action][UnitAction.action] field will be null if the action cannot be done here and now - * (no movement left, too close to another city). - */ - fun getFoundCityAction(unit: MapUnit, tile: Tile): UnitAction? { - val unique = unit.getMatchingUniques(UniqueType.FoundCity) - .filter { unique -> unique.conditionals.none { it.type == UniqueType.UnitActionExtraLimitedTimes } } - .firstOrNull() - if (unique == null || tile.isWater || tile.isImpassible()) return null - // Spain should still be able to build Conquistadors in a one city challenge - but can't settle them - if (unit.civ.isOneCityChallenger() && unit.civ.hasEverOwnedOriginalCapital) return null - if (usagesLeft(unit, unique)==0) return null - - if (unit.currentMovement <= 0 || !tile.canBeSettled()) - return UnitAction(UnitActionType.FoundCity, action = null) - - val hasActionModifiers = unique.conditionals.any { it.type?.targetTypes?.contains(UniqueTarget.UnitActionModifier) == true } - val foundAction = { - if (unit.civ.playerType != PlayerType.AI) - UncivGame.Current.settings.addCompletedTutorialTask("Found city") - unit.civ.addCity(tile.position) - if (tile.ruleset.tileImprovements.containsKey(Constants.cityCenter)) - tile.changeImprovement(Constants.cityCenter) - tile.removeRoad() - - if (hasActionModifiers) activateSideEffects(unit, unique) - else unit.destroy() - GUI.setUpdateWorldOnNextRender() // Set manually, since this could be triggered from the ConfirmPopup and not from the UnitActionsTable - } - - if (unit.civ.playerType == PlayerType.AI) - return UnitAction(UnitActionType.FoundCity, action = foundAction) - - return UnitAction( - type = UnitActionType.FoundCity, - title = - if (hasActionModifiers) actionTextWithSideEffects(UnitActionType.FoundCity.value, unique, unit) - else UnitActionType.FoundCity.value, - uncivSound = UncivSound.Chimes, - action = { - // check if we would be breaking a promise - val leaders = testPromiseNotToSettle(unit.civ, tile) - if (leaders == null) - foundAction() - else { - // ask if we would be breaking a promise - val text = "Do you want to break your promise to [$leaders]?" - ConfirmPopup(GUI.getWorldScreen(), text, "Break promise", action = foundAction).open(force = true) - } - } - ) - } - - /** - * Checks whether a civ founding a city on a certain tile would break a promise. - * @param civInfo The civilization trying to found a city - * @param tile The tile where the new city would go - * @return null if no promises broken, else a String listing the leader(s) we would p* off. - */ - private fun testPromiseNotToSettle(civInfo: Civilization, tile: Tile): String? { - val brokenPromises = HashSet() - for (otherCiv in civInfo.getKnownCivs().filter { it.isMajorCiv() && !civInfo.isAtWarWith(it) }) { - val diplomacyManager = otherCiv.getDiplomacyManager(civInfo) - if (diplomacyManager.hasFlag(DiplomacyFlags.AgreedToNotSettleNearUs)) { - val citiesWithin6Tiles = otherCiv.cities - .filter { it.getCenterTile().aerialDistanceTo(tile) <= 6 } - .filter { otherCiv.hasExplored(it.getCenterTile()) } - if (citiesWithin6Tiles.isNotEmpty()) brokenPromises += otherCiv.getLeaderDisplayName() - } - } - return if(brokenPromises.isEmpty()) null else brokenPromises.joinToString(", ") - } private fun addPromoteAction(unit: MapUnit, actionList: ArrayList) { if (unit.isCivilian() || !unit.promotions.canBePromoted()) return @@ -261,50 +144,6 @@ object UnitActions { }.takeIf { unit.currentMovement > 0 && unit.attacksThisTurn == 0 }) } - private fun addSetupAction(unit: MapUnit, actionList: ArrayList) { - if (!unit.hasUnique(UniqueType.MustSetUp) || unit.isEmbarked()) return - val isSetUp = unit.isSetUpForSiege() - actionList += UnitAction(UnitActionType.SetUp, - isCurrentAction = isSetUp, - action = { - unit.action = UnitActionType.SetUp.value - unit.useMovementPoints(1f) - }.takeIf { unit.currentMovement > 0 && !isSetUp }) - } - - private fun addParadropAction(unit: MapUnit, actionList: ArrayList) { - val paradropUniques = - unit.getMatchingUniques(UniqueType.MayParadrop) - if (!paradropUniques.any() || unit.isEmbarked()) return - unit.cache.paradropRange = paradropUniques.maxOfOrNull { it.params[0] }!!.toInt() - actionList += UnitAction(UnitActionType.Paradrop, - isCurrentAction = unit.isPreparingParadrop(), - action = { - if (unit.isPreparingParadrop()) unit.action = null - else unit.action = UnitActionType.Paradrop.value - }.takeIf { - !unit.hasUnitMovedThisTurn() && - unit.currentTile.isFriendlyTerritory(unit.civ) && - !unit.isEmbarked() - }) - } - - private fun addAirSweepAction(unit: MapUnit, actionList: ArrayList) { - val airsweepUniques = - unit.getMatchingUniques(UniqueType.CanAirsweep) - if (!airsweepUniques.any()) return - actionList += UnitAction(UnitActionType.AirSweep, - isCurrentAction = unit.isPreparingAirSweep(), - action = { - if (unit.isPreparingAirSweep()) unit.action = null - else unit.action = UnitActionType.AirSweep.value - }.takeIf { - unit.canAttack() - } - ) - } - - private fun addExplorationActions(unit: MapUnit, actionList: ArrayList) { if (unit.baseUnit.movesLikeAirUnits()) return if (unit.isExploring()) return @@ -314,252 +153,6 @@ object UnitActions { } } - private fun addTransformActions( - unit: MapUnit, - actionList: ArrayList - ) { - val upgradeAction = getTransformActions(unit) - actionList += upgradeAction - } - - /** */ - private fun getTransformActions( - unit: MapUnit - ): ArrayList { - val unitTile = unit.getTile() - val civInfo = unit.civ - val stateForConditionals = StateForConditionals(unit = unit, civInfo = civInfo, tile = unitTile) - val transformList = ArrayList() - for (unique in unit.baseUnit().getMatchingUniques(UniqueType.CanTransform, stateForConditionals)) { - val unitToTransformTo = civInfo.getEquivalentUnit(unique.params[0]) - - if (unitToTransformTo.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals) - .any { !it.conditionalsApply(stateForConditionals) }) - continue - - // Check _new_ resource requirements - // Using Counter to aggregate is a bit exaggerated, but - respect the mad modder. - val resourceRequirementsDelta = Counter() - for ((resource, amount) in unit.baseUnit().getResourceRequirementsPerTurn()) - resourceRequirementsDelta.add(resource, -amount) - for ((resource, amount) in unitToTransformTo.getResourceRequirementsPerTurn()) - resourceRequirementsDelta.add(resource, amount) - val newResourceRequirementsString = resourceRequirementsDelta.entries - .filter { it.value > 0 } - .joinToString { "${it.value} {${it.key}}".tr() } - - val title = if (newResourceRequirementsString.isEmpty()) - "Transform to [${unitToTransformTo.name}]" - else "Transform to [${unitToTransformTo.name}]\n([$newResourceRequirementsString])" - - transformList.add(UnitAction(UnitActionType.Transform, - title = title, - action = { - unit.destroy() - val newUnit = civInfo.units.placeUnitNearTile(unitTile.position, unitToTransformTo) - - /** We were UNABLE to place the new unit, which means that the unit failed to upgrade! - * The only known cause of this currently is "land units upgrading to water units" which fail to be placed. - */ - if (newUnit == null) { - val resurrectedUnit = civInfo.units.placeUnitNearTile(unitTile.position, unit.baseUnit)!! - unit.copyStatisticsTo(resurrectedUnit) - } else { // Managed to upgrade - unit.copyStatisticsTo(newUnit) - newUnit.currentMovement = 0f - } - }.takeIf { - unit.currentMovement > 0 && !unit.isEmbarked() - } - ) ) - } - return transformList - } - - private fun addBuildingImprovementsAction( - unit: MapUnit, - actionList: ArrayList, - tile: Tile) { - if (!unit.cache.hasUniqueToBuildImprovements) return - - val couldConstruct = unit.currentMovement > 0 - && !tile.isCityCenter() - && unit.civ.gameInfo.ruleset.tileImprovements.values.any { - ImprovementPickerScreen.canReport(tile.improvementFunctions.getImprovementBuildingProblems(it, unit.civ).toSet()) - && unit.canBuildImprovement(it) - } - - actionList += UnitAction(UnitActionType.ConstructImprovement, - isCurrentAction = unit.currentTile.hasImprovementInProgress(), - action = { - GUI.pushScreen(ImprovementPickerScreen(tile, unit) { - if (GUI.getSettings().autoUnitCycle) - GUI.getWorldScreen().switchToNextUnit() - }) - }.takeIf { couldConstruct } - ) - } - - private fun getRepairTurns(unit: MapUnit): Int { - val tile = unit.currentTile - if (!tile.isPillaged()) return 0 - if (tile.improvementInProgress == Constants.repair) return tile.turnsToImprovement - var repairTurns = tile.ruleset.tileImprovements[Constants.repair]!!.getTurnsToBuild(unit.civ, unit) - - val pillagedImprovement = tile.getImprovementToRepair()!! - val turnsToBuild = pillagedImprovement.getTurnsToBuild(unit.civ, unit) - // cap repair to number of turns to build original improvement - if (turnsToBuild < repairTurns) repairTurns = turnsToBuild - return repairTurns - } - - private fun addRepairAction(unit: MapUnit, actionList: ArrayList) { - if (!unit.currentTile.ruleset.tileImprovements.containsKey(Constants.repair)) return - if (!unit.cache.hasUniqueToBuildImprovements) return - if (unit.isEmbarked()) return - val tile = unit.getTile() - if (tile.isCityCenter()) return - if (!tile.isPillaged()) return - - val couldConstruct = unit.currentMovement > 0 - && !tile.isCityCenter() && tile.improvementInProgress != Constants.repair - - val turnsToBuild = getRepairTurns(unit) - - actionList += UnitAction(UnitActionType.Repair, - title = "${UnitActionType.Repair} [${unit.currentTile.getImprovementToRepair()!!.name}] - [${turnsToBuild}${Fonts.turn}]", - action = getRepairAction(unit).takeIf { couldConstruct } - ) - } - - fun getRepairAction(unit: MapUnit): () -> Unit { - return { - val tile = unit.currentTile - tile.turnsToImprovement = getRepairTurns(unit) - tile.improvementInProgress = Constants.repair - } - } - - private fun addAutomateAction(unit: MapUnit, actionList: ArrayList, showingAdditionalActions:Boolean) { - - // If either of these are true it goes in primary actions, else in additional actions - if ((unit.hasUnique(UniqueType.AutomationPrimaryAction) || unit.cache.hasUniqueToBuildImprovements) != showingAdditionalActions) - return - - if (unit.isAutomated()) return - - actionList += UnitAction(UnitActionType.Automate, - isCurrentAction = unit.isAutomated(), - action = { - unit.action = UnitActionType.Automate.value - UnitAutomation.automateUnitMoves(unit) - }.takeIf { unit.currentMovement > 0 } - ) - } - - fun getAddInCapitalAction(unit: MapUnit, tile: Tile): UnitAction { - return UnitAction(UnitActionType.AddInCapital, - title = "Add to [${unit.getMatchingUniques(UniqueType.AddInCapital).first().params[0]}]", - action = { - unit.civ.victoryManager.currentsSpaceshipParts.add(unit.name, 1) - unit.destroy() - }.takeIf { tile.isCityCenter() && tile.getCity()!!.isCapital() && tile.getCity()!!.civ == unit.civ } - ) - } - - private fun addAddInCapitalAction(unit: MapUnit, actionList: ArrayList, tile: Tile) { - if (!unit.hasUnique(UniqueType.AddInCapital)) return - - actionList += getAddInCapitalAction(unit, tile) - } - - fun getImprovementConstructionActions(unit: MapUnit, tile: Tile): ArrayList { - val finalActions = ArrayList() - val uniquesToCheck = unit.getMatchingUniques(UniqueType.ConstructImprovementInstantly) - val civResources = unit.civ.getCivResourcesByName() - - for (unique in uniquesToCheck) { - // Skip actions with a "[amount] extra times" conditional - these are treated in addTriggerUniqueActions instead - if (unique.conditionals.any { it.type == UniqueType.UnitActionExtraLimitedTimes }) continue - - val improvementName = unique.params[0] - val improvement = tile.ruleset.tileImprovements[improvementName] - ?: continue - if (usagesLeft(unit, unique) == 0) continue - - val resourcesAvailable = improvement.uniqueObjects.none { - improvementUnique -> - improvementUnique.isOfType(UniqueType.ConsumesResources) && - (civResources[improvementUnique.params[1]] ?: 0) < improvementUnique.params[0].toInt() - } - - finalActions += UnitAction(UnitActionType.Create, - title = actionTextWithSideEffects("Create [$improvementName]", unique, unit), - action = { - val unitTile = unit.getTile() - unitTile.changeImprovement(improvementName, unit.civ) - - // without this the world screen won't show the improvement because it isn't the 'last seen improvement' - unit.civ.cache.updateViewableTiles() - - activateSideEffects(unit, unique) - }.takeIf { - resourcesAvailable - && unit.currentMovement > 0f - && tile.improvementFunctions.canBuildImprovement(improvement, unit.civ) - // Next test is to prevent interfering with UniqueType.CreatesOneImprovement - - // not pretty, but users *can* remove the building from the city queue an thus clear this: - && !tile.isMarkedForCreatesOneImprovement() - && !tile.isImpassible() // Not 100% sure that this check is necessary... - }) - } - return finalActions - } - - fun takeOverTilesAround(civ: Civilization, tile: Tile) { - // This method should only be called for a citadel - therefore one of the neighbour tile - // must belong to unit's civ, so minByOrNull in the nearestCity formula should be never `null`. - // That is, unless a mod does not specify the proper unique - then fallbackNearestCity will take over. - - fun priority(tile: Tile): Int { // helper calculates priority (lower is better): distance plus razing malus - val city = tile.getCity()!! // !! assertion is guaranteed by the outer filter selector. - return city.getCenterTile().aerialDistanceTo(tile) + - (if (city.isBeingRazed) 5 else 0) - } - fun fallbackNearestCity(civ: Civilization, tile: Tile) = - civ.cities.minByOrNull { - it.getCenterTile().aerialDistanceTo(tile) + - (if (it.isBeingRazed) 5 else 0) - }!! - - // In the rare case more than one city owns tiles neighboring the citadel - // this will prioritize the nearest one not being razed - val nearestCity = tile.neighbors - .filter { it.getOwner() == civ } - .minByOrNull { priority(it) }?.getCity() - ?: fallbackNearestCity(civ, tile) - - // 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 = tile.getTilesInDistance(1) - .filter { !it.isCityCenter() && it.getOwner() != civ } - - val civsToNotify = mutableSetOf() - for (tileToTakeOver in tilesToTakeOver) { - val otherCiv = tileToTakeOver.getOwner() - if (otherCiv != null) { - // decrease relations for -10 pt/tile - if (!otherCiv.knows(civ)) otherCiv.diplomacyFunctions.makeCivilizationsMeet(civ) - otherCiv.getDiplomacyManager(civ).addModifier(DiplomaticModifiers.StealingTerritory, -10f) - civsToNotify.add(otherCiv) - } - nearestCity.expansion.takeOwnership(tileToTakeOver) - } - - for (otherCiv in civsToNotify) - otherCiv.addNotification("Your territory has been stolen by [$civ]!", - tile.position, NotificationCategory.Cities, civ.civName, NotificationIcon.War) - } private fun addFortifyActions(actionList: ArrayList, unit: MapUnit, showingAdditionalActions: Boolean) { if (unit.isFortified() && !showingAdditionalActions) { @@ -607,14 +200,6 @@ object UnitActions { } } - fun canPillage(unit: MapUnit, tile: Tile): Boolean { - if (unit.isTransported) return false - if (!tile.canPillageTile()) return false - val tileOwner = tile.getOwner() - // Can't pillage friendly tiles, just like you can't attack them - it's an 'act of war' thing - return tileOwner == null || unit.civ.isAtWarWith(tileOwner) - } - private fun addGiftAction(unit: MapUnit, actionList: ArrayList, tile: Tile) { val getGiftAction = getGiftAction(unit, tile) if (getGiftAction != null) actionList += getGiftAction @@ -666,28 +251,21 @@ object UnitActions { return UnitAction(UnitActionType.GiftUnit, action = giftAction) } - private fun addTriggerUniqueActions(unit: MapUnit, actionList: ArrayList){ - for (unique in unit.getUniques()) { - // not a unit action - if (unique.conditionals.none { it.type?.targetTypes?.contains(UniqueTarget.UnitActionModifier) == true }) continue - // extends an existing unit action - if (unique.conditionals.any { it.type == UniqueType.UnitActionExtraLimitedTimes }) continue - if (!unique.isTriggerable) continue - if (usagesLeft(unit, unique)==0) continue + private fun addAutomateAction(unit: MapUnit, actionList: ArrayList, showingAdditionalActions:Boolean) { - val baseTitle = if (unique.isOfType(UniqueType.OneTimeEnterGoldenAgeTurns)) - unique.placeholderText.fillPlaceholders( - unit.civ.goldenAges.calculateGoldenAgeLength( - unique.params[0].toInt()).toString()) - else unique.text.removeConditionals() - val title = actionTextWithSideEffects(baseTitle, unique, unit) + // If either of these are true it goes in primary actions, else in additional actions + if ((unit.hasUnique(UniqueType.AutomationPrimaryAction) || unit.cache.hasUniqueToBuildImprovements) != showingAdditionalActions) + return - val unitAction = UnitAction(type = UnitActionType.TriggerUnique, title){ - UniqueTriggerActivation.triggerUnitwideUnique(unique, unit) - activateSideEffects(unit, unique) - } - actionList += unitAction - } + if (unit.isAutomated()) return + + actionList += UnitAction(UnitActionType.Automate, + isCurrentAction = unit.isAutomated(), + action = { + unit.action = UnitActionType.Automate.value + UnitAutomation.automateUnitMoves(unit) + }.takeIf { unit.currentMovement > 0 } + ) } private fun addWaitAction(unit: MapUnit, actionList: ArrayList) { @@ -710,77 +288,5 @@ object UnitActions { } ) } - - fun getMovementPointsToUse(actionUnique: Unique): Int { - val movementCost = actionUnique.conditionals - .filter { it.type == UniqueType.UnitActionMovementCost } - .minOfOrNull { it.params[0].toInt() } - if (movementCost != null) return movementCost - return 1 - } - - fun activateSideEffects(unit: MapUnit, actionUnique: Unique){ - val movementCost = getMovementPointsToUse(actionUnique) - unit.useMovementPoints(movementCost.toFloat()) - - for (conditional in actionUnique.conditionals){ - when (conditional.type){ - UniqueType.UnitActionConsumeUnit -> unit.consume() - UniqueType.UnitActionLimitedTimes, UniqueType.UnitActionOnce -> { - if (usagesLeft(unit, actionUnique) == 1 - && actionUnique.conditionals.any { it.type==UniqueType.UnitActionAfterWhichConsumed }) { - unit.consume() - continue - } - val usagesSoFar = unit.abilityToTimesUsed[actionUnique.placeholderText] ?: 0 - unit.abilityToTimesUsed[actionUnique.placeholderText] = usagesSoFar + 1 - } - else -> continue - } - } - } - - /** Returns 'null' if usages are not limited */ - fun usagesLeft(unit:MapUnit, actionUnique: Unique): Int?{ - val usagesTotal = getMaxUsages(unit, actionUnique) ?: return null - val usagesSoFar = unit.abilityToTimesUsed[actionUnique.placeholderText] ?: 0 - return usagesTotal - usagesSoFar - } - - fun getMaxUsages(unit: MapUnit, actionUnique: Unique): Int? { - val extraTimes = unit.getMatchingUniques(actionUnique.type!!) - .filter { it.text.removeConditionals() == actionUnique.text.removeConditionals() } - .flatMap { unique -> unique.conditionals.filter { it.type == UniqueType.UnitActionExtraLimitedTimes } } - .sumOf { it.params[0].toInt() } - - val times = actionUnique.conditionals - .filter { it.type == UniqueType.UnitActionLimitedTimes } - .maxOfOrNull { it.params[0].toInt() } - if (times != null) return times + extraTimes - if (actionUnique.conditionals.any { it.type == UniqueType.UnitActionOnce }) return 1 + extraTimes - - return null - } - - fun actionTextWithSideEffects(originalText: String, actionUnique: Unique, unit: MapUnit): String { - val sideEffectString = getSideEffectString(unit, actionUnique) - if (sideEffectString == "") return originalText - else return "{$originalText} $sideEffectString" - } - - fun getSideEffectString(unit:MapUnit, actionUnique: Unique): String { - val effects = ArrayList() - - val maxUsages = getMaxUsages(unit, actionUnique) - if (maxUsages!=null) effects += "${usagesLeft(unit, actionUnique)}/$maxUsages" - - if (actionUnique.conditionals.any { it.type == UniqueType.UnitActionConsumeUnit } - || actionUnique.conditionals.any { it.type == UniqueType.UnitActionAfterWhichConsumed } && usagesLeft(unit, actionUnique) == 1 - ) effects += Fonts.death.toString() - else effects += getMovementPointsToUse(actionUnique).toString() + Fonts.movement - - - return if (effects.isEmpty()) "" - else "(${effects.joinToString { it.tr() }})" - } } + 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 new file mode 100644 index 0000000000..6d164366e4 --- /dev/null +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt @@ -0,0 +1,414 @@ +package com.unciv.ui.screens.worldscreen.unit.actions + +import com.unciv.Constants +import com.unciv.GUI +import com.unciv.UncivGame +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.Tile +import com.unciv.models.Counter +import com.unciv.models.UncivSound +import com.unciv.models.UnitAction +import com.unciv.models.UnitActionType +import com.unciv.models.ruleset.unique.StateForConditionals +import com.unciv.models.ruleset.unique.UniqueTarget +import com.unciv.models.ruleset.unique.UniqueTriggerActivation +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.translations.fillPlaceholders +import com.unciv.models.translations.removeConditionals +import com.unciv.models.translations.tr +import com.unciv.ui.components.Fonts +import com.unciv.ui.popups.ConfirmPopup +import com.unciv.ui.screens.pickerscreens.ImprovementPickerScreen + +object UnitActionsFromUniques { + + fun addCreateWaterImprovements(unit: MapUnit, actionList: ArrayList) { + val waterImprovementAction = getWaterImprovementAction(unit) + if (waterImprovementAction != null) actionList += waterImprovementAction + } + + fun getWaterImprovementAction(unit: MapUnit): UnitAction? { + val tile = unit.currentTile + if (!tile.isWater || !unit.hasUnique(UniqueType.CreateWaterImprovements) || tile.resource == null) return null + + val improvementName = tile.tileResource.getImprovingImprovement(tile, unit.civ) ?: return null + val improvement = tile.ruleset.tileImprovements[improvementName] ?: return null + if (!tile.improvementFunctions.canBuildImprovement(improvement, unit.civ)) return null + + return UnitAction(UnitActionType.Create, "Create [$improvementName]", + action = { + tile.changeImprovement(improvementName, unit.civ) + unit.destroy() // Modders may wish for a nondestructive way, but that should be another Unique + }.takeIf { unit.currentMovement > 0 }) + } + + + fun addFoundCityAction(unit: MapUnit, actionList: ArrayList, tile: Tile) { + val getFoundCityAction = getFoundCityAction(unit, tile) + if (getFoundCityAction != null) actionList += getFoundCityAction + } + + /** Produce a [UnitAction] for founding a city. + * @param unit The unit to do the founding. + * @param tile The tile to found a city on. + * @return null if impossible (the unit lacks the ability to found), + * or else a [UnitAction] 'defining' the founding. + * The [action][UnitAction.action] field will be null if the action cannot be done here and now + * (no movement left, too close to another city). + */ + fun getFoundCityAction(unit: MapUnit, tile: Tile): UnitAction? { + val unique = unit.getMatchingUniques(UniqueType.FoundCity) + .filter { unique -> unique.conditionals.none { it.type == UniqueType.UnitActionExtraLimitedTimes } } + .firstOrNull() + if (unique == null || tile.isWater || tile.isImpassible()) return null + // Spain should still be able to build Conquistadors in a one city challenge - but can't settle them + if (unit.civ.isOneCityChallenger() && unit.civ.hasEverOwnedOriginalCapital) return null + if (UnitActionModifiers.usagesLeft(unit, unique) ==0) return null + + if (unit.currentMovement <= 0 || !tile.canBeSettled()) + return UnitAction(UnitActionType.FoundCity, action = null) + + val hasActionModifiers = unique.conditionals.any { it.type?.targetTypes?.contains( + UniqueTarget.UnitActionModifier + ) == true } + val foundAction = { + if (unit.civ.playerType != PlayerType.AI) + UncivGame.Current.settings.addCompletedTutorialTask("Found city") + unit.civ.addCity(tile.position) + if (tile.ruleset.tileImprovements.containsKey(Constants.cityCenter)) + tile.changeImprovement(Constants.cityCenter) + tile.removeRoad() + + if (hasActionModifiers) UnitActionModifiers.activateSideEffects(unit, unique) + else unit.destroy() + GUI.setUpdateWorldOnNextRender() // Set manually, since this could be triggered from the ConfirmPopup and not from the UnitActionsTable + } + + if (unit.civ.playerType == PlayerType.AI) + return UnitAction(UnitActionType.FoundCity, action = foundAction) + + return UnitAction( + type = UnitActionType.FoundCity, + title = + if (hasActionModifiers) UnitActionModifiers.actionTextWithSideEffects( + UnitActionType.FoundCity.value, + unique, + unit + ) + else UnitActionType.FoundCity.value, + uncivSound = UncivSound.Chimes, + action = { + // check if we would be breaking a promise + val leaders = testPromiseNotToSettle(unit.civ, tile) + if (leaders == null) + foundAction() + else { + // ask if we would be breaking a promise + val text = "Do you want to break your promise to [$leaders]?" + ConfirmPopup( + GUI.getWorldScreen(), + text, + "Break promise", + action = foundAction + ).open(force = true) + } + } + ) + } + + /** + * Checks whether a civ founding a city on a certain tile would break a promise. + * @param civInfo The civilization trying to found a city + * @param tile The tile where the new city would go + * @return null if no promises broken, else a String listing the leader(s) we would p* off. + */ + private fun testPromiseNotToSettle(civInfo: Civilization, tile: Tile): String? { + val brokenPromises = HashSet() + for (otherCiv in civInfo.getKnownCivs().filter { it.isMajorCiv() && !civInfo.isAtWarWith(it) }) { + val diplomacyManager = otherCiv.getDiplomacyManager(civInfo) + if (diplomacyManager.hasFlag(DiplomacyFlags.AgreedToNotSettleNearUs)) { + val citiesWithin6Tiles = otherCiv.cities + .filter { it.getCenterTile().aerialDistanceTo(tile) <= 6 } + .filter { otherCiv.hasExplored(it.getCenterTile()) } + if (citiesWithin6Tiles.isNotEmpty()) brokenPromises += otherCiv.getLeaderDisplayName() + } + } + return if(brokenPromises.isEmpty()) null else brokenPromises.joinToString(", ") + } + + fun addSetupAction(unit: MapUnit, actionList: ArrayList) { + if (!unit.hasUnique(UniqueType.MustSetUp) || unit.isEmbarked()) return + val isSetUp = unit.isSetUpForSiege() + actionList += UnitAction(UnitActionType.SetUp, + isCurrentAction = isSetUp, + action = { + unit.action = UnitActionType.SetUp.value + unit.useMovementPoints(1f) + }.takeIf { unit.currentMovement > 0 && !isSetUp }) + } + + fun addParadropAction(unit: MapUnit, actionList: ArrayList) { + val paradropUniques = + unit.getMatchingUniques(UniqueType.MayParadrop) + if (!paradropUniques.any() || unit.isEmbarked()) return + unit.cache.paradropRange = paradropUniques.maxOfOrNull { it.params[0] }!!.toInt() + actionList += UnitAction(UnitActionType.Paradrop, + isCurrentAction = unit.isPreparingParadrop(), + action = { + if (unit.isPreparingParadrop()) unit.action = null + else unit.action = UnitActionType.Paradrop.value + }.takeIf { + !unit.hasUnitMovedThisTurn() && + unit.currentTile.isFriendlyTerritory(unit.civ) && + !unit.isEmbarked() + }) + } + + fun addAirSweepAction(unit: MapUnit, actionList: ArrayList) { + val airsweepUniques = + unit.getMatchingUniques(UniqueType.CanAirsweep) + if (!airsweepUniques.any()) return + actionList += UnitAction(UnitActionType.AirSweep, + isCurrentAction = unit.isPreparingAirSweep(), + action = { + if (unit.isPreparingAirSweep()) unit.action = null + else unit.action = UnitActionType.AirSweep.value + }.takeIf { + unit.canAttack() + } + ) + } + fun addTriggerUniqueActions(unit: MapUnit, actionList: ArrayList){ + for (unique in unit.getUniques()) { + // not a unit action + if (unique.conditionals.none { it.type?.targetTypes?.contains(UniqueTarget.UnitActionModifier) == true }) continue + // extends an existing unit action + if (unique.conditionals.any { it.type == UniqueType.UnitActionExtraLimitedTimes }) continue + if (!unique.isTriggerable) continue + if (UnitActionModifiers.usagesLeft(unit, unique) ==0) continue + + val baseTitle = if (unique.isOfType(UniqueType.OneTimeEnterGoldenAgeTurns)) + unique.placeholderText.fillPlaceholders( + unit.civ.goldenAges.calculateGoldenAgeLength( + unique.params[0].toInt()).toString()) + else unique.text.removeConditionals() + val title = UnitActionModifiers.actionTextWithSideEffects(baseTitle, unique, unit) + + val unitAction = UnitAction(type = UnitActionType.TriggerUnique, title) { + UniqueTriggerActivation.triggerUnitwideUnique(unique, unit) + UnitActionModifiers.activateSideEffects(unit, unique) + } + actionList += unitAction + } + } + + fun getAddInCapitalAction(unit: MapUnit, tile: Tile): UnitAction { + return UnitAction(UnitActionType.AddInCapital, + title = "Add to [${ + unit.getMatchingUniques(UniqueType.AddInCapital).first().params[0] + }]", + action = { + unit.civ.victoryManager.currentsSpaceshipParts.add(unit.name, 1) + unit.destroy() + }.takeIf { + tile.isCityCenter() && tile.getCity()!! + .isCapital() && tile.getCity()!!.civ == unit.civ + } + ) + } + + fun addAddInCapitalAction(unit: MapUnit, actionList: ArrayList, tile: Tile) { + if (!unit.hasUnique(UniqueType.AddInCapital)) return + actionList += getAddInCapitalAction(unit, tile) + } + + fun getImprovementConstructionActions(unit: MapUnit, tile: Tile): ArrayList { + val finalActions = ArrayList() + val uniquesToCheck = unit.getMatchingUniques(UniqueType.ConstructImprovementInstantly) + val civResources = unit.civ.getCivResourcesByName() + + for (unique in uniquesToCheck) { + // Skip actions with a "[amount] extra times" conditional - these are treated in addTriggerUniqueActions instead + if (unique.conditionals.any { it.type == UniqueType.UnitActionExtraLimitedTimes }) continue + + val improvementName = unique.params[0] + val improvement = tile.ruleset.tileImprovements[improvementName] + ?: continue + if (UnitActionModifiers.usagesLeft(unit, unique) == 0) continue + + val resourcesAvailable = improvement.uniqueObjects.none { + improvementUnique -> + improvementUnique.isOfType(UniqueType.ConsumesResources) && + (civResources[improvementUnique.params[1]] ?: 0) < improvementUnique.params[0].toInt() + } + + finalActions += UnitAction(UnitActionType.Create, + title = UnitActionModifiers.actionTextWithSideEffects( + "Create [$improvementName]", + unique, + unit + ), + action = { + val unitTile = unit.getTile() + unitTile.changeImprovement(improvementName, unit.civ) + + // without this the world screen won't show the improvement because it isn't the 'last seen improvement' + unit.civ.cache.updateViewableTiles() + + UnitActionModifiers.activateSideEffects(unit, unique) + }.takeIf { + resourcesAvailable + && unit.currentMovement > 0f + && tile.improvementFunctions.canBuildImprovement(improvement, unit.civ) + // Next test is to prevent interfering with UniqueType.CreatesOneImprovement - + // not pretty, but users *can* remove the building from the city queue an thus clear this: + && !tile.isMarkedForCreatesOneImprovement() + && !tile.isImpassible() // Not 100% sure that this check is necessary... + }) + } + return finalActions + } + + fun addTransformActions( + unit: MapUnit, + actionList: ArrayList + ) { + val upgradeAction = getTransformActions(unit) + actionList += upgradeAction + } + + private fun getTransformActions( + unit: MapUnit + ): ArrayList { + val unitTile = unit.getTile() + val civInfo = unit.civ + val stateForConditionals = + StateForConditionals(unit = unit, civInfo = civInfo, tile = unitTile) + val transformList = ArrayList() + for (unique in unit.baseUnit().getMatchingUniques(UniqueType.CanTransform, stateForConditionals)) { + val unitToTransformTo = civInfo.getEquivalentUnit(unique.params[0]) + + if (unitToTransformTo.getMatchingUniques( + UniqueType.OnlyAvailableWhen, + StateForConditionals.IgnoreConditionals + ) + .any { !it.conditionalsApply(stateForConditionals) }) + continue + + // Check _new_ resource requirements + // Using Counter to aggregate is a bit exaggerated, but - respect the mad modder. + val resourceRequirementsDelta = Counter() + for ((resource, amount) in unit.baseUnit().getResourceRequirementsPerTurn()) + resourceRequirementsDelta.add(resource, -amount) + for ((resource, amount) in unitToTransformTo.getResourceRequirementsPerTurn()) + resourceRequirementsDelta.add(resource, amount) + val newResourceRequirementsString = resourceRequirementsDelta.entries + .filter { it.value > 0 } + .joinToString { "${it.value} {${it.key}}".tr() } + + val title = if (newResourceRequirementsString.isEmpty()) + "Transform to [${unitToTransformTo.name}]" + else "Transform to [${unitToTransformTo.name}]\n([$newResourceRequirementsString])" + + transformList.add(UnitAction(UnitActionType.Transform, + title = title, + action = { + unit.destroy() + val newUnit = + civInfo.units.placeUnitNearTile(unitTile.position, unitToTransformTo) + + /** We were UNABLE to place the new unit, which means that the unit failed to upgrade! + * The only known cause of this currently is "land units upgrading to water units" which fail to be placed. + */ + + /** We were UNABLE to place the new unit, which means that the unit failed to upgrade! + * The only known cause of this currently is "land units upgrading to water units" which fail to be placed. + */ + if (newUnit == null) { + val resurrectedUnit = + civInfo.units.placeUnitNearTile(unitTile.position, unit.baseUnit)!! + unit.copyStatisticsTo(resurrectedUnit) + } else { // Managed to upgrade + unit.copyStatisticsTo(newUnit) + newUnit.currentMovement = 0f + } + }.takeIf { + unit.currentMovement > 0 && !unit.isEmbarked() + } + )) + } + return transformList + } + + fun addBuildingImprovementsAction( + unit: MapUnit, + actionList: ArrayList, + tile: Tile + ) { + if (!unit.cache.hasUniqueToBuildImprovements) return + + val couldConstruct = unit.currentMovement > 0 + && !tile.isCityCenter() + && unit.civ.gameInfo.ruleset.tileImprovements.values.any { + ImprovementPickerScreen.canReport( + tile.improvementFunctions.getImprovementBuildingProblems( + it, + unit.civ + ).toSet() + ) + && unit.canBuildImprovement(it) + } + + actionList += UnitAction(UnitActionType.ConstructImprovement, + isCurrentAction = unit.currentTile.hasImprovementInProgress(), + action = { + GUI.pushScreen(ImprovementPickerScreen(tile, unit) { + if (GUI.getSettings().autoUnitCycle) + GUI.getWorldScreen().switchToNextUnit() + }) + }.takeIf { couldConstruct } + ) + } + + private fun getRepairTurns(unit: MapUnit): Int { + val tile = unit.currentTile + if (!tile.isPillaged()) return 0 + if (tile.improvementInProgress == Constants.repair) return tile.turnsToImprovement + var repairTurns = tile.ruleset.tileImprovements[Constants.repair]!!.getTurnsToBuild(unit.civ, unit) + + val pillagedImprovement = tile.getImprovementToRepair()!! + val turnsToBuild = pillagedImprovement.getTurnsToBuild(unit.civ, unit) + // cap repair to number of turns to build original improvement + if (turnsToBuild < repairTurns) repairTurns = turnsToBuild + return repairTurns + } + + fun addRepairAction(unit: MapUnit, actionList: ArrayList){ + val repairAction = getRepairAction(unit) + if (repairAction != null) actionList.add(repairAction) + } + fun getRepairAction(unit: MapUnit) : UnitAction? { + if (!unit.currentTile.ruleset.tileImprovements.containsKey(Constants.repair)) return null + if (!unit.cache.hasUniqueToBuildImprovements) return null + if (unit.isEmbarked()) return null + val tile = unit.getTile() + if (tile.isCityCenter()) return null + if (!tile.isPillaged()) return null + + val couldConstruct = unit.currentMovement > 0 + && !tile.isCityCenter() && tile.improvementInProgress != Constants.repair + + val turnsToBuild = getRepairTurns(unit) + + return UnitAction(UnitActionType.Repair, + title = "${UnitActionType.Repair} [${unit.currentTile.getImprovementToRepair()!!.name}] - [${turnsToBuild}${Fonts.turn}]", + action = { + tile.turnsToImprovement = getRepairTurns(unit) + tile.improvementInProgress = Constants.repair + }.takeIf { couldConstruct } + ) + } +} diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsPillage.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsPillage.kt index 37621cfbd2..07e33f49b9 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsPillage.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsPillage.kt @@ -67,7 +67,7 @@ object UnitActionsPillage { if (pillagingImprovement) // only Improvements heal HP unit.healBy(25) - }.takeIf { unit.currentMovement > 0 && UnitActions.canPillage(unit, tile) } + }.takeIf { unit.currentMovement > 0 && canPillage(unit, tile) } ) } @@ -114,4 +114,12 @@ object UnitActionsPillage { toCityPillageYield.notify(" which has been sent to [${closestCity?.name}]") globalPillageYield.notify("") } + + fun canPillage(unit: MapUnit, tile: Tile): Boolean { + if (unit.isTransported) return false + if (!tile.canPillageTile()) return false + val tileOwner = tile.getOwner() + // Can't pillage friendly tiles, just like you can't attack them - it's an 'act of war' thing + return tileOwner == null || unit.civ.isAtWarWith(tileOwner) + } } diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsReligion.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsReligion.kt index 572db85c34..8623eb305c 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsReligion.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsReligion.kt @@ -106,12 +106,12 @@ object UnitActionsReligion { action = { city.religion.removeAllPressuresExceptFor(unit.religion!!) if (city.religion.religionThisIsTheHolyCityOf != null) { - val religion = unit.civ.gameInfo.religions[city.religion.religionThisIsTheHolyCityOf]!! + val holyCityReligion = unit.civ.gameInfo.religions[city.religion.religionThisIsTheHolyCityOf]!! if (city.religion.religionThisIsTheHolyCityOf != unit.religion && !city.religion.isBlockedHolyCity) { - religion.getFounder().addNotification("An [${unit.baseUnit.name}] has removed your religion [${religion.getReligionDisplayName()}] from its Holy City [${city.name}]!", NotificationCategory.Religion) + holyCityReligion.getFounder().addNotification("An [${unit.baseUnit.name}] has removed your religion [${holyCityReligion.getReligionDisplayName()}] from its Holy City [${city.name}]!", NotificationCategory.Religion) city.religion.isBlockedHolyCity = true } else if (city.religion.religionThisIsTheHolyCityOf == unit.religion && city.religion.isBlockedHolyCity) { - religion.getFounder().addNotification("An [${unit.baseUnit.name}] has restored [${city.name}] as the Holy City of your religion [${religion.getReligionDisplayName()}]!", NotificationCategory.Religion) + holyCityReligion.getFounder().addNotification("An [${unit.baseUnit.name}] has restored [${city.name}] as the Holy City of your religion [${holyCityReligion.getReligionDisplayName()}]!", NotificationCategory.Religion) city.religion.isBlockedHolyCity = false } } diff --git a/tests/src/com/unciv/uniques/UnitUniquesTests.kt b/tests/src/com/unciv/uniques/UnitUniquesTests.kt index e8b08ad7cf..49828f2d16 100644 --- a/tests/src/com/unciv/uniques/UnitUniquesTests.kt +++ b/tests/src/com/unciv/uniques/UnitUniquesTests.kt @@ -8,7 +8,7 @@ import com.unciv.testing.GdxTestRunner import com.unciv.testing.TestGame import com.unciv.ui.screens.pickerscreens.PromotionTree import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions -import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions.getImprovementConstructionActions +import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques import org.junit.Assert import org.junit.Before import org.junit.Test @@ -66,7 +66,7 @@ class UnitUniquesTests { val unit = game.addUnit("Great Engineer", civ, unitTile) unit.currentMovement = unit.baseUnit.movement.toFloat() // Required! val actionsWithoutIron = try { - getImprovementConstructionActions(unit, unitTile) + UnitActionsFromUniques.getImprovementConstructionActions(unit, unitTile) } catch (ex: Throwable) { // Give that IndexOutOfBoundsException a nicer name Assert.fail("getImprovementConstructionActions throws Exception ${ex.javaClass.simpleName}") @@ -88,7 +88,7 @@ class UnitUniquesTests { Assert.assertTrue("Test preparation failed to add Iron to Civ resources", ironAvailable >= 3) // See if that same Engineer could create a Manufactory NOW - val actionsWithIron = getImprovementConstructionActions(unit, unitTile) + val actionsWithIron = UnitActionsFromUniques.getImprovementConstructionActions(unit, unitTile) .filter { it.action != null } Assert.assertFalse("Great Engineer SHOULD be able to create a Manufactory modded to require Iron once Iron is available", actionsWithIron.isEmpty()) @@ -152,7 +152,7 @@ class UnitUniquesTests { // add unit val centerTile = game.tileMap[0,0] val unit = game.addUnit("Scout", civ, centerTile) - var tree = PromotionTree(unit) + val tree = PromotionTree(unit) Assert.assertFalse("We shouldn't be able to get the promotion without XP", tree.canBuyUpTo(promotionTestBranchB))