diff --git a/core/src/com/unciv/logic/city/City.kt b/core/src/com/unciv/logic/city/City.kt index 07dbb04677..ffb58fec11 100644 --- a/core/src/com/unciv/logic/city/City.kt +++ b/core/src/com/unciv/logic/city/City.kt @@ -212,11 +212,20 @@ class City : IsPartOfGameInfoSerialization { fun getStatReserve(stat: Stat): Int { return when (stat) { + Stat.Production -> cityConstructions.getWorkDone(cityConstructions.getCurrentConstruction().name) Stat.Food -> population.foodStored else -> civ.getStatReserve(stat) } } + fun hasStatToBuy(stat: Stat, price: Int): Boolean { + return when { + civ.gameInfo.gameParameters.godMode -> true + price == 0 -> true + else -> getStatReserve(stat) >= price + } + } + internal fun getMaxHealth() = 200 + cityConstructions.getBuiltBuildings().sumOf { it.cityHealth } diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index edf8cdb2ae..18ff3f939b 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -7,6 +7,7 @@ import com.unciv.logic.MultiFilter import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.battle.BattleUnitCapture import com.unciv.logic.battle.MapUnitCombatant +import com.unciv.logic.city.City import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationIcon @@ -204,6 +205,10 @@ class MapUnit : IsPartOfGameInfoSerialization { fun getTile(): Tile = currentTile + fun getClosestCity(): City? = civ.cities.minByOrNull { + it.getCenterTile().aerialDistanceTo(currentTile) + } + fun isMilitary() = baseUnit.isMilitary() fun isCivilian() = baseUnit.isCivilian() diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 10ab6ff481..b95d216163 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -359,7 +359,8 @@ enum class UniqueType( CanHurryResearch("Can hurry technology research", UniqueTarget.Unit), CanHurryPolicy("Can generate a large amount of culture", UniqueTarget.Unit), CanTradeWithCityStateForGoldAndInfluence("Can undertake a trade mission with City-State, giving a large sum of gold and [amount] Influence", UniqueTarget.Unit), - CanTransform("Can transform to [unit]", UniqueTarget.Unit), + CanTransform("Can transform to [unit]", UniqueTarget.UnitAction, + docDescription = "By default consumes all movement"), AutomationPrimaryAction("Automation is a primary action", UniqueTarget.Unit, flags = UniqueFlag.setOfHiddenToUsers), @@ -504,7 +505,14 @@ enum class UniqueType( ///////////////////////////////////////// region 05 UNIT ACTION MODIFIERS ///////////////////////////////////////// UnitActionConsumeUnit("by consuming this unit", UniqueTarget.UnitActionModifier), - UnitActionMovementCost("for [amount] movement", UniqueTarget.UnitActionModifier), + UnitActionMovementCost("for [amount] movement", UniqueTarget.UnitActionModifier, + docDescription = "Will consume up to [amount] of Movement to execute"), + UnitActionMovementCostAll("for all movement", UniqueTarget.UnitActionModifier, + docDescription = "Will consume all Movement to execute"), + UnitActionMovementCostRequired("requires [amount] movement", UniqueTarget.UnitActionModifier, + docDescription = "Requires [amount] of Movement to execute. Unit's Movement is rounded up"), + UnitActionStatsCost("costs [stats] stats", UniqueTarget.UnitActionModifier, + docDescription = "A positive Integer value will be subtracted from your stock. Food and Production will be removed from Closest City's current stock"), UnitActionOnce("once", UniqueTarget.UnitActionModifier), UnitActionLimitedTimes("[amount] times", UniqueTarget.UnitActionModifier), UnitActionExtraLimitedTimes("[amount] additional time(s)", UniqueTarget.UnitActionModifier), diff --git a/core/src/com/unciv/models/stats/Stats.kt b/core/src/com/unciv/models/stats/Stats.kt index ec8d3e672c..19ce921ac8 100644 --- a/core/src/com/unciv/models/stats/Stats.kt +++ b/core/src/com/unciv/models/stats/Stats.kt @@ -185,6 +185,13 @@ open class Stats( } } + /** Return a string of just +/- value and Stat symbol*/ + fun toStringOnlyIcons(): String { + return this.joinToString { + (if (it.value > 0) "+" else "") + it.value.toInt() + " " + it.key.character + } + } + /** Represents one [key][Stat]/[value][Float] pair returned by the [iterator] */ data class StatValuePair (val key: Stat, val value: Float) 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 index b5489b5c5c..a0f60744e0 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionModifiers.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionModifiers.kt @@ -3,9 +3,12 @@ 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.stats.Stat +import com.unciv.models.stats.Stats import com.unciv.models.translations.removeConditionals import com.unciv.models.translations.tr import com.unciv.ui.components.fonts.Fonts +import kotlin.math.ceil object UnitActionModifiers { fun canUse(unit: MapUnit, actionUnique: Unique): Boolean { @@ -18,16 +21,59 @@ object UnitActionModifiers { .filter { unique -> unique.conditionals.none { it.type == UniqueType.UnitActionExtraLimitedTimes } } .filter { canUse(unit, it) } - private fun getMovementPointsToUse(actionUnique: Unique): Int { + private fun getMovementPointsToUse(unit: MapUnit, actionUnique: Unique, defaultAllMovement: Boolean = false): Int { + if (actionUnique.conditionals.any { it.type == UniqueType.UnitActionMovementCostAll }) + return unit.getMaxMovement() val movementCost = actionUnique.conditionals - .filter { it.type == UniqueType.UnitActionMovementCost } - .minOfOrNull { it.params[0].toInt() } - if (movementCost != null) return movementCost - return 1 + .filter { it.type == UniqueType.UnitActionMovementCost || it.type == UniqueType.UnitActionMovementCostRequired } + .maxOfOrNull { it.params[0].toInt() } + + if (movementCost != null) + return movementCost + return if (defaultAllMovement) unit.getMaxMovement() else 1 } - fun activateSideEffects(unit: MapUnit, actionUnique: Unique) { - val movementCost = getMovementPointsToUse(actionUnique) + private fun getMovementPointsRequired(actionUnique: Unique): Int { + if (actionUnique.conditionals.any { it.type == UniqueType.UnitActionMovementCostAll }) + return 1 + val movementCostRequired = actionUnique.conditionals + .filter { it.type == UniqueType.UnitActionMovementCostRequired } + .minOfOrNull { it.params[0].toInt() } + return movementCostRequired ?: 1 + } + + /**Check if the stat costs in this Action Modifier can be spent by the Civ/Closest City without + * going into the negatives + * @return Boolean + */ + private fun canSpendStatsCost(unit: MapUnit, actionUnique: Unique): Boolean { + for (conditional in actionUnique.conditionals.filter { it.type == UniqueType.UnitActionStatsCost }) { + for ((stat, value) in conditional.stats) { + if (unit.getClosestCity() != null) { + if (!unit.getClosestCity()!!.hasStatToBuy(stat, value.toInt())) { + return false + } + } else if (stat in Stat.statsWithCivWideField) { + if (!unit.civ.hasStatToBuy(stat, value.toInt())) + return false + } else return false // no city to spend the Stat + } + } + + return true + } + + /**Checks if this Action Unique can be executed, based on action modifiers + * @return Boolean + */ + fun canActivateSideEffects(unit: MapUnit, actionUnique: Unique): Boolean { + return canUse(unit, actionUnique) + && getMovementPointsRequired(actionUnique) <= ceil(unit.currentMovement).toInt() + && canSpendStatsCost(unit, actionUnique) + } + + fun activateSideEffects(unit: MapUnit, actionUnique: Unique, defaultAllMovement: Boolean = false) { + val movementCost = getMovementPointsToUse(unit, actionUnique, defaultAllMovement) unit.useMovementPoints(movementCost.toFloat()) for (conditional in actionUnique.conditionals) { @@ -42,6 +88,15 @@ object UnitActionModifiers { val usagesSoFar = unit.abilityToTimesUsed[actionUnique.placeholderText] ?: 0 unit.abilityToTimesUsed[actionUnique.placeholderText] = usagesSoFar + 1 } + UniqueType.UnitActionStatsCost -> { + // do Stat costs, either Civ-wide or local city + // should have validated this doesn't send us negative + for ((stat, value) in conditional.stats) { + if (stat in Stat.statsWithCivWideField) { + unit.civ.addStat(stat, -value.toInt()) + } else unit.getClosestCity()?.addStat(stat, -value.toInt()) + } + } else -> continue } } @@ -75,17 +130,23 @@ object UnitActionModifiers { else return "{$originalText} $sideEffectString" } - private fun getSideEffectString(unit: MapUnit, actionUnique: Unique): String { + fun getSideEffectString(unit: MapUnit, actionUnique: Unique, defaultAllMovement: Boolean = false): String { val effects = ArrayList() val maxUsages = getMaxUsages(unit, actionUnique) if (maxUsages!=null) effects += "${usagesLeft(unit, actionUnique)}/$maxUsages" + if (actionUnique.conditionals.any { it.type == UniqueType.UnitActionStatsCost}) { + val statCost = Stats() + for (conditional in actionUnique.conditionals.filter { it.type == UniqueType.UnitActionStatsCost }) + statCost.add(conditional.stats) + effects += (statCost * -1).toStringOnlyIcons() + } + 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 - + else effects += getMovementPointsToUse(unit, actionUnique, defaultAllMovement).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 index 32177173dc..4c74a526b3 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt @@ -18,6 +18,7 @@ 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.stats.Stat +import com.unciv.models.stats.Stats import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.removeConditionals import com.unciv.models.translations.tr @@ -93,7 +94,7 @@ object UnitActionsFromUniques { action = foundAction ).open(force = true) } - } + }.takeIf { UnitActionModifiers.canActivateSideEffects(unit, unique) } ) } @@ -213,7 +214,12 @@ object UnitActionsFromUniques { } }() - yield(UnitAction(UnitActionType.TriggerUnique, 80f, title, action = unitAction)) + yield( + UnitAction(UnitActionType.TriggerUnique, 80f, title, + action = unitAction.takeIf { + UnitActionModifiers.canActivateSideEffects(unit, unique) + }) + ) } } @@ -295,6 +301,7 @@ object UnitActionsFromUniques { // 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... + && UnitActionModifiers.canActivateSideEffects(unit, unique) } )) } @@ -354,21 +361,19 @@ object UnitActionsFromUniques { .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])" + var title = "Transform to [${unitToTransformTo.name}] " + title += UnitActionModifiers.getSideEffectString(unit, unique, true) + if (newResourceRequirementsString.isNotEmpty()) + title += "\n([$newResourceRequirementsString])" yield(UnitAction(UnitActionType.Transform, 70f, title = title, action = { + val oldMovement = unit.currentMovement 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. */ @@ -378,10 +383,18 @@ object UnitActionsFromUniques { unit.copyStatisticsTo(resurrectedUnit) } else { // Managed to upgrade unit.copyStatisticsTo(newUnit) - newUnit.currentMovement = 0f + // have to handle movement manually because we killed the old unit + // a .destroy() unit has 0 movement + // and a new one may have less Max Movement + newUnit.currentMovement = oldMovement + // adjust if newUnit has lower Max Movement + if (newUnit.currentMovement.toInt() > newUnit.getMaxMovement()) + newUnit.currentMovement = newUnit.getMaxMovement().toFloat() + // execute any side effects, Stat and Movement adjustments + UnitActionModifiers.activateSideEffects(newUnit, unique, true) } }.takeIf { - unit.currentMovement > 0 && !unit.isEmbarked() + !unit.isEmbarked() && UnitActionModifiers.canActivateSideEffects(unit, unique) } )) } 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 a3cd1dda1c..68d4103729 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 @@ -36,7 +36,8 @@ object UnitActionsReligion { if (hasActionModifiers) UnitActionModifiers.activateSideEffects(unit, unique) else unit.consume() - }.takeIf { unit.civ.religionManager.mayFoundReligionHere(tile) } + }.takeIf { unit.civ.religionManager.mayFoundReligionHere(tile) + && UnitActionModifiers.canActivateSideEffects(unit, unique)} )) } @@ -63,7 +64,8 @@ object UnitActionsReligion { unit.civ.religionManager.useProphetForEnhancingReligion(unit) if (hasActionModifiers) UnitActionModifiers.activateSideEffects(unit, unique) else unit.consume() - }.takeIf { unit.civ.religionManager.mayEnhanceReligionHere(tile) } + }.takeIf { unit.civ.religionManager.mayEnhanceReligionHere(tile) + && UnitActionModifiers.canActivateSideEffects(unit, unique)} )) } @@ -99,7 +101,8 @@ object UnitActionsReligion { city.religion.removeAllPressuresExceptFor(unit.religion!!) UnitActionModifiers.activateSideEffects(unit, newStyleUnique) - }.takeIf { unit.currentMovement > 0 && unit.civ.religionManager.maySpreadReligionNow(unit) } + }.takeIf { unit.civ.religionManager.maySpreadReligionNow(unit) + && UnitActionModifiers.canActivateSideEffects(unit, newStyleUnique)} )) } @@ -138,7 +141,7 @@ object UnitActionsReligion { } } UnitActionModifiers.activateSideEffects(unit, newStyleUnique) - }.takeIf { unit.currentMovement > 0f } + }.takeIf { UnitActionModifiers.canActivateSideEffects(unit, newStyleUnique)} )) } } diff --git a/docs/Modders/Unique-parameters.md b/docs/Modders/Unique-parameters.md index 21a06dfe3f..576d40bc5b 100644 --- a/docs/Modders/Unique-parameters.md +++ b/docs/Modders/Unique-parameters.md @@ -182,11 +182,24 @@ Allowed values are: This indicates a text comprised of specific stats and is slightly more complex. -Each stats is comprised of several stat changes, each in the form of `+{amount} {stat}`, where 'stat' is one of the seven major stats mentioned above. +Each stats is comprised of several stat changes, each in the form of `+{amount} {stat}`, +where 'stat' is one of the seven major stats +(eg `Production`, `Food`, `Gold`, `Science`, `Culture`, `Happiness` and `Faith`). For example: `+1 Science`. These can be strung together with ", " between them, for example: `+2 Production, +3 Food`. +## stockpiledResource + +This indicates a text that corresponds to a custom Stockpile Resource. + +These are global civilization resources that act similar to the main Civ-wide resources like `Gold` and `Faith`. +You can generate them and consume them. And actions that would consume them are blocked if you +don't have enough left in stock. + +To use, you need to first define a TileResources with the "Stockpiled" Unique. Then you can reference +them in other Uniques. + ## technologyFilter At the moment only implemented for [ModOptions.techsToRemove](Mod-file-structure/5-Miscellaneous-JSON-files.md#modoptionsjson).