From 0f63000ac817152a5e9e452a2859a6a923004ee1 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Wed, 25 May 2022 18:35:27 +0200 Subject: [PATCH] Show required resource for upgrades, rework upgrade logic (#6849) * Show required resource for upgrades, rework upgrade logic * Show required resource for upgrades - reviews --- .../jsons/translations/template.properties | 1 + .../src/com/unciv/logic/city/IConstruction.kt | 19 ++- core/src/com/unciv/logic/map/MapUnit.kt | 146 ++++++++++++++---- core/src/com/unciv/models/ModConstants.kt | 11 ++ core/src/com/unciv/models/UnitAction.kt | 20 +-- .../ruleset/unique/UniqueTriggerActivation.kt | 4 +- .../com/unciv/models/ruleset/unit/BaseUnit.kt | 2 +- .../unciv/ui/worldscreen/unit/UnitActions.kt | 134 ++++++++-------- docs/Other/Miscellaneous-JSON-files.md | 25 ++- 9 files changed, 240 insertions(+), 122 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index e5ec176376..d250285daa 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -843,6 +843,7 @@ Paradrop = Add in capital = Add to [comment] = Upgrade to [unitType] ([goldCost] gold) = +Upgrade to [unitType]\n([goldCost] gold, [resources]) = Found city = Promote = Health = diff --git a/core/src/com/unciv/logic/city/IConstruction.kt b/core/src/com/unciv/logic/city/IConstruction.kt index 66efc8cefe..ad178f14e1 100644 --- a/core/src/com/unciv/logic/city/IConstruction.kt +++ b/core/src/com/unciv/logic/city/IConstruction.kt @@ -84,13 +84,24 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques { class RejectionReasons: HashSet() { - + fun add(rejectionReason: RejectionReason) = add(RejectionReasonInstance(rejectionReason)) - + fun contains(rejectionReason: RejectionReason) = any { it.rejectionReason == rejectionReason } - fun filterTechPolicyEraWonderRequirements(): List { - return filterNot { it.rejectionReason in techPolicyEraWonderRequirements } + fun isOKIgnoringRequirements( + ignoreTechPolicyEraWonderRequirements: Boolean = false, + ignoreResources: Boolean = false + ): Boolean { + if (!ignoreTechPolicyEraWonderRequirements && !ignoreResources) return isEmpty() + if (!ignoreTechPolicyEraWonderRequirements) + return all { it.rejectionReason == RejectionReason.ConsumesResources } + if (!ignoreResources) + return all { it.rejectionReason in techPolicyEraWonderRequirements } + return all { + it.rejectionReason == RejectionReason.ConsumesResources || + it.rejectionReason in techPolicyEraWonderRequirements + } } fun hasAReasonToBeRemovedFromQueue(): Boolean { diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 29685df6e8..c756fefdfe 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -501,49 +501,127 @@ class MapUnit { return false } - fun getUnitToUpgradeTo(): BaseUnit { + /** + * Follow the upgrade chain, stopping when there is no [BaseUnit.upgradesTo] or a tech is not researched. + * @param [actionAllowStep] Will be called for each upgrade allowed by tech and has a double purpose: + * Side effects, e.g. for aggregation, are allowed, and + * returning `false` will abort the upgrade chain and not include the step in the final count. + * @return Number of allowed upgrade steps + */ + private fun followUpgradePath( + maxSteps: Int = Int.MAX_VALUE, + actionAllowStep: (oldUnit: BaseUnit, newUnit: BaseUnit)->Boolean + ): Int { var unit = baseUnit() + var steps = 0 // Go up the upgrade tree until you find the last one which is buildable - while (unit.upgradesTo != null && unit.getDirectUpgradeUnit(civInfo).requiredTech - .let { it == null || civInfo.tech.isResearched(it) } - ) - unit = unit.getDirectUpgradeUnit(civInfo) + while(steps < maxSteps) { + if (unit.upgradesTo == null) break + val newUnit = unit.getDirectUpgradeUnit(civInfo) + val techName = newUnit.requiredTech + if (techName != null && !civInfo.tech.isResearched(techName)) break + if (!actionAllowStep(unit, newUnit)) break + unit = newUnit + steps++ + } + return steps + } + + /** Get the base unit this map unit could upgrade to, respecting researched tech and nation uniques only. + * Note that if the unit can't upgrade, the current BaseUnit is returned. + * @param maxSteps follow the upgrade chain only this far. Useful values are default (directly upgrade to what tech ultimately allows) or 1 (Civ5 behaviour) + */ + // Used from UnitAutomation, UI action, canUpgrade + fun getUnitToUpgradeTo(maxSteps: Int = Int.MAX_VALUE): BaseUnit { + var unit = baseUnit() + followUpgradePath(maxSteps) { _, newUnit -> + unit = newUnit + true + } return unit } - /** @param ignoreRequired: Ignore possible tech/policy/building requirements. - * Used for upgrading units via ancient ruins. - */ - fun canUpgrade(unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(), ignoreRequired: Boolean = false): Boolean { - if (name == unitToUpgradeTo.name) return false - val rejectionReasons = unitToUpgradeTo.getRejectionReasons(civInfo) - if (rejectionReasons.isEmpty()) return true - if (ignoreRequired && rejectionReasons.filterTechPolicyEraWonderRequirements().isEmpty()) return true - - if (rejectionReasons.contains(RejectionReason.ConsumesResources)) { - // We need to remove the unit from the civ for this check, - // because if the unit requires, say, horses, and so does its upgrade, - // and the civ currently has 0 horses, we need to see if the upgrade will be buildable - // WHEN THE CURRENT UNIT IS NOT HERE - civInfo.removeUnit(this) - val canUpgrade = - if (ignoreRequired) unitToUpgradeTo.isBuildableIgnoringTechs(civInfo) - else unitToUpgradeTo.isBuildable(civInfo) - civInfo.addUnit(this) - return canUpgrade - } - return false + /** Check if the default upgrade would do more than one step + * - to avoid showing both the single step and normal upgrades in UnitActions */ + fun canUpgradeMultipleSteps(): Boolean { + return 1 < followUpgradePath(2) { _, _ -> true } } - fun getCostOfUpgrade(): Int { - val unitToUpgradeTo = getUnitToUpgradeTo() - var goldCostOfUpgrade = (unitToUpgradeTo.cost - baseUnit().cost) * 2f + 10f - for (unique in civInfo.getMatchingUniques(UniqueType.UnitUpgradeCost, StateForConditionals(civInfo, unit=this))) - goldCostOfUpgrade *= unique.params[0].toPercent() + /** Check whether this unit can upgrade to [unitToUpgradeTo]. This does not check or follow the + * normal upgrade chain defined by [BaseUnit.upgradesTo], unless [unitToUpgradeTo] is left at default. + * @param maxSteps only used for default of [unitToUpgradeTo], ignored otherwise. + * @param ignoreRequirements Ignore possible tech/policy/building requirements (e.g. resource requirements still count). + * Used for upgrading units via ancient ruins. + * @param ignoreResources Ignore resource requirements (tech still counts) + * Used to display disabled Upgrade button + */ + fun canUpgrade( + maxSteps: Int = Int.MAX_VALUE, + unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(maxSteps), + ignoreRequirements: Boolean = false, + ignoreResources: Boolean = false + ): Boolean { + if (name == unitToUpgradeTo.name) return false + val rejectionReasons = unitToUpgradeTo.getRejectionReasons(civInfo) + if (rejectionReasons.isOKIgnoringRequirements(ignoreRequirements, ignoreResources)) return true - if (goldCostOfUpgrade < 0) return 0 // For instance, Landsknecht costs less than Spearman, so upgrading would cost negative gold - return goldCostOfUpgrade.toInt() + // The resource requirements check above did not consider that the resources + // this unit currently "consumes" are available for an upgrade too - if that's one of the + // reasons, repeat the check with those resources in the pool. + if (!rejectionReasons.contains(RejectionReason.ConsumesResources)) + return false + + //TODO redesign without kludge: Inform getRejectionReasons about 'virtually available' resources somehow + + // We need to remove the unit from the civ for this check, + // because if the unit requires, say, horses, and so does its upgrade, + // and the civ currently has 0 horses, we need to see if the upgrade will be buildable + // WHEN THE CURRENT UNIT IS NOT HERE + civInfo.removeUnit(this) + val canUpgrade = unitToUpgradeTo.getRejectionReasons(civInfo) + .isOKIgnoringRequirements(ignoreTechPolicyEraWonderRequirements = ignoreRequirements) + civInfo.addUnit(this) + return canUpgrade + } + + /** Determine gold cost of a Unit Upgrade, potentially over several steps. + * @param unitToUpgradeTo the final BaseUnit. Must be reachable via normal upgrades or else + * the function will return the cost to upgrade to the last possible and researched normal upgrade. + * @return Gold cost in increments of 5, never negative. Will return 0 for invalid inputs (unit can't upgrade or is is already a [unitToUpgradeTo]) + * @see CvUnit::upgradePrice + */ + // Only one use from getUpgradeAction at the moment, so AI-specific rules omitted + //todo Does the AI never buy upgrades??? + fun getCostOfUpgrade(unitToUpgradeTo: BaseUnit): Int { + // Source rounds to int every step, we don't + //TODO From the source, this should apply _Production_ modifiers (Temple of Artemis? GameSpeed! StartEra!), at the moment it doesn't + + var goldCostOfUpgrade = 0 + + val ruleset = civInfo.gameInfo.ruleSet + val constants = ruleset.modOptions.constants.unitUpgradeCost + // apply modifiers: Wonders (Pentagon), Policies (Professional Army). Cached outside loop despite + // the UniqueType being allowed on a BaseUnit - we don't have a MapUnit in the loop. + // Actually instantiating every intermediate to support such mods: todo + var civModifier = 1f + val stateForConditionals = StateForConditionals(civInfo, unit = this) + for (unique in civInfo.getMatchingUniques(UniqueType.UnitUpgradeCost, stateForConditionals)) + civModifier *= unique.params[0].toPercent() + + followUpgradePath(actionAllowStep = fun(oldUnit: BaseUnit, newUnit: BaseUnit): Boolean { + // do clamping and rounding here so upgrading stepwise costs the same as upgrading far down the chain + var stepCost = constants.base + stepCost += (constants.perProduction * (newUnit.cost - oldUnit.cost)).coerceAtLeast(0f) + val era = ruleset.eras[ruleset.technologies[newUnit.requiredTech]?.era()] + if (era != null) + stepCost *= (1f + era.eraNumber * constants.eraMultiplier) + stepCost = (stepCost * civModifier).pow(constants.exponent) + goldCostOfUpgrade += (stepCost / constants.roundTo).toInt() * constants.roundTo + return newUnit != unitToUpgradeTo // stop at requested BaseUnit to upgrade to + }) + + return goldCostOfUpgrade } diff --git a/core/src/com/unciv/models/ModConstants.kt b/core/src/com/unciv/models/ModConstants.kt index 06df660dc0..d329c7a9f5 100644 --- a/core/src/com/unciv/models/ModConstants.kt +++ b/core/src/com/unciv/models/ModConstants.kt @@ -34,6 +34,16 @@ class ModConstants { var minimalCityDistance = 3 var minimalCityDistanceOnDifferentContinents = 2 + // Constants used to calculate Unit Upgrade gold Cost (can only be modded all-or-nothing) + class UnitUpgradeCost { + val base = 10f + val perProduction = 2f + val eraMultiplier = 0f // 0.3 in Civ5 cpp sources but 0 in xml + val exponent = 1f + val roundTo = 5 + } + var unitUpgradeCost = UnitUpgradeCost() + // NaturalWonderGenerator uses these to determine the number of Natural Wonders to spawn for a given map size. // With these values, radius * mul + add gives a 1-2-3-4-5 progression for Unciv predefined map sizes and a 2-3-4-5-6-7 progression for the original Civ5 map sizes. // 0.124 = (Civ5.Huge.getHexagonalRadiusForArea(w*h) - Civ5.Duel.getHexagonalRadiusForArea(w*h)) / 5 (if you do not round in the radius function) @@ -63,6 +73,7 @@ class ModConstants { if (other.unitSupplyPerPopulation != defaults.unitSupplyPerPopulation) unitSupplyPerPopulation = other.unitSupplyPerPopulation if (other.minimalCityDistance != defaults.minimalCityDistance) minimalCityDistance = other.minimalCityDistance if (other.minimalCityDistanceOnDifferentContinents != defaults.minimalCityDistanceOnDifferentContinents) minimalCityDistanceOnDifferentContinents = other.minimalCityDistanceOnDifferentContinents + if (other.unitUpgradeCost != defaults.unitUpgradeCost) unitUpgradeCost = other.unitUpgradeCost if (other.naturalWonderCountMultiplier != defaults.naturalWonderCountMultiplier) naturalWonderCountMultiplier = other.naturalWonderCountMultiplier if (other.naturalWonderCountAddedConstant != defaults.naturalWonderCountAddedConstant) naturalWonderCountAddedConstant = other.naturalWonderCountAddedConstant if (other.ancientRuinCountMultiplier != defaults.ancientRuinCountMultiplier) ancientRuinCountMultiplier = other.ancientRuinCountMultiplier diff --git a/core/src/com/unciv/models/UnitAction.kt b/core/src/com/unciv/models/UnitAction.kt index 540db322f9..1b6b212be7 100644 --- a/core/src/com/unciv/models/UnitAction.kt +++ b/core/src/com/unciv/models/UnitAction.kt @@ -7,7 +7,6 @@ import com.badlogic.gdx.utils.Align import com.unciv.ui.utils.KeyCharAndCode import com.unciv.ui.images.ImageGetter import com.unciv.Constants -import com.unciv.models.translations.equalsPlaceholderText import com.unciv.models.translations.getPlaceholderParameters import com.unciv.ui.utils.darken @@ -25,31 +24,28 @@ data class UnitAction( ) { fun getIcon(): Actor { if (type.imageGetter != null) return type.imageGetter.invoke() - return when { - type == UnitActionType.Upgrade - && title.equalsPlaceholderText("Upgrade to [] ([] gold)") -> { + return when (type) { + UnitActionType.Upgrade -> { ImageGetter.getUnitIcon(title.getPlaceholderParameters()[0]) } - type == UnitActionType.Create - && title.equalsPlaceholderText("Create []") -> { + UnitActionType.Create -> { ImageGetter.getImprovementIcon(title.getPlaceholderParameters()[0]) } - type == UnitActionType.SpreadReligion - && title.equalsPlaceholderText("Spread []") -> { + UnitActionType.SpreadReligion -> { val religionName = title.getPlaceholderParameters()[0] ImageGetter.getReligionImage( - if (ImageGetter.religionIconExists(religionName)) religionName + if (ImageGetter.religionIconExists(religionName)) religionName else "Pantheon" ).apply { color = Color.BLACK } } - type == UnitActionType.Fortify || type == UnitActionType.FortifyUntilHealed -> { + UnitActionType.Fortify, UnitActionType.FortifyUntilHealed -> { val match = fortificationRegex.matchEntire(title) val percentFortified = match?.groups?.get(1)?.value?.toInt() ?: 0 - ImageGetter.getImage("OtherIcons/Shield").apply { + ImageGetter.getImage("OtherIcons/Shield").apply { color = Color.GREEN.darken(1f - percentFortified / 80f) } } - else -> ImageGetter.getImage("OtherIcons/Star") + else -> ImageGetter.getImage("OtherIcons/Star").apply { color = Color.BLACK } } } companion object { diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index f1afa84a1d..690e586783 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -17,7 +17,7 @@ import kotlin.random.Random // Buildings, techs, policies, ancient ruins and promotions can have 'triggered' effects object UniqueTriggerActivation { - /** @return boolean whether an action was successfully preformed */ + /** @return boolean whether an action was successfully performed */ fun triggerCivwideUnique( unique: Unique, civInfo: CivilizationInfo, @@ -470,7 +470,7 @@ object UniqueTriggerActivation { return false } - /** @return boolean whether an action was successfully preformed */ + /** @return boolean whether an action was successfully performed */ fun triggerUnitwideUnique( unique: Unique, unit: MapUnit, diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 68a3e9e758..1e65aa41bb 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -467,7 +467,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { fun isBuildableIgnoringTechs(civInfo: CivilizationInfo): Boolean { val rejectionReasons = getRejectionReasons(civInfo) - return rejectionReasons.filterTechPolicyEraWonderRequirements().isEmpty() + return rejectionReasons.isOKIgnoringRequirements(ignoreTechPolicyEraWonderRequirements = true) } override fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat?): Boolean { diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 150b888f79..023fa14b7a 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -12,6 +12,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo +import com.unciv.models.Counter import com.unciv.models.UncivSound import com.unciv.models.UnitAction import com.unciv.models.UnitActionType @@ -83,6 +84,9 @@ object UnitActions { addSleepActions(actionList, unit, true) addFortifyActions(actionList, unit, true) + if (unit.canUpgradeMultipleSteps()) + addUnitUpgradeAction(unit, actionList, 1) + addSwapAction(unit, actionList, worldScreen) addDisbandAction(actionList, unit, worldScreen) addGiftAction(unit, actionList, tile) @@ -301,96 +305,90 @@ object UnitActions { } } - private fun addUnitUpgradeAction(unit: MapUnit, actionList: ArrayList) { - val upgradeAction = getUpgradeAction(unit) + private fun addUnitUpgradeAction( + unit: MapUnit, + actionList: ArrayList, + maxSteps: Int = Int.MAX_VALUE + ) { + val upgradeAction = getUpgradeAction(unit, maxSteps) if (upgradeAction != null) actionList += upgradeAction } - fun getUpgradeAction(unit: MapUnit): UnitAction? { - val tile = unit.currentTile + /** Common implementation for [getUpgradeAction], [getFreeUpgradeAction] and [getAncientRuinsUpgradeAction] */ + private fun getUpgradeAction( + unit: MapUnit, + maxSteps: Int, + isFree: Boolean, + isSpecial: Boolean + ): UnitAction? { if (unit.baseUnit().upgradesTo == null) return null - if (!unit.canUpgrade()) return null - if (tile.getOwner() != unit.civInfo) return null - - val upgradedUnit = unit.getUnitToUpgradeTo() - val goldCostOfUpgrade = unit.getCostOfUpgrade() + val unitTile = unit.getTile() + val civInfo = unit.civInfo + if (!isFree && unitTile.getOwner() != civInfo) return null + + val upgradesTo = unit.baseUnit().upgradesTo + val specialUpgradesTo = unit.baseUnit().specialUpgradesTo + val upgradedUnit = when { + isSpecial && specialUpgradesTo != null -> civInfo.getEquivalentUnit (specialUpgradesTo) + isFree && upgradesTo != null -> civInfo.getEquivalentUnit(upgradesTo) // getUnitToUpgradeTo can't ignore tech + else -> unit.getUnitToUpgradeTo(maxSteps) + } + if (!unit.canUpgrade(unitToUpgradeTo = upgradedUnit, ignoreRequirements = isFree, ignoreResources = true)) + return null + + // Check _new_ resource requirements (display only - yes even for free or special upgrades) + // Using Counter to aggregate is a bit exaggerated, but - respect the mad modder. + val resourceRequirementsDelta = Counter() + for ((resource, amount) in unit.baseUnit().getResourceRequirements()) + resourceRequirementsDelta.add(resource, -amount) + for ((resource, amount) in upgradedUnit.getResourceRequirements()) + resourceRequirementsDelta.add(resource, amount) + val newResourceRequirementsString = resourceRequirementsDelta.entries + .filter { it.value > 0 } + .joinToString { "${it.value} {${it.key}}".tr() } + + val goldCostOfUpgrade = if (isFree) 0 else unit.getCostOfUpgrade(upgradedUnit) + + // No string for "FREE" variants, these are never shown to the user. + // The free actions are only triggered via OneTimeUnitUpgrade or OneTimeUnitSpecialUpgrade in UniqueTriggerActivation. + val title = if (newResourceRequirementsString.isEmpty()) + "Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)" + else "Upgrade to [${upgradedUnit.name}]\n([$goldCostOfUpgrade] gold, [$newResourceRequirementsString])" return UnitAction(UnitActionType.Upgrade, - title = "Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)", + title = title, action = { - val unitTile = unit.getTile() unit.destroy() - val newUnit = unit.civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name) + val newUnit = civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name) /** 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 readdedUnit = unit.civInfo.placeUnitNearTile(unitTile.position, unit.name) - unit.copyStatisticsTo(readdedUnit!!) + val resurrectedUnit = civInfo.placeUnitNearTile(unitTile.position, unit.name)!! + unit.copyStatisticsTo(resurrectedUnit) } else { // Managed to upgrade - unit.civInfo.addGold(-goldCostOfUpgrade) + if (!isFree) civInfo.addGold(-goldCostOfUpgrade) unit.copyStatisticsTo(newUnit) newUnit.currentMovement = 0f } }.takeIf { - unit.civInfo.gold >= goldCostOfUpgrade - && unit.currentMovement > 0 - && !unit.isEmbarked() - } - ) - } - - fun getFreeUpgradeAction(unit: MapUnit): UnitAction? { - if (unit.baseUnit().upgradesTo == null) return null - val upgradedUnit = unit.civInfo.getEquivalentUnit(unit.baseUnit().upgradesTo!!) - if (!unit.canUpgrade(upgradedUnit, true)) return null - - return UnitAction(UnitActionType.Upgrade, - title = "Upgrade to [${upgradedUnit.name}] (FREE)", - action = { - val unitTile = unit.getTile() - unit.destroy() - val newUnit = unit.civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name) - - /** 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 readdedUnit = unit.civInfo.placeUnitNearTile(unitTile.position, unit.name) - unit.copyStatisticsTo(readdedUnit!!) - } else { // Managed to upgrade - unit.copyStatisticsTo(newUnit) - newUnit.currentMovement = 0f - } + isFree || ( + unit.civInfo.gold >= goldCostOfUpgrade + && unit.currentMovement > 0 + && !unit.isEmbarked() + && unit.canUpgrade(unitToUpgradeTo = upgradedUnit) + ) } ) } - fun getAncientRuinsUpgradeAction(unit: MapUnit): UnitAction? { - val upgradedUnitName = - when { - unit.baseUnit.specialUpgradesTo != null -> unit.baseUnit.specialUpgradesTo - unit.baseUnit.upgradesTo != null -> unit.baseUnit.upgradesTo - else -> return null - } - val upgradedUnit = - unit.civInfo.getEquivalentUnit(unit.civInfo.gameInfo.ruleSet.units[upgradedUnitName]!!) - - if (!unit.canUpgrade(upgradedUnit,true)) return null - - return UnitAction(UnitActionType.Upgrade, - title = "Upgrade to [${upgradedUnit.name}] (free)", - action = { - val unitTile = unit.getTile() - unit.destroy() - val newUnit = unit.civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name)!! - unit.copyStatisticsTo(newUnit) - - newUnit.currentMovement = 0f - } - ) - } + fun getUpgradeAction(unit: MapUnit, maxSteps: Int = Int.MAX_VALUE) = + getUpgradeAction(unit, maxSteps, isFree = false, isSpecial = false) + fun getFreeUpgradeAction(unit: MapUnit) = + getUpgradeAction(unit, 1, isFree = true, isSpecial = false) + fun getAncientRuinsUpgradeAction(unit: MapUnit) = + getUpgradeAction(unit, 1, isFree = true, isSpecial = true) private fun addBuildingImprovementsAction(unit: MapUnit, actionList: ArrayList, tile: TileInfo, worldScreen: WorldScreen, unitTable: UnitTable) { if (!unit.hasUniqueToBuildImprovements) return diff --git a/docs/Other/Miscellaneous-JSON-files.md b/docs/Other/Miscellaneous-JSON-files.md index 31ea73c224..ad84a897f5 100644 --- a/docs/Other/Miscellaneous-JSON-files.md +++ b/docs/Other/Miscellaneous-JSON-files.md @@ -89,8 +89,10 @@ The file can have the following attributes, including the values Unciv sets (no ### ModConstants Stored in ModOptions.constants, this is a collection of constants used internally in Unciv. +This is the only structure that is _merged_ field by field from mods, not overwritten, so you can change XP from Barbarians in one mod +and city distance in another. In case of conflicts, there is no guarantee which mod wins, only that _default_ values are ignored. -| Attribute | Type | Optional | Notes | +| Attribute | Type | Default | Notes | | --------- | ---- | -------- | ----- | | maxXPfromBarbarians | Int | 30 | [^A] | | cityStrengthBase| Float | 8.0 | [^B] | @@ -102,6 +104,7 @@ Stored in ModOptions.constants, this is a collection of constants used internall | unitSupplyPerPopulation| Float | 0.5 | [^C] | | minimalCityDistance| Int | 3 | [^D] | | minimalCityDistanceOnDifferentContinents| Int | 2 | [^D] | +| unitUpgradeCost | Object | see below | [^J] | | naturalWonderCountMultiplier| Float | 0.124 | [^E] | | naturalWonderCountAddedConstant| Float | 0.1 | [^E] | | ancientRuinCountMultiplier| Float | 0.02 | [^F] | @@ -134,6 +137,26 @@ Legend: - [^F]: MapGenerator.spreadAncientRuins: number of ruins = suitable tile count * this - [^H]: MapGenerator.spawnLakesAndCoasts: Water bodies up to this tile count become Lakes - [^I]: RiverGenerator: river frequency and length bounds +- [^J]: A [UnitUpgradeCost](#UnitUpgradeCost) sub-structure. + +#### UnitUpgradeCost + +These values are not merged individually, only the entire sub-structure is. + +| Attribute | Type | Default | Notes | +| --------- | ---- | -------- | ----- | +| base | Float | 10 | | +| perProduction | Float | 2 | | +| eraMultiplier | Float | 0 | | +| exponent | Float | 1 | | +| roundTo | Int | 5 | | + +The formula for the gold cost of a unit upgrade is (rounded down to a multiple of `roundTo`): + ( max((`base` + `perProduction` * (new_unit_cost - old_unit_cost)), 0) + * (1 + eraNumber * `eraMultiplier`) * `civModifier` + ) ^ `exponent` +With `civModifier` being the multiplicative aggregate of ["\[relativeAmount\]% Gold cost of upgrading"](../uniques.md#global_uniques) uniques that apply. + ## VictoryTypes.json