Upgrade path redone

This commit is contained in:
Yair Morgenstern
2022-12-21 20:50:25 +02:00
parent bfbabcb97e
commit 7427932985
5 changed files with 64 additions and 89 deletions

View File

@ -4,7 +4,7 @@ object BuildConfig {
const val kotlinVersion = "1.7.21" const val kotlinVersion = "1.7.21"
const val appName = "Unciv" const val appName = "Unciv"
const val appCodeNumber = 781 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 gdxVersion = "1.11.0"
const val roboVMVersion = "2.3.1" const val roboVMVersion = "2.3.1"

View File

@ -91,18 +91,6 @@ class RejectionReasons: HashSet<RejectionReasonInstance>() {
fun contains(rejectionReason: RejectionReason) = any { it.rejectionReason == rejectionReason } 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 { fun hasAReasonToBeRemovedFromQueue(): Boolean {
return any { it.rejectionReason in reasonsToDefinitivelyRemoveFromQueue } return any { it.rejectionReason in reasonsToDefinitivelyRemoveFromQueue }
} }
@ -117,7 +105,7 @@ class RejectionReasons: HashSet<RejectionReasonInstance>() {
// Used for constant variables in the functions above // Used for constant variables in the functions above
companion object { companion object {
private val techPolicyEraWonderRequirements = hashSetOf( val techPolicyEraWonderRequirements = hashSetOf(
RejectionReason.Obsoleted, RejectionReason.Obsoleted,
RejectionReason.RequiresTech, RejectionReason.RequiresTech,
RejectionReason.RequiresPolicy, RejectionReason.RequiresPolicy,

View File

@ -10,6 +10,7 @@ import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.city.CityInfo import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.RejectionReason import com.unciv.logic.city.RejectionReason
import com.unciv.logic.city.RejectionReasons
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.LocationAction import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.NotificationIcon
@ -493,88 +494,72 @@ class MapUnit : IsPartOfGameInfoSerialization {
return false 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 /** Returns FULL upgrade path, without checking what we can or cannot build currently.
while(steps < maxSteps) { * Does not contain current baseunit, so will be empty if no upgrades. */
if (unit.upgradesTo == null) break fun getUpgradePath(): List<BaseUnit>{
val newUnit = unit.getDirectUpgradeUnit(civInfo) var currentUnit = baseUnit
val techName = newUnit.requiredTech val upgradeList = arrayListOf<BaseUnit>()
if (techName != null && !civInfo.tech.isResearched(techName)) break while (currentUnit.upgradesTo != null){
if (!actionAllowStep(unit, newUnit)) break val nextUpgrade = civInfo.getEquivalentUnit(currentUnit.upgradesTo!!)
unit = newUnit upgradeList.add(currentUnit)
steps++ currentUnit = nextUpgrade
} }
return steps return upgradeList
} }
/** Get the base unit this map unit could upgrade to, respecting researched tech and nation uniques only. /** 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. * 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 // Used from UnitAutomation, UI action, canUpgrade
fun getUnitToUpgradeTo(maxSteps: Int = Int.MAX_VALUE): BaseUnit { fun getUnitToUpgradeTo(): BaseUnit {
var unit = baseUnit() val upgradePath = getUpgradePath()
followUpgradePath(maxSteps) { _, newUnit ->
unit = newUnit
true
}
return unit
}
/** Check if the default upgrade would do more than one step fun isInvalidUpgradeDestination(baseUnit: BaseUnit): Boolean{
* - to avoid showing both the single step and normal upgrades in UnitActions */ if (baseUnit.requiredTech != null && !civInfo.tech.isResearched(baseUnit.requiredTech!!))
fun canUpgradeMultipleSteps(): Boolean { return true
return 1 < followUpgradePath(2) { _, _ -> 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 /** 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. * 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). * @param ignoreRequirements Ignore possible tech/policy/building requirements (e.g. resource requirements still count).
* Used for upgrading units via ancient ruins. * Used for upgrading units via ancient ruins.
* @param ignoreResources Ignore resource requirements (tech still counts) * @param ignoreResources Ignore resource requirements (tech still counts)
* Used to display disabled Upgrade button * Used to display disabled Upgrade button
*/ */
fun canUpgrade( fun canUpgrade(
maxSteps: Int = Int.MAX_VALUE, unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(),
unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(maxSteps),
ignoreRequirements: Boolean = false, ignoreRequirements: Boolean = false,
ignoreResources: Boolean = false ignoreResources: Boolean = false
): Boolean { ): Boolean {
if (name == unitToUpgradeTo.name) return false 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, // We need to remove the unit from the civ for this check,
// because if the unit requires, say, horses, and so does its upgrade, // 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 // and the civ currently has 0 horses, we need to see if the upgrade will be buildable
// WHEN THE CURRENT UNIT IS NOT HERE // WHEN THE CURRENT UNIT IS NOT HERE
// TODO redesign without kludge: Inform getRejectionReasons about 'virtually available' resources somehow
civInfo.removeUnit(this) civInfo.removeUnit(this)
val canUpgrade = unitToUpgradeTo.getRejectionReasons(civInfo) val rejectionReasons = unitToUpgradeTo.getRejectionReasons(civInfo)
.isOkForUnitUpgradeIgnoringRequirements(ignoreTechPolicyEraWonderRequirements = ignoreRequirements)
civInfo.addUnit(this) 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. /** 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)) for (unique in civInfo.getMatchingUniques(UniqueType.UnitUpgradeCost, stateForConditionals))
civModifier *= unique.params[0].toPercent() 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 // do clamping and rounding here so upgrading stepwise costs the same as upgrading far down the chain
var stepCost = constants.base var stepCost = constants.base
stepCost += (constants.perProduction * (newUnit.cost - oldUnit.cost)).coerceAtLeast(0f) stepCost += (constants.perProduction * (baseUnit.cost - currentUnit.cost)).coerceAtLeast(0f)
val era = ruleset.eras[ruleset.technologies[newUnit.requiredTech]?.era()] val era = ruleset.eras[ruleset.technologies[baseUnit.requiredTech]?.era()]
if (era != null) if (era != null)
stepCost *= (1f + era.eraNumber * constants.eraMultiplier) stepCost *= (1f + era.eraNumber * constants.eraMultiplier)
stepCost = (stepCost * civModifier).pow(constants.exponent) stepCost = (stepCost * civModifier).pow(constants.exponent)
goldCostOfUpgrade += (stepCost / constants.roundTo).toInt() * constants.roundTo 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 return goldCostOfUpgrade
} }

View File

@ -126,15 +126,17 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
): Boolean { ): Boolean {
fun ruleset() = state.civInfo!!.gameInfo.ruleSet 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 val relevantTile by lazy { state.attackedTile
?: state.tile ?: state.tile
?: state.unit?.getTile() ?: state.unit?.getTile()
?: state.cityInfo?.getCenterTile() ?: 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()) } val stateBasedRandom by lazy { Random(state.hashCode()) }

View File

@ -89,9 +89,6 @@ object UnitActions {
addSleepActions(actionList, unit, true) addSleepActions(actionList, unit, true)
addFortifyActions(actionList, unit, true) addFortifyActions(actionList, unit, true)
if (unit.canUpgradeMultipleSteps())
addUnitUpgradeAction(unit, actionList, 1)
addSwapAction(unit, actionList, worldScreen) addSwapAction(unit, actionList, worldScreen)
addDisbandAction(actionList, unit, worldScreen) addDisbandAction(actionList, unit, worldScreen)
addGiftAction(unit, actionList, tile) addGiftAction(unit, actionList, tile)
@ -394,17 +391,15 @@ object UnitActions {
private fun addUnitUpgradeAction( private fun addUnitUpgradeAction(
unit: MapUnit, unit: MapUnit,
actionList: ArrayList<UnitAction>, actionList: ArrayList<UnitAction>
maxSteps: Int = Int.MAX_VALUE
) { ) {
val upgradeAction = getUpgradeAction(unit, maxSteps) val upgradeAction = getUpgradeAction(unit)
if (upgradeAction != null) actionList += upgradeAction if (upgradeAction != null) actionList += upgradeAction
} }
/** Common implementation for [getUpgradeAction], [getFreeUpgradeAction] and [getAncientRuinsUpgradeAction] */ /** Common implementation for [getUpgradeAction], [getFreeUpgradeAction] and [getAncientRuinsUpgradeAction] */
private fun getUpgradeAction( private fun getUpgradeAction(
unit: MapUnit, unit: MapUnit,
maxSteps: Int,
isFree: Boolean, isFree: Boolean,
isSpecial: Boolean isSpecial: Boolean
): UnitAction? { ): UnitAction? {
@ -416,10 +411,11 @@ object UnitActions {
val upgradesTo = unit.baseUnit().upgradesTo val upgradesTo = unit.baseUnit().upgradesTo
val specialUpgradesTo = unit.baseUnit().specialUpgradesTo val specialUpgradesTo = unit.baseUnit().specialUpgradesTo
val upgradedUnit = when { val upgradedUnit = when {
isSpecial && specialUpgradesTo != null -> civInfo.getEquivalentUnit (specialUpgradesTo) isSpecial && specialUpgradesTo != null -> civInfo.getEquivalentUnit(specialUpgradesTo)
isFree && upgradesTo != null -> civInfo.getEquivalentUnit(upgradesTo) // getUnitToUpgradeTo can't ignore tech (isFree || isSpecial) && upgradesTo != null -> civInfo.getEquivalentUnit(upgradesTo) // Only get DIRECT upgrade
else -> unit.getUnitToUpgradeTo(maxSteps) else -> unit.getUnitToUpgradeTo() // Get EVENTUAL upgrade, all the way up the chain
} }
if (!unit.canUpgrade(unitToUpgradeTo = upgradedUnit, ignoreRequirements = isFree, ignoreResources = true)) if (!unit.canUpgrade(unitToUpgradeTo = upgradedUnit, ignoreRequirements = isFree, ignoreResources = true))
return null return null
@ -470,12 +466,12 @@ object UnitActions {
) )
} }
fun getUpgradeAction(unit: MapUnit, maxSteps: Int = Int.MAX_VALUE) = fun getUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, maxSteps, isFree = false, isSpecial = false) getUpgradeAction(unit, isFree = false, isSpecial = false)
fun getFreeUpgradeAction(unit: MapUnit) = fun getFreeUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, 1, isFree = true, isSpecial = false) getUpgradeAction(unit, isFree = true, isSpecial = false)
fun getAncientRuinsUpgradeAction(unit: MapUnit) = fun getAncientRuinsUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, 1, isFree = true, isSpecial = true) getUpgradeAction(unit, isFree = true, isSpecial = true)
private fun addBuildingImprovementsAction(unit: MapUnit, actionList: ArrayList<UnitAction>, tile: TileInfo, worldScreen: WorldScreen, unitTable: UnitTable) { private fun addBuildingImprovementsAction(unit: MapUnit, actionList: ArrayList<UnitAction>, tile: TileInfo, worldScreen: WorldScreen, unitTable: UnitTable) {
if (!unit.hasUniqueToBuildImprovements) return if (!unit.hasUniqueToBuildImprovements) return