chore: move unit upgrade functions to separate class

This commit is contained in:
Yair Morgenstern
2023-01-22 00:12:51 +02:00
parent af24a03cc9
commit 1d84238dc9
5 changed files with 138 additions and 124 deletions

View File

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

View File

@ -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<BaseUnit>{
var currentUnit = baseUnit
val upgradeList = arrayListOf<BaseUnit>()
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 <a href="https://github.com/dmnd/CvGameCoreSource/blob/6501d2398113a5100ffa854c146fb6f113992898/CvGameCoreDLL_Expansion1/CvUnit.cpp#L7728">CvUnit::upgradePrice</a>
*/
// 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

View File

@ -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<BaseUnit>{
var currentUnit = unit.baseUnit
val upgradeList = arrayListOf<BaseUnit>()
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 <a href="https://github.com/dmnd/CvGameCoreSource/blob/6501d2398113a5100ffa854c146fb6f113992898/CvGameCoreDLL_Expansion1/CvUnit.cpp#L7728">CvUnit::upgradePrice</a>
*/
// 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
}
}

View File

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

View File

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