Great General typed uniques and improved moddability (#6818)

* Great General UniqueTyped and improved moddability - draft

* Great General UniqueTyped and improved moddability - reviews

* Great General UniqueTyped and improved moddability - no reason not to cache another

* Integration with JackRainy's solution

* Integration with JackRainy's solution - part 2

* Revert of maxGreatGeneralBonusRadius logic

* Minor refactoring

* Code review: minor refactoring

* Keep the warning for the modders about the obsolete unique

* Code review: Better wording for the unique

Co-authored-by: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com>
This commit is contained in:
Jack Rainy 2022-05-18 00:02:53 +03:00 committed by GitHub
parent 7079619fe2
commit 4986505363
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 192 additions and 83 deletions

View File

@ -1634,7 +1634,7 @@
{ {
"name": "Great General", "name": "Great General",
"unitType": "Civilian", "unitType": "Civilian",
"uniques": ["Can start an [8]-turn golden age", "Bonus for units in 2 tile radius 15%", "Can construct [Citadel]", "uniques": ["Can start an [8]-turn golden age", "[+15]% Strength bonus for [Military] units within [2] tiles", "Can construct [Citadel]",
"Great Person - [War]", "Unbuildable", "Uncapturable"], "Great Person - [War]", "Unbuildable", "Uncapturable"],
"movement": 2 "movement": 2
}, },
@ -1643,7 +1643,7 @@
"unitType": "Civilian", "unitType": "Civilian",
"uniqueTo": "Mongolia", "uniqueTo": "Mongolia",
"replaces": "Great General", "replaces": "Great General",
"uniques": ["Can start an [8]-turn golden age","Bonus for units in 2 tile radius 15%", "uniques": ["Can start an [8]-turn golden age","[+15]% Strength bonus for [Military] units within [2] tiles",
"All adjacent units heal [+15] HP when healing", "[+15] HP when healing", "Can construct [Citadel]", "Great Person - [War]", "Unbuildable", "Uncapturable"], "All adjacent units heal [+15] HP when healing", "[+15] HP when healing", "Can construct [Citadel]", "Great Person - [War]", "Unbuildable", "Uncapturable"],
"movement": 5 "movement": 5
}, },

View File

@ -1310,7 +1310,7 @@
{ {
"name": "Great General", "name": "Great General",
"unitType": "Civilian", "unitType": "Civilian",
"uniques": ["Can start an [8]-turn golden age", "Bonus for units in 2 tile radius 15%", "Can construct [Citadel]", "uniques": ["Can start an [8]-turn golden age", "[+15]% Strength bonus for [Military] units within [2] tiles", "Can construct [Citadel]",
"Great Person - [War]", "Unbuildable", "Uncapturable"], "Great Person - [War]", "Unbuildable", "Uncapturable"],
"movement": 2 "movement": 2
}, },
@ -1319,7 +1319,7 @@
"unitType": "Civilian", "unitType": "Civilian",
"uniqueTo": "Mongolia", "uniqueTo": "Mongolia",
"replaces": "Great General", "replaces": "Great General",
"uniques": ["Can start an [8]-turn golden age","Bonus for units in 2 tile radius 15%", "uniques": ["Can start an [8]-turn golden age", "[+15]% Strength bonus for [Military] units within [2] tiles",
"All adjacent units heal [+15] HP when healing", "[+15] HP when healing", "Can construct [Citadel]", "Great Person - [War]", "Unbuildable", "Uncapturable"], "All adjacent units heal [+15] HP when healing", "[+15] HP when healing", "Can construct [Citadel]", "Great Person - [War]", "Unbuildable", "Uncapturable"],
"movement": 5 "movement": 5
}, },

View File

@ -144,6 +144,14 @@ object BackwardCompatibility {
unit.promotions.addPromotion(startingPromo, true) unit.promotions.addPromotion(startingPromo, true)
} }
/** Upgrade the uniques from deprecated format to the new more general one **/
fun GameInfo.updateGreatGeneralUniques() {
ruleSet.units.values.filter { it.uniques.contains("Bonus for units in 2 tile radius 15%") }.forEach {
it.uniques.remove("Bonus for units in 2 tile radius 15%")
it.uniques.add("[+15]% Strength bonus for [Military] units within [2] tiles")
}
}
/** Move max XP from barbarians to new home */ /** Move max XP from barbarians to new home */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun ModOptions.updateDeprecations() { fun ModOptions.updateDeprecations() {

View File

@ -6,6 +6,7 @@ import com.unciv.logic.BackwardCompatibility.guaranteeUnitPromotions
import com.unciv.logic.BackwardCompatibility.migrateBarbarianCamps import com.unciv.logic.BackwardCompatibility.migrateBarbarianCamps
import com.unciv.logic.BackwardCompatibility.migrateSeenImprovements import com.unciv.logic.BackwardCompatibility.migrateSeenImprovements
import com.unciv.logic.BackwardCompatibility.removeMissingModReferences import com.unciv.logic.BackwardCompatibility.removeMissingModReferences
import com.unciv.logic.BackwardCompatibility.updateGreatGeneralUniques
import com.unciv.logic.automation.NextTurnAutomation import com.unciv.logic.automation.NextTurnAutomation
import com.unciv.logic.civilization.* import com.unciv.logic.civilization.*
import com.unciv.logic.city.CityInfo import com.unciv.logic.city.CityInfo
@ -417,6 +418,7 @@ class GameInfo {
removeMissingModReferences() removeMissingModReferences()
updateGreatGeneralUniques()
for (baseUnit in ruleSet.units.values) for (baseUnit in ruleSet.units.values)
baseUnit.ruleset = ruleSet baseUnit.ruleset = ruleSet

View File

@ -163,8 +163,7 @@ object NextTurnAutomation {
while (delta > 0) { while (delta > 0) {
// Now remove the best offer valued below delta until the deal is barely acceptable // Now remove the best offer valued below delta until the deal is barely acceptable
val offerToRemove = counterofferAsks.filter { it.value <= delta }.maxByOrNull { it.value } val offerToRemove = counterofferAsks.filter { it.value <= delta }.maxByOrNull { it.value }
if (offerToRemove == null) ?: break // Nothing more can be removed, at least en bloc
break // Nothing more can be removed, at least en bloc
delta -= offerToRemove.value delta -= offerToRemove.value
counterofferAsks.remove(offerToRemove.key) counterofferAsks.remove(offerToRemove.key)
} }
@ -831,7 +830,7 @@ object NextTurnAutomation {
when { when {
unit.baseUnit.isRanged() -> rangedUnits.add(unit) unit.baseUnit.isRanged() -> rangedUnits.add(unit)
unit.baseUnit.isMelee() -> meleeUnits.add(unit) unit.baseUnit.isMelee() -> meleeUnits.add(unit)
unit.hasUnique("Bonus for units in 2 tile radius 15%") unit.isGreatPersonOfType("War")
-> generals.add(unit) // Generals move after military units -> generals.add(unit) // Generals move after military units
else -> civilianUnits.add(unit) else -> civilianUnits.add(unit)
} }

View File

@ -1,6 +1,7 @@
package com.unciv.logic.automation package com.unciv.logic.automation
import com.unciv.logic.battle.Battle import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.GreatGeneralImplementation
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.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
@ -45,28 +46,16 @@ object SpecificUnitAutomation {
} }
} }
fun automateGreatGeneral(unit: MapUnit) { fun automateGreatGeneral(unit: MapUnit): Boolean {
//try to follow nearby units. Do not garrison in city if possible //try to follow nearby units. Do not garrison in city if possible
val militaryUnitTilesInDistance = unit.movement.getDistanceToTiles().asSequence() val maxAffectedTroopsTile = GreatGeneralImplementation.getBestAffectedTroopsTile(unit)
.filter { ?: return false
val militant = it.key.militaryUnit
militant != null && militant.civInfo == unit.civInfo
&& (it.key.civilianUnit == null || it.key.civilianUnit == unit)
&& militant.getMaxMovement() <= 2 && !it.key.isCityCenter()
}
val maxAffectedTroopsTile = militaryUnitTilesInDistance unit.movement.headTowards(maxAffectedTroopsTile)
.maxByOrNull { return true
it.key.getTilesInDistance(2).count { tile -> }
val militaryUnit = tile.militaryUnit
militaryUnit != null && militaryUnit.civInfo == unit.civInfo
}
}?.key
if (maxAffectedTroopsTile != null) {
unit.movement.headTowards(maxAffectedTroopsTile)
return
}
fun automateCitadelPlacer(unit: MapUnit): Boolean {
// try to revenge and capture their tiles // try to revenge and capture their tiles
val enemyCities = unit.civInfo.getKnownCivs() val enemyCities = unit.civInfo.getKnownCivs()
.filter { unit.civInfo.getDiplomacyManager(it).hasModifier(DiplomaticModifiers.StealingTerritory) } .filter { unit.civInfo.getDiplomacyManager(it).hasModifier(DiplomaticModifiers.StealingTerritory) }
@ -94,16 +83,19 @@ object SpecificUnitAutomation {
unit.movement.headTowards(tileToSteal) unit.movement.headTowards(tileToSteal)
if (unit.currentMovement > 0 && unit.currentTile == tileToSteal) if (unit.currentMovement > 0 && unit.currentTile == tileToSteal)
UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke() UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke()
return return true
} }
// try to build a citadel for defensive purposes // try to build a citadel for defensive purposes
if (WorkerAutomation.evaluateFortPlacement(unit.currentTile, unit.civInfo, true)) { if (WorkerAutomation.evaluateFortPlacement(unit.currentTile, unit.civInfo, true)) {
UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke() UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke()
return return true
} }
return false
}
//if no unit to follow, take refuge in city or build citadel there. fun automateGreatGeneralFallback(unit: MapUnit) {
// if no unit to follow, take refuge in city or build citadel there.
val reachableTest: (TileInfo) -> Boolean = { val reachableTest: (TileInfo) -> Boolean = {
it.civilianUnit == null && it.civilianUnit == null &&
unit.movement.canMoveTo(it) unit.movement.canMoveTo(it)
@ -112,22 +104,26 @@ object SpecificUnitAutomation {
val cityToGarrison = unit.civInfo.cities.asSequence().map { it.getCenterTile() } val cityToGarrison = unit.civInfo.cities.asSequence().map { it.getCenterTile() }
.sortedBy { it.aerialDistanceTo(unit.currentTile) } .sortedBy { it.aerialDistanceTo(unit.currentTile) }
.firstOrNull { reachableTest(it) } .firstOrNull { reachableTest(it) }
?: return
if (cityToGarrison != null) { if (!unit.hasCitadelPlacementUnique) {
// try to find a good place for citadel nearby unit.movement.headTowards(cityToGarrison)
val potentialTilesNearCity = cityToGarrison.getTilesInDistanceRange(3..4)
val tileForCitadel = potentialTilesNearCity.firstOrNull {
reachableTest(it) &&
WorkerAutomation.evaluateFortPlacement(it, unit.civInfo, true)
}
if (tileForCitadel != null) {
unit.movement.headTowards(tileForCitadel)
if (unit.currentMovement > 0 && unit.currentTile == tileForCitadel)
UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke()
} else
unit.movement.headTowards(cityToGarrison)
return return
} }
// try to find a good place for citadel nearby
val tileForCitadel = cityToGarrison.getTilesInDistanceRange(3..4)
.firstOrNull {
reachableTest(it) &&
WorkerAutomation.evaluateFortPlacement(it, unit.civInfo, true)
}
if (tileForCitadel == null) {
unit.movement.headTowards(cityToGarrison)
return
}
unit.movement.headTowards(tileForCitadel)
if (unit.currentMovement > 0 && unit.currentTile == tileForCitadel)
UnitActions.getImprovementConstructionActions(unit, unit.currentTile)
.firstOrNull()?.action?.invoke()
} }
private fun rankTileAsCityCenter(tileInfo: TileInfo, nearbyTileRankings: Map<TileInfo, Float>, private fun rankTileAsCityCenter(tileInfo: TileInfo, nearbyTileRankings: Map<TileInfo, Float>,

View File

@ -157,9 +157,15 @@ object UnitAutomation {
// For now its a simple option to allow AI to win a science victory again // For now its a simple option to allow AI to win a science victory again
if (unit.hasUnique(UniqueType.AddInCapital)) if (unit.hasUnique(UniqueType.AddInCapital))
return SpecificUnitAutomation.automateAddInCapital(unit) return SpecificUnitAutomation.automateAddInCapital(unit)
if (unit.hasUnique("Bonus for units in 2 tile radius 15%")) //todo this now supports "Great General"-like mod units not combining 'aura' and citadel
return SpecificUnitAutomation.automateGreatGeneral(unit) // abilities, but not additional capabilities if automation finds no use for those two
if (unit.hasStrengthBonusInRadiusUnique && SpecificUnitAutomation.automateGreatGeneral(unit))
return
if (unit.hasCitadelPlacementUnique && SpecificUnitAutomation.automateCitadelPlacer(unit))
return
if (unit.hasCitadelPlacementUnique || unit.hasStrengthBonusInRadiusUnique)
return SpecificUnitAutomation.automateGreatGeneralFallback(unit)
if (unit.hasUnique(UniqueType.ConstructImprovementConsumingUnit)) if (unit.hasUnique(UniqueType.ConstructImprovementConsumingUnit))
return SpecificUnitAutomation.automateImprovementPlacer(unit) // includes great people plus moddable units return SpecificUnitAutomation.automateImprovementPlacer(unit) // includes great people plus moddable units

View File

@ -40,10 +40,9 @@ object BattleDamage {
val conditionalState = StateForConditionals(civInfo, cityInfo = (combatant as? CityCombatant)?.city, ourCombatant = combatant, theirCombatant = enemy, val conditionalState = StateForConditionals(civInfo, cityInfo = (combatant as? CityCombatant)?.city, ourCombatant = combatant, theirCombatant = enemy,
attackedTile = attackedTile, combatAction = combatAction) attackedTile = attackedTile, combatAction = combatAction)
if (combatant is MapUnitCombatant) { if (combatant is MapUnitCombatant) {
for (unique in combatant.getMatchingUniques(UniqueType.Strength, conditionalState, true)) { for (unique in combatant.getMatchingUniques(UniqueType.Strength, conditionalState, true)) {
modifiers.add(getModifierStringFromUnique(unique), unique.params[0].toInt()) modifiers.add(getModifierStringFromUnique(unique), unique.params[0].toInt())
} }
@ -61,9 +60,7 @@ object BattleDamage {
//https://www.carlsguides.com/strategy/civilization5/war/combatbonuses.php //https://www.carlsguides.com/strategy/civilization5/war/combatbonuses.php
val adjacentUnits = combatant.getTile().neighbors.flatMap { it.getUnits() } val adjacentUnits = combatant.getTile().neighbors.flatMap { it.getUnits() }
for (unique in adjacentUnits.filter { it.civInfo.isAtWarWith(civInfo) }
for (unique in adjacentUnits.filter { it.civInfo.isAtWarWith(combatant.getCivInfo()) }
.flatMap { it.getMatchingUniques(UniqueType.StrengthForAdjacentEnemies) }) .flatMap { it.getMatchingUniques(UniqueType.StrengthForAdjacentEnemies) })
if (combatant.matchesCategory(unique.params[1]) && combatant.getTile() if (combatant.matchesCategory(unique.params[1]) && combatant.getTile()
.matchesFilter(unique.params[2]) .matchesFilter(unique.params[2])
@ -73,17 +70,11 @@ object BattleDamage {
val civResources = civInfo.getCivResourcesByName() val civResources = civInfo.getCivResourcesByName()
for (resource in combatant.unit.baseUnit.getResourceRequirements().keys) for (resource in combatant.unit.baseUnit.getResourceRequirements().keys)
if (civResources[resource]!! < 0 && !civInfo.isBarbarian()) if (civResources[resource]!! < 0 && !civInfo.isBarbarian())
modifiers["Missing resource"] = -25 modifiers["Missing resource"] = -25 //todo ModConstants
val (greatGeneralName, greatGeneralBonus) = GreatGeneralImplementation.getGreatGeneralBonus(combatant.unit)
val nearbyCivUnits = combatant.unit.getTile().getTilesInDistance(2) if (greatGeneralBonus != 0)
.flatMap { it.getUnits() }.filter { it.civInfo == combatant.unit.civInfo } modifiers[greatGeneralName] = greatGeneralBonus
if (nearbyCivUnits.any { it.hasUnique("Bonus for units in 2 tile radius 15%") }) {
val greatGeneralModifier =
if (combatant.unit.civInfo.hasUnique(UniqueType.GreatGeneralProvidesDoubleCombatBonus)) 30 else 15
modifiers["Great General"] = greatGeneralModifier
}
for (unique in combatant.unit.getMatchingUniques(UniqueType.StrengthWhenStacked)) { for (unique in combatant.unit.getMatchingUniques(UniqueType.StrengthWhenStacked)) {
var stackedUnitsBonus = 0 var stackedUnitsBonus = 0

View File

@ -0,0 +1,98 @@
package com.unciv.logic.battle
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.logic.automation.SpecificUnitAutomation // for Kdoc
object GreatGeneralImplementation {
private data class GeneralBonusData(val general: MapUnit, val radius: Int, val filter: String, val bonus: Int) {
constructor(general: MapUnit, unique: Unique) : this(
general,
radius = unique.params[2].toIntOrNull() ?: 0,
filter = unique.params[1],
bonus = unique.params[0].toIntOrNull() ?: 0
)
}
/**
* Determine the "Great General" bonus for [unit] by searching for units carrying the [UniqueType.StrengthBonusInRadius] in the vicinity.
*
* Used by [BattleDamage.getGeneralModifiers].
*
* @return A pair of unit's name and bonus (percentage) as Int (typically 15), or 0 if no applicable Great General equivalents found
*/
fun getGreatGeneralBonus(unit: MapUnit): Pair<String, Int> {
val civInfo = unit.civInfo
val allGenerals = civInfo.getCivUnits()
.filter { it.hasStrengthBonusInRadiusUnique }
if (allGenerals.none()) return Pair("", 0)
val greatGeneral = allGenerals
.flatMap { general ->
general.getMatchingUniques(UniqueType.StrengthBonusInRadius)
.map { GeneralBonusData(general, it) }
}.filter {
// Support the border case when a mod unit has several
// GreatGeneralAura uniques (e.g. +50% as radius 1, +25% at radius 2, +5% at radius 3)
// The "Military" test is also supported deep down in unit.matchesFilter, a small
// optimization for the most common case, as this function is only called for `MapUnitCombatant`s
it.general.currentTile.aerialDistanceTo(unit.getTile()) <= it.radius
&& (it.filter == "Military" || unit.matchesFilter(it.filter))
}
val greatGeneralModifier = greatGeneral.maxByOrNull { it.bonus } ?: return Pair("",0)
if (unit.hasUnique(UniqueType.GreatGeneralProvidesDoubleCombatBonus, checkCivInfoUniques = true)
&& greatGeneralModifier.general.isGreatPersonOfType("War")) // apply only on "true" generals
return Pair(greatGeneralModifier.general.name, greatGeneralModifier.bonus * 2)
return Pair(greatGeneralModifier.general.name, greatGeneralModifier.bonus)
}
/**
* Find a tile for accompanying a military unit where the total bonus for all affected units is maximized.
*
* Used by [SpecificUnitAutomation.automateGreatGeneral].
*/
fun getBestAffectedTroopsTile(general: MapUnit): TileInfo? {
// Normally we have only one Unique here. But a mix is not forbidden, so let's try to support mad modders.
// (imagine several GreatGeneralAura uniques - +50% at radius 1, +25% at radius 2, +5% at radius 3 - possibly learnable from promotions via buildings or natural wonders?)
// Map out the uniques sorted by bonus, as later only the best bonus will apply.
val generalBonusData = (
general.getMatchingUniques(UniqueType.StrengthBonusInRadius).map { GeneralBonusData(general, it) }
).sortedWith(compareByDescending<GeneralBonusData> { it.bonus }.thenBy { it.radius })
.toList()
// Get candidate units to 'follow', coarsely.
// The mapUnitFilter of the unique won't apply here but in the ranking of the "Aura" effectiveness.
val unitMaxMovement = general.getMaxMovement()
val militaryUnitTilesInDistance = general.movement.getDistanceToTiles().asSequence()
.map { it.key }
.filter { tile ->
val militaryUnit = tile.militaryUnit
militaryUnit != null && militaryUnit.civInfo == general.civInfo
&& (tile.civilianUnit == null || tile.civilianUnit == general)
&& militaryUnit.getMaxMovement() <= unitMaxMovement
&& !tile.isCityCenter()
}
// rank tiles and find best
val unitBonusRadius = generalBonusData.maxOfOrNull { it.radius }
?: return null
return militaryUnitTilesInDistance
.maxByOrNull { unitTile ->
unitTile.getTilesInDistance(unitBonusRadius).sumOf { auraTile ->
val militaryUnit = auraTile.militaryUnit
if (militaryUnit == null || militaryUnit.civInfo != general.civInfo) 0
else generalBonusData.firstOrNull {
// "Military" as commented above only a small optimization
auraTile.aerialDistanceTo(unitTile) <= it.radius
&& (it.filter == "Military" || militaryUnit.matchesFilter(it.filter))
}?.bonus ?: 0
}
}
}
}

View File

@ -1,15 +1,9 @@
package com.unciv.logic.civilization package com.unciv.logic.civilization
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.Json.Serializer
import com.badlogic.gdx.utils.JsonValue
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.json.HashMapVector2 import com.unciv.json.HashMapVector2
import com.unciv.json.json
import com.unciv.logic.BarbarianManager
import com.unciv.logic.Encampment
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.UncivShowableException import com.unciv.logic.UncivShowableException
import com.unciv.logic.automation.NextTurnAutomation import com.unciv.logic.automation.NextTurnAutomation
@ -123,7 +117,7 @@ class CivilizationInfo {
@Transient @Transient
val lastEraResourceUsedForUnit = HashMap<String, Int>() val lastEraResourceUsedForUnit = HashMap<String, Int>()
@Transient @Transient
var thingsToFocusOnForVictory = setOf<Victory.Focus>() var thingsToFocusOnForVictory = setOf<Victory.Focus>()
@ -757,7 +751,7 @@ class CivilizationInfo {
goldenAges.civInfo = this goldenAges.civInfo = this
civConstructions.setTransients(civInfo = this) civConstructions.setTransients(civInfo = this)
policies.civInfo = this policies.civInfo = this
if (policies.adoptedPolicies.size > 0 && policies.numberOfAdoptedPolicies == 0) if (policies.adoptedPolicies.size > 0 && policies.numberOfAdoptedPolicies == 0)
policies.numberOfAdoptedPolicies = policies.adoptedPolicies.count { !Policy.isBranchCompleteByName(it) } policies.numberOfAdoptedPolicies = policies.adoptedPolicies.count { !Policy.isBranchCompleteByName(it) }
@ -776,14 +770,14 @@ class CivilizationInfo {
tech.setTransients() tech.setTransients()
ruinsManager.setTransients(this) ruinsManager.setTransients(this)
for (diplomacyManager in diplomacy.values) { for (diplomacyManager in diplomacy.values) {
diplomacyManager.civInfo = this diplomacyManager.civInfo = this
diplomacyManager.updateHasOpenBorders() diplomacyManager.updateHasOpenBorders()
} }
victoryManager.civInfo = this victoryManager.civInfo = this
thingsToFocusOnForVictory = getPreferredVictoryTypeObject()?.getThingsToFocus(this) ?: setOf() thingsToFocusOnForVictory = getPreferredVictoryTypeObject()?.getThingsToFocus(this) ?: setOf()
for (cityInfo in cities) { for (cityInfo in cities) {
@ -811,6 +805,7 @@ class CivilizationInfo {
} }
hasLongCountDisplayUnique = hasUnique(UniqueType.MayanCalendarDisplay) hasLongCountDisplayUnique = hasUnique(UniqueType.MayanCalendarDisplay)
} }
fun updateSightAndResources() { fun updateSightAndResources() {

View File

@ -23,7 +23,6 @@ import com.unciv.models.ruleset.unit.UnitType
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.ui.utils.filterAndLogic import com.unciv.ui.utils.filterAndLogic
import com.unciv.ui.utils.toPercent import com.unciv.ui.utils.toPercent
import com.unciv.ui.worldscreen.unit.UnitActions
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.pow import kotlin.math.pow
@ -120,6 +119,11 @@ class MapUnit {
@Transient @Transient
var hasUniqueToBuildImprovements = false // not canBuildImprovements to avoid confusion var hasUniqueToBuildImprovements = false // not canBuildImprovements to avoid confusion
@Transient
var hasStrengthBonusInRadiusUnique = false
@Transient
var hasCitadelPlacementUnique = false
/** civName owning the unit */ /** civName owning the unit */
lateinit var owner: String lateinit var owner: String
@ -340,6 +344,11 @@ class MapUnit {
hasUniqueToBuildImprovements = hasUnique(UniqueType.BuildImprovements) hasUniqueToBuildImprovements = hasUnique(UniqueType.BuildImprovements)
canEnterForeignTerrain = hasUnique(UniqueType.CanEnterForeignTiles) canEnterForeignTerrain = hasUnique(UniqueType.CanEnterForeignTiles)
|| hasUnique(UniqueType.CanEnterForeignTilesButLosesReligiousStrength) || hasUnique(UniqueType.CanEnterForeignTilesButLosesReligiousStrength)
hasStrengthBonusInRadiusUnique = hasUnique(UniqueType.StrengthBonusInRadius)
hasCitadelPlacementUnique = getMatchingUniques(UniqueType.ConstructImprovementConsumingUnit)
.mapNotNull { civInfo.gameInfo.ruleSet.tileImprovements[it.params[0]] }
.any { it.hasUnique(UniqueType.TakeOverTilesAroundWhenBuilt) }
} }
fun copyStatisticsTo(newUnit: MapUnit) { fun copyStatisticsTo(newUnit: MapUnit) {
@ -568,6 +577,7 @@ class MapUnit {
fun canGarrison() = isMilitary() && baseUnit.isLandUnit() fun canGarrison() = isMilitary() && baseUnit.isLandUnit()
fun isGreatPerson() = baseUnit.isGreatPerson() fun isGreatPerson() = baseUnit.isGreatPerson()
fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type)
//endregion //endregion

View File

@ -73,7 +73,7 @@ class UnitPromotions {
// If we upgrade this unit to its new version, we already need to have this promotion added, // If we upgrade this unit to its new version, we already need to have this promotion added,
// so this has to go after the `promotions.add(promotionname)` line. // so this has to go after the `promotions.add(promotionname)` line.
doDirectPromotionEffects(promotion) doDirectPromotionEffects(promotion)
unit.updateUniques(ruleset) unit.updateUniques(ruleset)
// Since some units get promotions upon construction, they will get the addPromotion from the unit.postBuildEvent // Since some units get promotions upon construction, they will get the addPromotion from the unit.postBuildEvent

View File

@ -632,7 +632,7 @@ class Ruleset {
if (tileImprovements[improvementName]==null) continue // this will be caught in the checkUniques if (tileImprovements[improvementName]==null) continue // this will be caught in the checkUniques
if ((tileImprovements[improvementName] as Stats).none() && if ((tileImprovements[improvementName] as Stats).none() &&
unit.isCivilian() && unit.isCivilian() &&
!unit.hasUnique("Bonus for units in 2 tile radius 15%")) { !unit.isGreatPersonOfType("War")) {
lines.add("${unit.name} can place improvement $improvementName which has no stats, preventing unit automation!", lines.add("${unit.name} can place improvement $improvementName which has no stats, preventing unit automation!",
RulesetErrorSeverity.Warning) RulesetErrorSeverity.Warning)
} }

View File

@ -232,7 +232,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
UnitsFightFullStrengthWhenDamaged("Units fight as though they were at full strength even when damaged", UniqueTarget.Global), UnitsFightFullStrengthWhenDamaged("Units fight as though they were at full strength even when damaged", UniqueTarget.Global),
GoldWhenDiscoveringNaturalWonder("100 Gold for discovering a Natural Wonder (bonus enhanced to 500 Gold if first to discover it)", UniqueTarget.Global), GoldWhenDiscoveringNaturalWonder("100 Gold for discovering a Natural Wonder (bonus enhanced to 500 Gold if first to discover it)", UniqueTarget.Global),
UnhappinessFromCitiesDoubled("Unhappiness from number of Cities doubled", UniqueTarget.Global), UnhappinessFromCitiesDoubled("Unhappiness from number of Cities doubled", UniqueTarget.Global),
GreatGeneralProvidesDoubleCombatBonus("Great General provides double combat bonus", UniqueTarget.Global), GreatGeneralProvidesDoubleCombatBonus("Great General provides double combat bonus", UniqueTarget.Unit, UniqueTarget.Global),
TechBoostWhenScientificBuildingsBuiltInCapital("Receive a tech boost when scientific buildings/wonders are built in capital", UniqueTarget.Global), TechBoostWhenScientificBuildingsBuiltInCapital("Receive a tech boost when scientific buildings/wonders are built in capital", UniqueTarget.Global),
MayNotGenerateGreatProphet("May not generate great prophet equivalents naturally", UniqueTarget.Global), MayNotGenerateGreatProphet("May not generate great prophet equivalents naturally", UniqueTarget.Global),
@Deprecated("as of 4.0.3", ReplaceWith("When conquering an encampment, earn [25] Gold and recruit a Barbarian unit <with [67]% chance>")) @Deprecated("as of 4.0.3", ReplaceWith("When conquering an encampment, earn [25] Gold and recruit a Barbarian unit <with [67]% chance>"))
@ -413,6 +413,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
StrengthBonusVsCityStates("+30% Strength when fighting City-State units and cities", UniqueTarget.Global), StrengthBonusVsCityStates("+30% Strength when fighting City-State units and cities", UniqueTarget.Global),
StrengthForAdjacentEnemies("[relativeAmount]% Strength for enemy [combatantFilter] units in adjacent [tileFilter] tiles", UniqueTarget.Unit), StrengthForAdjacentEnemies("[relativeAmount]% Strength for enemy [combatantFilter] units in adjacent [tileFilter] tiles", UniqueTarget.Unit),
StrengthWhenStacked("[relativeAmount]% Strength when stacked with [mapUnitFilter]", UniqueTarget.Unit), // candidate for conditional! StrengthWhenStacked("[relativeAmount]% Strength when stacked with [mapUnitFilter]", UniqueTarget.Unit), // candidate for conditional!
StrengthBonusInRadius("[relativeAmount]% Strength bonus for [mapUnitFilter] units within [amount] tiles", UniqueTarget.Unit),
AdditionalAttacks("[amount] additional attacks per turn", UniqueTarget.Unit, UniqueTarget.Global), AdditionalAttacks("[amount] additional attacks per turn", UniqueTarget.Unit, UniqueTarget.Global),
Movement("[amount] Movement", UniqueTarget.Unit, UniqueTarget.Global), Movement("[amount] Movement", UniqueTarget.Unit, UniqueTarget.Global),
@ -529,8 +530,6 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
RemoveOtherReligions("Removes other religions when spreading religion", UniqueTarget.Unit), RemoveOtherReligions("Removes other religions when spreading religion", UniqueTarget.Unit),
CanActionSeveralTimes("Can [action] [amount] times", UniqueTarget.Unit), CanActionSeveralTimes("Can [action] [amount] times", UniqueTarget.Unit),
// TODO needs to be more general
BonusForUnitsInRadius("Bonus for units in 2 tile radius 15%", UniqueTarget.Unit),
CanSpeedupConstruction("Can speed up construction of a building", UniqueTarget.Unit), CanSpeedupConstruction("Can speed up construction of a building", UniqueTarget.Unit),
CanHurryResearch("Can hurry technology research", UniqueTarget.Unit), CanHurryResearch("Can hurry technology research", UniqueTarget.Unit),
@ -761,6 +760,8 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
// region DEPRECATED AND REMOVED // region DEPRECATED AND REMOVED
@Deprecated("as of 4.1.0", ReplaceWith("[+15]% Strength bonus for [Military] units within [2] tiles"), DeprecationLevel.ERROR)
BonusForUnitsInRadius("Bonus for units in 2 tile radius 15%", UniqueTarget.Unit),
@Deprecated("as of 4.0.15", ReplaceWith("Irremovable"), DeprecationLevel.ERROR) @Deprecated("as of 4.0.15", ReplaceWith("Irremovable"), DeprecationLevel.ERROR)
Indestructible("Indestructible", UniqueTarget.Improvement), Indestructible("Indestructible", UniqueTarget.Improvement),

View File

@ -577,7 +577,8 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
} }
} }
fun isGreatPerson() = hasUnique(UniqueType.GreatPerson) fun isGreatPerson() = getMatchingUniques(UniqueType.GreatPerson).any()
fun isGreatPersonOfType(type: String) = getMatchingUniques(UniqueType.GreatPerson).any { it.params[0] == type }
fun isNuclearWeapon() = hasUnique(UniqueType.NuclearWeapon) fun isNuclearWeapon() = hasUnique(UniqueType.NuclearWeapon)

View File

@ -458,7 +458,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Global Applicable to: Global
??? example "Great General provides double combat bonus" ??? example "Great General provides double combat bonus"
Applicable to: Global Applicable to: Global, Unit
??? example "Receive a tech boost when scientific buildings/wonders are built in capital" ??? example "Receive a tech boost when scientific buildings/wonders are built in capital"
Applicable to: Global Applicable to: Global
@ -998,6 +998,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Unit Applicable to: Unit
??? example "[relativeAmount]% Strength bonus for [mapUnitFilter] units in [amount] tiles"
Example: "[+20]% Strength bonus for [Wounded] units in [3] tiles"
Applicable to: Unit
??? example "May found a religion" ??? example "May found a religion"
Applicable to: Unit Applicable to: Unit
@ -1214,9 +1219,6 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Unit Applicable to: Unit
??? example "Bonus for units in 2 tile radius 15%"
Applicable to: Unit
??? example "Can speed up construction of a building" ??? example "Can speed up construction of a building"
Applicable to: Unit Applicable to: Unit