From 1d84238dc96be2264b46e28d69823714a8851340 Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Sun, 22 Jan 2023 00:12:51 +0200 Subject: [PATCH] chore: move unit upgrade functions to separate class --- .../logic/automation/unit/UnitAutomation.kt | 2 +- .../com/unciv/logic/map/mapunit/MapUnit.kt | 118 +--------------- .../logic/map/mapunit/UnitUpgradeManager.kt | 126 ++++++++++++++++++ .../ui/overviewscreen/UnitOverviewTable.kt | 4 +- .../unciv/ui/worldscreen/unit/UnitActions.kt | 12 +- 5 files changed, 138 insertions(+), 124 deletions(-) create mode 100644 core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index d3b824057c..26090fa032 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -116,7 +116,7 @@ object UnitAutomation { internal fun tryUpgradeUnit(unit: MapUnit): Boolean { if (unit.baseUnit.upgradesTo == null) return false - val upgradedUnit = unit.getUnitToUpgradeTo() + val upgradedUnit = unit.upgrade.getUnitToUpgradeTo() if (!upgradedUnit.isBuildable(unit.civInfo)) return false // for resource reasons, usually if (upgradedUnit.getResourceRequirements().keys.any { !unit.baseUnit.requiresResource(it) }) { diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index f02fb8ec59..651b145cfd 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -9,8 +9,6 @@ import com.unciv.logic.automation.unit.WorkerAutomation import com.unciv.logic.battle.Battle import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.city.CityInfo -import com.unciv.logic.city.RejectionReason -import com.unciv.logic.city.RejectionReasons import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.LocationAction import com.unciv.logic.civilization.NotificationCategory @@ -52,6 +50,9 @@ class MapUnit : IsPartOfGameInfoSerialization { @Transient val movement = UnitMovementAlgorithms(this) + @Transient + val upgrade = UnitUpgradeManager(this) + @Transient var isDestroyed = false @@ -496,119 +497,6 @@ class MapUnit : IsPartOfGameInfoSerialization { } - /** Returns FULL upgrade path, without checking what we can or cannot build currently. - * Does not contain current baseunit, so will be empty if no upgrades. */ - fun getUpgradePath(): List{ - var currentUnit = baseUnit - val upgradeList = arrayListOf() - while (currentUnit.upgradesTo != null){ - val nextUpgrade = civInfo.getEquivalentUnit(currentUnit.upgradesTo!!) - currentUnit = nextUpgrade - upgradeList.add(currentUnit) - } - return upgradeList - } - - /** 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. - */ - // Used from UnitAutomation, UI action, canUpgrade - fun getUnitToUpgradeTo(): BaseUnit { - val upgradePath = getUpgradePath() - - fun isInvalidUpgradeDestination(baseUnit: BaseUnit): Boolean{ - if (baseUnit.requiredTech != null && !civInfo.tech.isResearched(baseUnit.requiredTech!!)) - return true - if (baseUnit.getMatchingUniques(UniqueType.OnlyAvailableWhen).any { - !it.conditionalsApply(StateForConditionals(civInfo, unit = this )) - }) return true - return false - } - - for (baseUnit in upgradePath.reversed()){ - if (isInvalidUpgradeDestination(baseUnit)) continue - return baseUnit - } - return baseUnit - } - - /** 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 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( - unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(), - ignoreRequirements: Boolean = false, - ignoreResources: Boolean = false - ): Boolean { - if (name == unitToUpgradeTo.name) return false - - // 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 - // TODO redesign without kludge: Inform getRejectionReasons about 'virtually available' resources somehow - civInfo.units.removeUnit(this) - val rejectionReasons = unitToUpgradeTo.getRejectionReasons(civInfo) - civInfo.units.addUnit(this) - - var relevantRejectionReasons = rejectionReasons.asSequence().filterNot { it.rejectionReason == RejectionReason.Unbuildable } - if (ignoreRequirements) - relevantRejectionReasons = relevantRejectionReasons.filterNot { it.rejectionReason in RejectionReasons.techPolicyEraWonderRequirements } - if (ignoreResources) - relevantRejectionReasons = relevantRejectionReasons.filterNot { it.rejectionReason == RejectionReason.ConsumesResources } - return relevantRejectionReasons.none() - } - - /** 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() - - val upgradePath = getUpgradePath() - var currentUnit = baseUnit - for (baseUnit in upgradePath) { - // do clamping and rounding here so upgrading stepwise costs the same as upgrading far down the chain - var stepCost = constants.base - stepCost += (constants.perProduction * (baseUnit.cost - currentUnit.cost)).coerceAtLeast(0f) - val era = ruleset.eras[ruleset.technologies[baseUnit.requiredTech]?.era()] - if (era != null) - stepCost *= (1f + era.eraNumber * constants.eraMultiplier) - stepCost = (stepCost * civModifier).pow(constants.exponent) - stepCost *= civInfo.gameInfo.speed.modifier - goldCostOfUpgrade += (stepCost / constants.roundTo).toInt() * constants.roundTo - if (baseUnit == unitToUpgradeTo) - break // stop at requested BaseUnit to upgrade to - currentUnit = baseUnit - } - - - return goldCostOfUpgrade - } - - fun canFortify(): Boolean { if (baseUnit.isWaterUnit()) return false if (isCivilian()) return false diff --git a/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt b/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt new file mode 100644 index 0000000000..53e8b04d7e --- /dev/null +++ b/core/src/com/unciv/logic/map/mapunit/UnitUpgradeManager.kt @@ -0,0 +1,126 @@ +package com.unciv.logic.map.mapunit + +import com.unciv.logic.city.RejectionReason +import com.unciv.logic.city.RejectionReasons +import com.unciv.models.ruleset.unique.StateForConditionals +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.ruleset.unit.BaseUnit +import com.unciv.ui.utils.extensions.toPercent +import kotlin.math.pow + +class UnitUpgradeManager(val unit:MapUnit) { + + /** Returns FULL upgrade path, without checking what we can or cannot build currently. + * Does not contain current baseunit, so will be empty if no upgrades. */ + private fun getUpgradePath(): List{ + var currentUnit = unit.baseUnit + val upgradeList = arrayListOf() + while (currentUnit.upgradesTo != null){ + val nextUpgrade = unit.civInfo.getEquivalentUnit(currentUnit.upgradesTo!!) + currentUnit = nextUpgrade + upgradeList.add(currentUnit) + } + return upgradeList + } + + /** 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. + */ + // Used from UnitAutomation, UI action, canUpgrade + fun getUnitToUpgradeTo(): BaseUnit { + val upgradePath = getUpgradePath() + + fun isInvalidUpgradeDestination(baseUnit: BaseUnit): Boolean{ + if (baseUnit.requiredTech != null && !unit.civInfo.tech.isResearched(baseUnit.requiredTech!!)) + return true + if (baseUnit.getMatchingUniques(UniqueType.OnlyAvailableWhen).any { + !it.conditionalsApply(StateForConditionals(unit.civInfo, unit = unit )) + }) return true + return false + } + + for (baseUnit in upgradePath.reversed()){ + if (isInvalidUpgradeDestination(baseUnit)) continue + return baseUnit + } + return unit.baseUnit + } + + /** 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 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( + unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(), + ignoreRequirements: Boolean = false, + ignoreResources: Boolean = false + ): Boolean { + if (unit.name == unitToUpgradeTo.name) return false + + // 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 + // TODO redesign without kludge: Inform getRejectionReasons about 'virtually available' resources somehow + unit.civInfo.units.removeUnit(unit) + val rejectionReasons = unitToUpgradeTo.getRejectionReasons(unit.civInfo) + unit.civInfo.units.addUnit(unit) + + var relevantRejectionReasons = rejectionReasons.asSequence().filterNot { it.rejectionReason == RejectionReason.Unbuildable } + if (ignoreRequirements) + relevantRejectionReasons = relevantRejectionReasons.filterNot { it.rejectionReason in RejectionReasons.techPolicyEraWonderRequirements } + if (ignoreResources) + relevantRejectionReasons = relevantRejectionReasons.filterNot { it.rejectionReason == RejectionReason.ConsumesResources } + return relevantRejectionReasons.none() + } + + /** 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 = unit.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(unit.civInfo, unit = unit) + for (unique in unit.civInfo.getMatchingUniques(UniqueType.UnitUpgradeCost, stateForConditionals)) + civModifier *= unique.params[0].toPercent() + + val upgradePath = getUpgradePath() + var currentUnit = unit.baseUnit + for (baseUnit in upgradePath) { + // do clamping and rounding here so upgrading stepwise costs the same as upgrading far down the chain + var stepCost = constants.base + stepCost += (constants.perProduction * (baseUnit.cost - currentUnit.cost)).coerceAtLeast(0f) + val era = ruleset.eras[ruleset.technologies[baseUnit.requiredTech]?.era()] + if (era != null) + stepCost *= (1f + era.eraNumber * constants.eraMultiplier) + stepCost = (stepCost * civModifier).pow(constants.exponent) + stepCost *= unit.civInfo.gameInfo.speed.modifier + goldCostOfUpgrade += (stepCost / constants.roundTo).toInt() * constants.roundTo + if (baseUnit == unitToUpgradeTo) + break // stop at requested BaseUnit to upgrade to + currentUnit = baseUnit + } + + + return goldCostOfUpgrade + } + + +} diff --git a/core/src/com/unciv/ui/overviewscreen/UnitOverviewTable.kt b/core/src/com/unciv/ui/overviewscreen/UnitOverviewTable.kt index 7eb299ccb6..7a6db7fa6d 100644 --- a/core/src/com/unciv/ui/overviewscreen/UnitOverviewTable.kt +++ b/core/src/com/unciv/ui/overviewscreen/UnitOverviewTable.kt @@ -229,10 +229,10 @@ class UnitOverviewTab( add(promotionsTable) // Upgrade column - if (unit.canUpgrade()) { + if (unit.upgrade.canUpgrade()) { val unitAction = UnitActions.getUpgradeAction(unit) val enable = unitAction?.action != null - val upgradeIcon = ImageGetter.getUnitIcon(unit.getUnitToUpgradeTo().name, + val upgradeIcon = ImageGetter.getUnitIcon(unit.upgrade.getUnitToUpgradeTo().name, if (enable) Color.GREEN else Color.GREEN.darken(0.5f)) if (enable) upgradeIcon.onClick { SoundPlayer.play(unitAction!!.uncivSound) diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 12a3989b6a..b738aaa0d3 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -414,10 +414,10 @@ object UnitActions { val upgradedUnit = when { isSpecial && specialUpgradesTo != null -> civInfo.getEquivalentUnit(specialUpgradesTo) (isFree || isSpecial) && upgradesTo != null -> civInfo.getEquivalentUnit(upgradesTo) // Only get DIRECT upgrade - else -> unit.getUnitToUpgradeTo() // Get EVENTUAL upgrade, all the way up the chain + else -> unit.upgrade.getUnitToUpgradeTo() // Get EVENTUAL upgrade, all the way up the chain } - if (!unit.canUpgrade(unitToUpgradeTo = upgradedUnit, ignoreRequirements = isFree, ignoreResources = true)) + if (!unit.upgrade.canUpgrade(unitToUpgradeTo = upgradedUnit, ignoreRequirements = isFree, ignoreResources = true)) return null // Check _new_ resource requirements (display only - yes even for free or special upgrades) @@ -431,7 +431,7 @@ object UnitActions { .filter { it.value > 0 } .joinToString { "${it.value} {${it.key}}".tr() } - val goldCostOfUpgrade = if (isFree) 0 else unit.getCostOfUpgrade(upgradedUnit) + val goldCostOfUpgrade = if (isFree) 0 else unit.upgrade.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. @@ -461,7 +461,7 @@ object UnitActions { unit.civInfo.gold >= goldCostOfUpgrade && unit.currentMovement > 0 && !unit.isEmbarked() - && unit.canUpgrade(unitToUpgradeTo = upgradedUnit) + && unit.upgrade.canUpgrade(unitToUpgradeTo = upgradedUnit) ) } ) @@ -494,7 +494,7 @@ object UnitActions { StateForConditionals(unit = unit, civInfo = civInfo, tile = unitTile))) { val upgradedUnit = civInfo.getEquivalentUnit(unique.params[0]) // don't show if haven't researched/is obsolete - if (!unit.canUpgrade(unitToUpgradeTo = upgradedUnit)) continue + if (!unit.upgrade.canUpgrade(unitToUpgradeTo = upgradedUnit)) continue // Check _new_ resource requirements // Using Counter to aggregate is a bit exaggerated, but - respect the mad modder. @@ -530,7 +530,7 @@ object UnitActions { }.takeIf { unit.currentMovement > 0 && !unit.isEmbarked() - && unit.canUpgrade(unitToUpgradeTo = upgradedUnit) + && unit.upgrade.canUpgrade(unitToUpgradeTo = upgradedUnit) } ) ) }