diff --git a/android/assets/jsons/Civ V - Gods & Kings/Units.json b/android/assets/jsons/Civ V - Gods & Kings/Units.json index 802dc4d2f6..3f0670ced8 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/Units.json +++ b/android/assets/jsons/Civ V - Gods & Kings/Units.json @@ -1653,6 +1653,7 @@ "Empire enters a [8]-turn Golden Age ", "[+15]% Strength bonus for [Military] units within [2] tiles", "Can instantly construct a [Citadel] improvement ", + "Can be earned through combat", "Great Person - [War]", "Unbuildable", "Uncapturable"], "movement": 2 }, @@ -1666,6 +1667,7 @@ "[+15]% Strength bonus for [Military] units within [2] tiles", "All adjacent units heal [+15] HP when healing", "[+15] HP when healing", "Can instantly construct a [Citadel] improvement ", + "Can be earned through combat", "Great Person - [War]", "Unbuildable", "Uncapturable"], "movement": 5 }, diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index c43df5f6d3..1bd2806ac2 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -1296,6 +1296,7 @@ "Empire enters a [8]-turn Golden Age ", "[+15]% Strength bonus for [Military] units within [2] tiles", "Can instantly construct a [Citadel] improvement ", + "Can be earned through combat", "Great Person - [War]", "Unbuildable", "Uncapturable"], "movement": 2 }, @@ -1309,6 +1310,7 @@ "[+15]% Strength bonus for [Military] units within [2] tiles", "All adjacent units heal [+15] HP when healing", "[+15] HP when healing", "Can instantly construct a [Citadel] improvement ", + "Can be earned through combat", "Great Person - [War]", "Unbuildable", "Uncapturable"], "movement": 5 }, diff --git a/core/src/com/unciv/logic/BackwardCompatibility.kt b/core/src/com/unciv/logic/BackwardCompatibility.kt index 238e44f519..1b03bdfd9f 100644 --- a/core/src/com/unciv/logic/BackwardCompatibility.kt +++ b/core/src/com/unciv/logic/BackwardCompatibility.kt @@ -37,6 +37,19 @@ object BackwardCompatibility { removeTechAndPolicies() } + fun GameInfo.migrateGreatGeneralPools() { + for (civ in civilizations) civ.greatPeople.run { + if (pointsForNextGreatGeneral >= pointsForNextGreatGeneralCounter["Great General"]) { + pointsForNextGreatGeneralCounter["Great General"] = pointsForNextGreatGeneral + } else pointsForNextGreatGeneral = pointsForNextGreatGeneralCounter["Great General"] + + + if (greatGeneralPoints >= greatGeneralPointsCounter["Great General"]) { + greatGeneralPointsCounter["Great General"] = greatGeneralPoints + } else greatGeneralPoints = greatGeneralPointsCounter["Great General"] + } + } + private fun GameInfo.removeUnitsAndPromotions() { for (tile in tileMap.values) { for (unit in tile.getUnits().toList()) { diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 51d83060c7..2a1fed890a 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -7,6 +7,7 @@ import com.unciv.UncivGame.Version import com.unciv.json.json import com.unciv.logic.BackwardCompatibility.convertFortify import com.unciv.logic.BackwardCompatibility.guaranteeUnitPromotions +import com.unciv.logic.BackwardCompatibility.migrateGreatGeneralPools import com.unciv.logic.BackwardCompatibility.migrateToTileHistory import com.unciv.logic.BackwardCompatibility.removeMissingModReferences import com.unciv.logic.GameInfo.Companion.CURRENT_COMPATIBILITY_NUMBER @@ -670,6 +671,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion guaranteeUnitPromotions() migrateToTileHistory() + + migrateGreatGeneralPools() } private fun updateCivilizationState() { diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 4f7a16cb89..77586b34e5 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -482,19 +482,29 @@ object Battle { promotions.XP += xpGained if (!otherIsBarbarian && civ.isMajorCiv()) { // Can't get great generals from Barbarians - val greatGeneralPointsBonus = thisCombatant - .getMatchingUniques(UniqueType.GreatPersonEarnedFaster, stateForConditionals, true) - .filter { unique -> - val unitName = unique.params[0] - // From the unique we know this unit exists - val unit = civ.gameInfo.ruleset.units[unitName]!! - unit.uniques.contains("Great Person - [War]") - } - .sumOf { it.params[1].toDouble() } - val greatGeneralPointsModifier = 1.0 + greatGeneralPointsBonus / 100 + var greatGeneralUnits = civ.gameInfo.ruleset.greatGeneralUnits + .filter { it.hasUnique(UniqueType.GreatPersonFromCombat, stateForConditionals) && + // Check if the unit is allowed for the Civ, ignoring build constrants + it.getRejectionReasons(civ).none { reason -> + !reason.isConstructionRejection() && + // Allow Generals even if not allowed via tech + !reason.techPolicyEraWonderRequirements() } + }.asSequence() + // For compatibility with older rulesets + if (civ.gameInfo.ruleset.greatGeneralUnits.isEmpty() && + civ.gameInfo.ruleset.units["Great General"] != null) + greatGeneralUnits += civ.gameInfo.ruleset.units["Great General"]!! - val greatGeneralPointsGained = (xpGained * greatGeneralPointsModifier).toInt() - civ.greatPeople.greatGeneralPoints += greatGeneralPointsGained + for (unit in greatGeneralUnits) { + val greatGeneralPointsBonus = thisCombatant + .getMatchingUniques(UniqueType.GreatPersonEarnedFaster, stateForConditionals, true) + .filter { unit.matchesFilter(it.params[0]) } + .sumOf { it.params[1].toDouble() } + val greatGeneralPointsModifier = 1.0 + greatGeneralPointsBonus / 100 + + val greatGeneralPointsGained = (xpGained * greatGeneralPointsModifier).toInt() + civ.greatPeople.greatGeneralPointsCounter[unit.name] += greatGeneralPointsGained + } } if (!thisCombatant.isDefeated() && !unitCouldAlreadyPromote && promotions.canBePromoted()) { diff --git a/core/src/com/unciv/logic/civilization/managers/GreatPersonManager.kt b/core/src/com/unciv/logic/civilization/managers/GreatPersonManager.kt index 85d20be5d5..22a983c830 100644 --- a/core/src/com/unciv/logic/civilization/managers/GreatPersonManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/GreatPersonManager.kt @@ -19,8 +19,10 @@ class GreatPersonManager : IsPartOfGameInfoSerialization { /** Base points, without speed modifier */ var pointsForNextGreatPersonCounter = Counter() // Initial values assigned in getPointsRequiredForGreatPerson as needed var pointsForNextGreatGeneral = 200 + var pointsForNextGreatGeneralCounter = Counter() // Initial values assigned when needed var greatPersonPointsCounter = Counter() + var greatGeneralPointsCounter = Counter() var greatGeneralPoints = 0 var freeGreatPeople = 0 /** Marks subset of [freeGreatPeople] as subject to maya ability restrictions (each only once untill all used) */ @@ -33,6 +35,8 @@ class GreatPersonManager : IsPartOfGameInfoSerialization { toReturn.freeGreatPeople = freeGreatPeople toReturn.greatPersonPointsCounter = greatPersonPointsCounter.clone() toReturn.pointsForNextGreatPersonCounter = pointsForNextGreatPersonCounter.clone() + toReturn.pointsForNextGreatGeneralCounter = pointsForNextGreatGeneralCounter.clone() + toReturn.greatGeneralPointsCounter = greatGeneralPointsCounter.clone() toReturn.pointsForNextGreatGeneral = pointsForNextGreatGeneral toReturn.greatGeneralPoints = greatGeneralPoints toReturn.mayaLimitedFreeGP = mayaLimitedFreeGP @@ -54,10 +58,16 @@ class GreatPersonManager : IsPartOfGameInfoSerialization { } fun getNewGreatPerson(): String? { - if (greatGeneralPoints > pointsForNextGreatGeneral) { - greatGeneralPoints -= pointsForNextGreatGeneral - pointsForNextGreatGeneral += 50 - return "Great General" + for ((unit, value) in greatGeneralPointsCounter){ + if (pointsForNextGreatGeneralCounter[unit] == 0) { + pointsForNextGreatGeneralCounter[unit] = 200 + } + val requiredPoints = pointsForNextGreatGeneralCounter[unit] + if (value > requiredPoints) { + greatGeneralPointsCounter[unit] -= requiredPoints + pointsForNextGreatGeneralCounter[unit] += 50 + return unit + } } for ((greatPerson, value) in greatPersonPointsCounter) { diff --git a/core/src/com/unciv/models/ruleset/IConstruction.kt b/core/src/com/unciv/models/ruleset/IConstruction.kt index ac800b45bc..f03f3393e7 100644 --- a/core/src/com/unciv/models/ruleset/IConstruction.kt +++ b/core/src/com/unciv/models/ruleset/IConstruction.kt @@ -118,6 +118,8 @@ class RejectionReason(val type: RejectionReasonType, fun isImportantRejection(): Boolean = type in orderedImportantRejectionTypes + fun isConstructionRejection(): Boolean = type in constructionRejectionReasonType + /** Returns the index of [orderedImportantRejectionTypes] with the smallest index having the * highest precedence */ fun getRejectionPrecedence(): Int { @@ -152,6 +154,12 @@ class RejectionReason(val type: RejectionReasonType, RejectionReasonType.MaxNumberBuildable, RejectionReasonType.NoPlaceToPutUnit, ) + // Used for units spawned, not built + private val constructionRejectionReasonType = listOf( + RejectionReasonType.Unbuildable, + RejectionReasonType.CannotBeBuiltUnhappiness, + RejectionReasonType.CannotBeBuilt, + ) } diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index fc12baf59a..da14120175 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -15,6 +15,7 @@ import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.unique.IHasUniques +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit @@ -69,6 +70,10 @@ class Ruleset { var victories = LinkedHashMap() var cityStateTypes = LinkedHashMap() + val greatGeneralUnits by lazy { + units.values.filter { it.hasUnique(UniqueType.GreatPersonFromCombat, StateForConditionals.IgnoreConditionals) } + } + val mods = LinkedHashSet() var modOptions = ModOptions() diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 1f91131ed7..b5c0ad619d 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -437,6 +437,7 @@ enum class UniqueType( // XP FlatXPGain("[amount] XP gained from combat", UniqueTarget.Unit, UniqueTarget.Global), PercentageXPGain("[relativeAmount]% XP gained from combat", UniqueTarget.Unit, UniqueTarget.Global), + GreatPersonFromCombat("Can be earned through combat", UniqueTarget.Unit), GreatPersonEarnedFaster("[greatPerson] is earned [relativeAmount]% faster", UniqueTarget.Unit, UniqueTarget.Global), // Invisibility diff --git a/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt b/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt index 4efc995505..1deb48368d 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt @@ -211,10 +211,14 @@ class StatsOverviewTab( add(greatPersonPointsPerTurn[greatPerson].toLabel()).right().row() } - val pointsForGreatGeneral = viewingPlayer.greatPeople.greatGeneralPoints - val pointsForNextGreatGeneral = viewingPlayer.greatPeople.pointsForNextGreatGeneral - add("Great General".toLabel()).left() - add("$pointsForGreatGeneral/$pointsForNextGreatGeneral".toLabel()) + val greatGeneralPoints = viewingPlayer.greatPeople.greatGeneralPointsCounter + val pointsForNextGreatGeneral = viewingPlayer.greatPeople.pointsForNextGreatGeneralCounter + for ((unit, points) in greatGeneralPoints) { + val pointsToGreatGeneral = pointsForNextGreatGeneral[unit] + add(unit.toLabel()).left() + add("$points/$pointsToGreatGeneral".toLabel()) + } + pack() } diff --git a/tests/src/com/unciv/logic/battle/BattleTest.kt b/tests/src/com/unciv/logic/battle/BattleTest.kt index 58d8da5e3d..3cc91c9258 100644 --- a/tests/src/com/unciv/logic/battle/BattleTest.kt +++ b/tests/src/com/unciv/logic/battle/BattleTest.kt @@ -216,8 +216,8 @@ class BattleTest { Battle.attack(MapUnitCombatant(defaultAttackerUnit), MapUnitCombatant(defaultDefenderUnit)) // then - assertEquals(5, attackerCiv.greatPeople.greatGeneralPoints) - assertEquals(4, defenderCiv.greatPeople.greatGeneralPoints) + assertEquals(5, attackerCiv.greatPeople.greatGeneralPointsCounter["Great General"]) + assertEquals(4, defenderCiv.greatPeople.greatGeneralPointsCounter["Great General"]) } @Test @@ -230,8 +230,8 @@ class BattleTest { Battle.attack(MapUnitCombatant(defaultAttackerUnit), MapUnitCombatant(barbarianUnit)) // then - assertEquals(0, attackerCiv.greatPeople.greatGeneralPoints) - assertEquals(0, barbarianCiv.greatPeople.greatGeneralPoints) + assertEquals(0, attackerCiv.greatPeople.greatGeneralPointsCounter["Great General"]) + assertEquals(0, barbarianCiv.greatPeople.greatGeneralPointsCounter["Great General"]) } @Test @@ -243,7 +243,7 @@ class BattleTest { Battle.attack(MapUnitCombatant(attackerUnit), MapUnitCombatant(defaultDefenderUnit)) // then - assertEquals(10, attackerCiv.greatPeople.greatGeneralPoints) + assertEquals(10, attackerCiv.greatPeople.greatGeneralPointsCounter["Great General"]) } @Test