From 74279329858d066d3026c3611e7478a2085deded Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Wed, 21 Dec 2022 20:50:25 +0200 Subject: [PATCH] Upgrade path redone --- buildSrc/src/main/kotlin/BuildConfig.kt | 2 +- .../src/com/unciv/logic/city/IConstruction.kt | 14 +-- core/src/com/unciv/logic/map/MapUnit.kt | 103 ++++++++---------- .../com/unciv/models/ruleset/unique/Unique.kt | 10 +- .../unciv/ui/worldscreen/unit/UnitActions.kt | 24 ++-- 5 files changed, 64 insertions(+), 89 deletions(-) diff --git a/buildSrc/src/main/kotlin/BuildConfig.kt b/buildSrc/src/main/kotlin/BuildConfig.kt index 3579f21985..b4b602ddbe 100644 --- a/buildSrc/src/main/kotlin/BuildConfig.kt +++ b/buildSrc/src/main/kotlin/BuildConfig.kt @@ -4,7 +4,7 @@ object BuildConfig { const val kotlinVersion = "1.7.21" const val appName = "Unciv" const val appCodeNumber = 781 - const val appVersion = "4.3.11-patch1" + const val appVersion = "4.3.11-upgrade-redone" const val gdxVersion = "1.11.0" const val roboVMVersion = "2.3.1" diff --git a/core/src/com/unciv/logic/city/IConstruction.kt b/core/src/com/unciv/logic/city/IConstruction.kt index d1fa699566..8995ecafdc 100644 --- a/core/src/com/unciv/logic/city/IConstruction.kt +++ b/core/src/com/unciv/logic/city/IConstruction.kt @@ -91,18 +91,6 @@ class RejectionReasons: HashSet() { fun contains(rejectionReason: RejectionReason) = any { it.rejectionReason == rejectionReason } - fun isOkForUnitUpgradeIgnoringRequirements( - ignoreTechPolicyEraWonderRequirements: Boolean = false, - ignoreResources: Boolean = false - ): Boolean { - var relevantRejectionReasons = this.asSequence().filterNot { it.rejectionReason == RejectionReason.Unbuildable } - if (ignoreTechPolicyEraWonderRequirements) - relevantRejectionReasons = relevantRejectionReasons.filterNot { it.rejectionReason in techPolicyEraWonderRequirements } - if (ignoreResources) - relevantRejectionReasons = relevantRejectionReasons.filterNot { it.rejectionReason == RejectionReason.ConsumesResources } - return relevantRejectionReasons.none() - } - fun hasAReasonToBeRemovedFromQueue(): Boolean { return any { it.rejectionReason in reasonsToDefinitivelyRemoveFromQueue } } @@ -117,7 +105,7 @@ class RejectionReasons: HashSet() { // Used for constant variables in the functions above companion object { - private val techPolicyEraWonderRequirements = hashSetOf( + val techPolicyEraWonderRequirements = hashSetOf( RejectionReason.Obsoleted, RejectionReason.RequiresTech, RejectionReason.RequiresPolicy, diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 9699eef9bd..a3fb326c77 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -10,6 +10,7 @@ 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.NotificationIcon @@ -493,88 +494,72 @@ class MapUnit : IsPartOfGameInfoSerialization { return false } - /** - * 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(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++ + /** 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!!) + upgradeList.add(currentUnit) + currentUnit = nextUpgrade } - return steps + 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. - * @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 - } + fun getUnitToUpgradeTo(): BaseUnit { + val upgradePath = getUpgradePath() - /** 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 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 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), + unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(), ignoreRequirements: Boolean = false, ignoreResources: Boolean = false ): Boolean { if (name == unitToUpgradeTo.name) return false - val rejectionReasons = unitToUpgradeTo.getRejectionReasons(civInfo) - if (rejectionReasons.isOkForUnitUpgradeIgnoringRequirements(ignoreRequirements, ignoreResources)) return true - - // 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 + // TODO redesign without kludge: Inform getRejectionReasons about 'virtually available' resources somehow civInfo.removeUnit(this) - val canUpgrade = unitToUpgradeTo.getRejectionReasons(civInfo) - .isOkForUnitUpgradeIgnoringRequirements(ignoreTechPolicyEraWonderRequirements = ignoreRequirements) + val rejectionReasons = unitToUpgradeTo.getRejectionReasons(civInfo) civInfo.addUnit(this) - return canUpgrade + + 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. @@ -601,17 +586,21 @@ class MapUnit : IsPartOfGameInfoSerialization { for (unique in civInfo.getMatchingUniques(UniqueType.UnitUpgradeCost, stateForConditionals)) civModifier *= unique.params[0].toPercent() - followUpgradePath(actionAllowStep = fun(oldUnit: BaseUnit, newUnit: BaseUnit): Boolean { + 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 * (newUnit.cost - oldUnit.cost)).coerceAtLeast(0f) - val era = ruleset.eras[ruleset.technologies[newUnit.requiredTech]?.era()] + 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) goldCostOfUpgrade += (stepCost / constants.roundTo).toInt() * constants.roundTo - return newUnit != unitToUpgradeTo // stop at requested BaseUnit to upgrade to - }) + if (baseUnit == unitToUpgradeTo) + break // stop at requested BaseUnit to upgrade to + currentUnit = baseUnit + } return goldCostOfUpgrade } diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 1a13003cd1..59990f2a94 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -126,15 +126,17 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s ): Boolean { fun ruleset() = state.civInfo!!.gameInfo.ruleSet + + val relevantUnit by lazy { + if (state.ourCombatant != null && state.ourCombatant is MapUnitCombatant) state.ourCombatant.unit + else state.unit + } + val relevantTile by lazy { state.attackedTile ?: state.tile ?: state.unit?.getTile() ?: state.cityInfo?.getCenterTile() } - val relevantUnit by lazy { - if (state.ourCombatant != null && state.ourCombatant is MapUnitCombatant) state.ourCombatant.unit - else state.unit - } val stateBasedRandom by lazy { Random(state.hashCode()) } diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 349b1d0d54..027abeb651 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -89,9 +89,6 @@ 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) @@ -394,17 +391,15 @@ object UnitActions { private fun addUnitUpgradeAction( unit: MapUnit, - actionList: ArrayList, - maxSteps: Int = Int.MAX_VALUE + actionList: ArrayList ) { - val upgradeAction = getUpgradeAction(unit, maxSteps) + val upgradeAction = getUpgradeAction(unit) if (upgradeAction != null) actionList += upgradeAction } /** Common implementation for [getUpgradeAction], [getFreeUpgradeAction] and [getAncientRuinsUpgradeAction] */ private fun getUpgradeAction( unit: MapUnit, - maxSteps: Int, isFree: Boolean, isSpecial: Boolean ): UnitAction? { @@ -416,10 +411,11 @@ object UnitActions { 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) + 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 } + if (!unit.canUpgrade(unitToUpgradeTo = upgradedUnit, ignoreRequirements = isFree, ignoreResources = true)) return null @@ -470,12 +466,12 @@ object UnitActions { ) } - fun getUpgradeAction(unit: MapUnit, maxSteps: Int = Int.MAX_VALUE) = - getUpgradeAction(unit, maxSteps, isFree = false, isSpecial = false) + fun getUpgradeAction(unit: MapUnit) = + getUpgradeAction(unit, isFree = false, isSpecial = false) fun getFreeUpgradeAction(unit: MapUnit) = - getUpgradeAction(unit, 1, isFree = true, isSpecial = false) + getUpgradeAction(unit, isFree = true, isSpecial = false) fun getAncientRuinsUpgradeAction(unit: MapUnit) = - getUpgradeAction(unit, 1, isFree = true, isSpecial = true) + getUpgradeAction(unit, isFree = true, isSpecial = true) private fun addBuildingImprovementsAction(unit: MapUnit, actionList: ArrayList, tile: TileInfo, worldScreen: WorldScreen, unitTable: UnitTable) { if (!unit.hasUniqueToBuildImprovements) return