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 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"

View File

@ -91,18 +91,6 @@ class RejectionReasons: HashSet<RejectionReasonInstance>() {
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<RejectionReasonInstance>() {
// Used for constant variables in the functions above
companion object {
private val techPolicyEraWonderRequirements = hashSetOf(
val techPolicyEraWonderRequirements = hashSetOf(
RejectionReason.Obsoleted,
RejectionReason.RequiresTech,
RejectionReason.RequiresPolicy,

View File

@ -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<BaseUnit>{
var currentUnit = baseUnit
val upgradeList = arrayListOf<BaseUnit>()
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
}

View File

@ -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()) }

View File

@ -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<UnitAction>,
maxSteps: Int = Int.MAX_VALUE
actionList: ArrayList<UnitAction>
) {
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<UnitAction>, tile: TileInfo, worldScreen: WorldScreen, unitTable: UnitTable) {
if (!unit.hasUniqueToBuildImprovements) return