Show required resource for upgrades, rework upgrade logic (#6849)

* Show required resource for upgrades, rework upgrade logic

* Show required resource for upgrades - reviews
This commit is contained in:
SomeTroglodyte 2022-05-25 18:35:27 +02:00 committed by GitHub
parent 0461d9d7fd
commit 0f63000ac8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 240 additions and 122 deletions

View File

@ -843,6 +843,7 @@ Paradrop =
Add in capital =
Add to [comment] =
Upgrade to [unitType] ([goldCost] gold) =
Upgrade to [unitType]\n([goldCost] gold, [resources]) =
Found city =
Promote =
Health =

View File

@ -89,8 +89,19 @@ class RejectionReasons: HashSet<RejectionReasonInstance>() {
fun contains(rejectionReason: RejectionReason) = any { it.rejectionReason == rejectionReason }
fun filterTechPolicyEraWonderRequirements(): List<RejectionReasonInstance> {
return filterNot { it.rejectionReason in techPolicyEraWonderRequirements }
fun isOKIgnoringRequirements(
ignoreTechPolicyEraWonderRequirements: Boolean = false,
ignoreResources: Boolean = false
): Boolean {
if (!ignoreTechPolicyEraWonderRequirements && !ignoreResources) return isEmpty()
if (!ignoreTechPolicyEraWonderRequirements)
return all { it.rejectionReason == RejectionReason.ConsumesResources }
if (!ignoreResources)
return all { it.rejectionReason in techPolicyEraWonderRequirements }
return all {
it.rejectionReason == RejectionReason.ConsumesResources ||
it.rejectionReason in techPolicyEraWonderRequirements
}
}
fun hasAReasonToBeRemovedFromQueue(): Boolean {

View File

@ -501,49 +501,127 @@ class MapUnit {
return false
}
fun getUnitToUpgradeTo(): BaseUnit {
/**
* 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 (unit.upgradesTo != null && unit.getDirectUpgradeUnit(civInfo).requiredTech
.let { it == null || civInfo.tech.isResearched(it) }
)
unit = unit.getDirectUpgradeUnit(civInfo)
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++
}
return steps
}
/** 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
}
/** @param ignoreRequired: Ignore possible tech/policy/building requirements.
* Used for upgrading units via ancient ruins.
*/
fun canUpgrade(unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(), ignoreRequired: Boolean = false): Boolean {
if (name == unitToUpgradeTo.name) return false
val rejectionReasons = unitToUpgradeTo.getRejectionReasons(civInfo)
if (rejectionReasons.isEmpty()) return true
if (ignoreRequired && rejectionReasons.filterTechPolicyEraWonderRequirements().isEmpty()) return true
if (rejectionReasons.contains(RejectionReason.ConsumesResources)) {
// 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
civInfo.removeUnit(this)
val canUpgrade =
if (ignoreRequired) unitToUpgradeTo.isBuildableIgnoringTechs(civInfo)
else unitToUpgradeTo.isBuildable(civInfo)
civInfo.addUnit(this)
return canUpgrade
}
return false
/** 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 getCostOfUpgrade(): Int {
val unitToUpgradeTo = getUnitToUpgradeTo()
var goldCostOfUpgrade = (unitToUpgradeTo.cost - baseUnit().cost) * 2f + 10f
for (unique in civInfo.getMatchingUniques(UniqueType.UnitUpgradeCost, StateForConditionals(civInfo, unit=this)))
goldCostOfUpgrade *= unique.params[0].toPercent()
/** 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),
ignoreRequirements: Boolean = false,
ignoreResources: Boolean = false
): Boolean {
if (name == unitToUpgradeTo.name) return false
val rejectionReasons = unitToUpgradeTo.getRejectionReasons(civInfo)
if (rejectionReasons.isOKIgnoringRequirements(ignoreRequirements, ignoreResources)) return true
if (goldCostOfUpgrade < 0) return 0 // For instance, Landsknecht costs less than Spearman, so upgrading would cost negative gold
return goldCostOfUpgrade.toInt()
// 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
civInfo.removeUnit(this)
val canUpgrade = unitToUpgradeTo.getRejectionReasons(civInfo)
.isOKIgnoringRequirements(ignoreTechPolicyEraWonderRequirements = ignoreRequirements)
civInfo.addUnit(this)
return canUpgrade
}
/** 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()
followUpgradePath(actionAllowStep = fun(oldUnit: BaseUnit, newUnit: BaseUnit): Boolean {
// 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()]
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
})
return goldCostOfUpgrade
}

View File

@ -34,6 +34,16 @@ class ModConstants {
var minimalCityDistance = 3
var minimalCityDistanceOnDifferentContinents = 2
// Constants used to calculate Unit Upgrade gold Cost (can only be modded all-or-nothing)
class UnitUpgradeCost {
val base = 10f
val perProduction = 2f
val eraMultiplier = 0f // 0.3 in Civ5 cpp sources but 0 in xml
val exponent = 1f
val roundTo = 5
}
var unitUpgradeCost = UnitUpgradeCost()
// NaturalWonderGenerator uses these to determine the number of Natural Wonders to spawn for a given map size.
// With these values, radius * mul + add gives a 1-2-3-4-5 progression for Unciv predefined map sizes and a 2-3-4-5-6-7 progression for the original Civ5 map sizes.
// 0.124 = (Civ5.Huge.getHexagonalRadiusForArea(w*h) - Civ5.Duel.getHexagonalRadiusForArea(w*h)) / 5 (if you do not round in the radius function)
@ -63,6 +73,7 @@ class ModConstants {
if (other.unitSupplyPerPopulation != defaults.unitSupplyPerPopulation) unitSupplyPerPopulation = other.unitSupplyPerPopulation
if (other.minimalCityDistance != defaults.minimalCityDistance) minimalCityDistance = other.minimalCityDistance
if (other.minimalCityDistanceOnDifferentContinents != defaults.minimalCityDistanceOnDifferentContinents) minimalCityDistanceOnDifferentContinents = other.minimalCityDistanceOnDifferentContinents
if (other.unitUpgradeCost != defaults.unitUpgradeCost) unitUpgradeCost = other.unitUpgradeCost
if (other.naturalWonderCountMultiplier != defaults.naturalWonderCountMultiplier) naturalWonderCountMultiplier = other.naturalWonderCountMultiplier
if (other.naturalWonderCountAddedConstant != defaults.naturalWonderCountAddedConstant) naturalWonderCountAddedConstant = other.naturalWonderCountAddedConstant
if (other.ancientRuinCountMultiplier != defaults.ancientRuinCountMultiplier) ancientRuinCountMultiplier = other.ancientRuinCountMultiplier

View File

@ -7,7 +7,6 @@ import com.badlogic.gdx.utils.Align
import com.unciv.ui.utils.KeyCharAndCode
import com.unciv.ui.images.ImageGetter
import com.unciv.Constants
import com.unciv.models.translations.equalsPlaceholderText
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.ui.utils.darken
@ -25,31 +24,28 @@ data class UnitAction(
) {
fun getIcon(): Actor {
if (type.imageGetter != null) return type.imageGetter.invoke()
return when {
type == UnitActionType.Upgrade
&& title.equalsPlaceholderText("Upgrade to [] ([] gold)") -> {
return when (type) {
UnitActionType.Upgrade -> {
ImageGetter.getUnitIcon(title.getPlaceholderParameters()[0])
}
type == UnitActionType.Create
&& title.equalsPlaceholderText("Create []") -> {
UnitActionType.Create -> {
ImageGetter.getImprovementIcon(title.getPlaceholderParameters()[0])
}
type == UnitActionType.SpreadReligion
&& title.equalsPlaceholderText("Spread []") -> {
UnitActionType.SpreadReligion -> {
val religionName = title.getPlaceholderParameters()[0]
ImageGetter.getReligionImage(
if (ImageGetter.religionIconExists(religionName)) religionName
else "Pantheon"
).apply { color = Color.BLACK }
}
type == UnitActionType.Fortify || type == UnitActionType.FortifyUntilHealed -> {
UnitActionType.Fortify, UnitActionType.FortifyUntilHealed -> {
val match = fortificationRegex.matchEntire(title)
val percentFortified = match?.groups?.get(1)?.value?.toInt() ?: 0
ImageGetter.getImage("OtherIcons/Shield").apply {
color = Color.GREEN.darken(1f - percentFortified / 80f)
}
}
else -> ImageGetter.getImage("OtherIcons/Star")
else -> ImageGetter.getImage("OtherIcons/Star").apply { color = Color.BLACK }
}
}
companion object {

View File

@ -17,7 +17,7 @@ import kotlin.random.Random
// Buildings, techs, policies, ancient ruins and promotions can have 'triggered' effects
object UniqueTriggerActivation {
/** @return boolean whether an action was successfully preformed */
/** @return boolean whether an action was successfully performed */
fun triggerCivwideUnique(
unique: Unique,
civInfo: CivilizationInfo,
@ -470,7 +470,7 @@ object UniqueTriggerActivation {
return false
}
/** @return boolean whether an action was successfully preformed */
/** @return boolean whether an action was successfully performed */
fun triggerUnitwideUnique(
unique: Unique,
unit: MapUnit,

View File

@ -467,7 +467,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
fun isBuildableIgnoringTechs(civInfo: CivilizationInfo): Boolean {
val rejectionReasons = getRejectionReasons(civInfo)
return rejectionReasons.filterTechPolicyEraWonderRequirements().isEmpty()
return rejectionReasons.isOKIgnoringRequirements(ignoreTechPolicyEraWonderRequirements = true)
}
override fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat?): Boolean {

View File

@ -12,6 +12,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo
import com.unciv.models.Counter
import com.unciv.models.UncivSound
import com.unciv.models.UnitAction
import com.unciv.models.UnitActionType
@ -83,6 +84,9 @@ 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)
@ -301,96 +305,90 @@ object UnitActions {
}
}
private fun addUnitUpgradeAction(unit: MapUnit, actionList: ArrayList<UnitAction>) {
val upgradeAction = getUpgradeAction(unit)
private fun addUnitUpgradeAction(
unit: MapUnit,
actionList: ArrayList<UnitAction>,
maxSteps: Int = Int.MAX_VALUE
) {
val upgradeAction = getUpgradeAction(unit, maxSteps)
if (upgradeAction != null) actionList += upgradeAction
}
fun getUpgradeAction(unit: MapUnit): UnitAction? {
val tile = unit.currentTile
/** Common implementation for [getUpgradeAction], [getFreeUpgradeAction] and [getAncientRuinsUpgradeAction] */
private fun getUpgradeAction(
unit: MapUnit,
maxSteps: Int,
isFree: Boolean,
isSpecial: Boolean
): UnitAction? {
if (unit.baseUnit().upgradesTo == null) return null
if (!unit.canUpgrade()) return null
if (tile.getOwner() != unit.civInfo) return null
val unitTile = unit.getTile()
val civInfo = unit.civInfo
if (!isFree && unitTile.getOwner() != civInfo) return null
val upgradedUnit = unit.getUnitToUpgradeTo()
val goldCostOfUpgrade = unit.getCostOfUpgrade()
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)
}
if (!unit.canUpgrade(unitToUpgradeTo = upgradedUnit, ignoreRequirements = isFree, ignoreResources = true))
return null
// Check _new_ resource requirements (display only - yes even for free or special upgrades)
// Using Counter to aggregate is a bit exaggerated, but - respect the mad modder.
val resourceRequirementsDelta = Counter<String>()
for ((resource, amount) in unit.baseUnit().getResourceRequirements())
resourceRequirementsDelta.add(resource, -amount)
for ((resource, amount) in upgradedUnit.getResourceRequirements())
resourceRequirementsDelta.add(resource, amount)
val newResourceRequirementsString = resourceRequirementsDelta.entries
.filter { it.value > 0 }
.joinToString { "${it.value} {${it.key}}".tr() }
val goldCostOfUpgrade = if (isFree) 0 else unit.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.
val title = if (newResourceRequirementsString.isEmpty())
"Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)"
else "Upgrade to [${upgradedUnit.name}]\n([$goldCostOfUpgrade] gold, [$newResourceRequirementsString])"
return UnitAction(UnitActionType.Upgrade,
title = "Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)",
title = title,
action = {
val unitTile = unit.getTile()
unit.destroy()
val newUnit = unit.civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name)
val newUnit = civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name)
/** We were UNABLE to place the new unit, which means that the unit failed to upgrade!
* The only known cause of this currently is "land units upgrading to water units" which fail to be placed.
*/
if (newUnit == null) {
val readdedUnit = unit.civInfo.placeUnitNearTile(unitTile.position, unit.name)
unit.copyStatisticsTo(readdedUnit!!)
val resurrectedUnit = civInfo.placeUnitNearTile(unitTile.position, unit.name)!!
unit.copyStatisticsTo(resurrectedUnit)
} else { // Managed to upgrade
unit.civInfo.addGold(-goldCostOfUpgrade)
if (!isFree) civInfo.addGold(-goldCostOfUpgrade)
unit.copyStatisticsTo(newUnit)
newUnit.currentMovement = 0f
}
}.takeIf {
unit.civInfo.gold >= goldCostOfUpgrade
&& unit.currentMovement > 0
&& !unit.isEmbarked()
isFree || (
unit.civInfo.gold >= goldCostOfUpgrade
&& unit.currentMovement > 0
&& !unit.isEmbarked()
&& unit.canUpgrade(unitToUpgradeTo = upgradedUnit)
)
}
)
}
fun getFreeUpgradeAction(unit: MapUnit): UnitAction? {
if (unit.baseUnit().upgradesTo == null) return null
val upgradedUnit = unit.civInfo.getEquivalentUnit(unit.baseUnit().upgradesTo!!)
if (!unit.canUpgrade(upgradedUnit, true)) return null
return UnitAction(UnitActionType.Upgrade,
title = "Upgrade to [${upgradedUnit.name}] (FREE)",
action = {
val unitTile = unit.getTile()
unit.destroy()
val newUnit = unit.civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name)
/** We were UNABLE to place the new unit, which means that the unit failed to upgrade!
* The only known cause of this currently is "land units upgrading to water units" which fail to be placed.
*/
if (newUnit == null) {
val readdedUnit = unit.civInfo.placeUnitNearTile(unitTile.position, unit.name)
unit.copyStatisticsTo(readdedUnit!!)
} else { // Managed to upgrade
unit.copyStatisticsTo(newUnit)
newUnit.currentMovement = 0f
}
}
)
}
fun getAncientRuinsUpgradeAction(unit: MapUnit): UnitAction? {
val upgradedUnitName =
when {
unit.baseUnit.specialUpgradesTo != null -> unit.baseUnit.specialUpgradesTo
unit.baseUnit.upgradesTo != null -> unit.baseUnit.upgradesTo
else -> return null
}
val upgradedUnit =
unit.civInfo.getEquivalentUnit(unit.civInfo.gameInfo.ruleSet.units[upgradedUnitName]!!)
if (!unit.canUpgrade(upgradedUnit,true)) return null
return UnitAction(UnitActionType.Upgrade,
title = "Upgrade to [${upgradedUnit.name}] (free)",
action = {
val unitTile = unit.getTile()
unit.destroy()
val newUnit = unit.civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name)!!
unit.copyStatisticsTo(newUnit)
newUnit.currentMovement = 0f
}
)
}
fun getUpgradeAction(unit: MapUnit, maxSteps: Int = Int.MAX_VALUE) =
getUpgradeAction(unit, maxSteps, isFree = false, isSpecial = false)
fun getFreeUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, 1, isFree = true, isSpecial = false)
fun getAncientRuinsUpgradeAction(unit: MapUnit) =
getUpgradeAction(unit, 1, isFree = true, isSpecial = true)
private fun addBuildingImprovementsAction(unit: MapUnit, actionList: ArrayList<UnitAction>, tile: TileInfo, worldScreen: WorldScreen, unitTable: UnitTable) {
if (!unit.hasUniqueToBuildImprovements) return

View File

@ -89,8 +89,10 @@ The file can have the following attributes, including the values Unciv sets (no
### ModConstants
Stored in ModOptions.constants, this is a collection of constants used internally in Unciv.
This is the only structure that is _merged_ field by field from mods, not overwritten, so you can change XP from Barbarians in one mod
and city distance in another. In case of conflicts, there is no guarantee which mod wins, only that _default_ values are ignored.
| Attribute | Type | Optional | Notes |
| Attribute | Type | Default | Notes |
| --------- | ---- | -------- | ----- |
| maxXPfromBarbarians | Int | 30 | [^A] |
| cityStrengthBase| Float | 8.0 | [^B] |
@ -102,6 +104,7 @@ Stored in ModOptions.constants, this is a collection of constants used internall
| unitSupplyPerPopulation| Float | 0.5 | [^C] |
| minimalCityDistance| Int | 3 | [^D] |
| minimalCityDistanceOnDifferentContinents| Int | 2 | [^D] |
| unitUpgradeCost | Object | see below | [^J] |
| naturalWonderCountMultiplier| Float | 0.124 | [^E] |
| naturalWonderCountAddedConstant| Float | 0.1 | [^E] |
| ancientRuinCountMultiplier| Float | 0.02 | [^F] |
@ -134,6 +137,26 @@ Legend:
- [^F]: MapGenerator.spreadAncientRuins: number of ruins = suitable tile count * this
- [^H]: MapGenerator.spawnLakesAndCoasts: Water bodies up to this tile count become Lakes
- [^I]: RiverGenerator: river frequency and length bounds
- [^J]: A [UnitUpgradeCost](#UnitUpgradeCost) sub-structure.
#### UnitUpgradeCost
These values are not merged individually, only the entire sub-structure is.
| Attribute | Type | Default | Notes |
| --------- | ---- | -------- | ----- |
| base | Float | 10 | |
| perProduction | Float | 2 | |
| eraMultiplier | Float | 0 | |
| exponent | Float | 1 | |
| roundTo | Int | 5 | |
The formula for the gold cost of a unit upgrade is (rounded down to a multiple of `roundTo`):
( max((`base` + `perProduction` * (new_unit_cost - old_unit_cost)), 0)
* (1 + eraNumber * `eraMultiplier`) * `civModifier`
) ^ `exponent`
With `civModifier` being the multiplicative aggregate of ["\[relativeAmount\]% Gold cost of upgrading"](../uniques.md#global_uniques) uniques that apply.
## VictoryTypes.json