reorg: Separated UnitActions into 3 files:

- UnitActionsFromUniques
- UnitActionModifiers
- UnitActions retains actions relevant to all units
This commit is contained in:
Yair Morgenstern 2023-09-28 15:30:39 +03:00
parent 6ffe13e780
commit d1b2d652e3
10 changed files with 603 additions and 545 deletions

View File

@ -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<Tile>) {
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()
}
}

View File

@ -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()!!)))}

View File

@ -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 -

View File

@ -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<Civilization>()
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 */

View File

@ -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<String>()
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() }})"
}
}

View File

@ -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<UnitAction>()
// 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<UnitAction>) {
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<UnitAction>, 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<String>()
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<UnitAction>) {
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<UnitAction>) {
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<UnitAction>) {
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<UnitAction>) {
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<UnitAction>) {
if (unit.baseUnit.movesLikeAirUnits()) return
if (unit.isExploring()) return
@ -314,252 +153,6 @@ object UnitActions {
}
}
private fun addTransformActions(
unit: MapUnit,
actionList: ArrayList<UnitAction>
) {
val upgradeAction = getTransformActions(unit)
actionList += upgradeAction
}
/** */
private fun getTransformActions(
unit: MapUnit
): ArrayList<UnitAction> {
val unitTile = unit.getTile()
val civInfo = unit.civ
val stateForConditionals = StateForConditionals(unit = unit, civInfo = civInfo, tile = unitTile)
val transformList = ArrayList<UnitAction>()
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<String>()
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<UnitAction>,
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<UnitAction>) {
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<UnitAction>, 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<UnitAction>, tile: Tile) {
if (!unit.hasUnique(UniqueType.AddInCapital)) return
actionList += getAddInCapitalAction(unit, tile)
}
fun getImprovementConstructionActions(unit: MapUnit, tile: Tile): ArrayList<UnitAction> {
val finalActions = ArrayList<UnitAction>()
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<Civilization>()
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<UnitAction>, 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<UnitAction>, 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<UnitAction>){
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<UnitAction>, 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<UnitAction>) {
@ -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<String>()
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() }})"
}
}

View File

@ -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<UnitAction>) {
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<UnitAction>, 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<String>()
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<UnitAction>) {
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<UnitAction>) {
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<UnitAction>) {
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<UnitAction>){
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<UnitAction>, tile: Tile) {
if (!unit.hasUnique(UniqueType.AddInCapital)) return
actionList += getAddInCapitalAction(unit, tile)
}
fun getImprovementConstructionActions(unit: MapUnit, tile: Tile): ArrayList<UnitAction> {
val finalActions = ArrayList<UnitAction>()
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<UnitAction>
) {
val upgradeAction = getTransformActions(unit)
actionList += upgradeAction
}
private fun getTransformActions(
unit: MapUnit
): ArrayList<UnitAction> {
val unitTile = unit.getTile()
val civInfo = unit.civ
val stateForConditionals =
StateForConditionals(unit = unit, civInfo = civInfo, tile = unitTile)
val transformList = ArrayList<UnitAction>()
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<String>()
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<UnitAction>,
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<UnitAction>){
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 }
)
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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))