From b8d79dc23d28398678b5734b7c1295993e3b2d6b Mon Sep 17 00:00:00 2001 From: Xander Lenstra <71121390+xlenstra@users.noreply.github.com> Date: Fri, 2 Jul 2021 14:45:18 +0200 Subject: [PATCH] Split off properties of era's to another JSON file (#4336) * Added Era.json and imported the data to the ruleset * Units at start are now also determined by eras.json * Research agreement costs now determined by json file * Gold and Culture provided at the start are now awarded * Can no longer build wonders that are more than two eras older than the starting era * Default population and buildings for settlers can now be added * Added checks for validity of eras.json file in mods * Colors for the icons of technologies are no also saved in eras.json * Removed constants for all era's as they have been generalized away by this PR * Removed spurios println's * Added compatibility for mods * Updated the military unit you get from ruines to be the military unit you received at the start of the game --- .../jsons/Civ V - Vanilla/Difficulties.json | 48 +++--- .../assets/jsons/Civ V - Vanilla/Eras.json | 150 ++++++++++++++++++ core/src/com/unciv/Constants.kt | 9 +- core/src/com/unciv/logic/GameStarter.kt | 103 ++++++++++-- core/src/com/unciv/logic/city/CityInfo.kt | 12 ++ .../logic/civilization/CivilizationInfo.kt | 10 +- core/src/com/unciv/logic/map/MapUnit.kt | 13 +- .../unciv/models/metadata/GameParameters.kt | 2 +- core/src/com/unciv/models/ruleset/Building.kt | 5 + .../com/unciv/models/ruleset/Difficulty.kt | 21 +-- core/src/com/unciv/models/ruleset/Era.kt | 35 ++++ core/src/com/unciv/models/ruleset/Ruleset.kt | 45 ++++-- .../ui/newgamescreen/GameOptionsTable.kt | 1 + core/src/com/unciv/ui/utils/ImageGetter.kt | 16 +- 14 files changed, 372 insertions(+), 98 deletions(-) create mode 100644 android/assets/jsons/Civ V - Vanilla/Eras.json create mode 100644 core/src/com/unciv/models/ruleset/Era.kt diff --git a/android/assets/jsons/Civ V - Vanilla/Difficulties.json b/android/assets/jsons/Civ V - Vanilla/Difficulties.json index fb34d3139f..4961734da8 100644 --- a/android/assets/jsons/Civ V - Vanilla/Difficulties.json +++ b/android/assets/jsons/Civ V - Vanilla/Difficulties.json @@ -9,7 +9,7 @@ "policyCostModifier": 0.5, "unhappinessModifier": 0.4, "barbarianBonus": 0.75, - "startingUnits": ["Settler", "Warrior"], + "playerBonusStartingUnits": [], // Note that the units from Eras.json are added to this pool. This should only contain bonus starting units. "aiCityGrowthModifier": 1.6, // that is to say it'll take them 1.6 times as long to grow the city "aiUnitCostModifier": 1.75, "aiBuildingCostModifier": 1.6, @@ -17,8 +17,8 @@ "aiBuildingMaintenanceModifier": 1, "aiUnitMaintenanceModifier": 1, "aiFreeTechs": [], - "aiMajorCivStartingUnits": ["Settler", "Warrior"], - "aiCityStateStartingUnits": ["Settler", "Warrior"], + "aiMajorCivBonusStartingUnits": [], + "aiCityStateBonusStartingUnits": [], "aiUnhappinessModifier": 1, "aisExchangeTechs": false, "turnBarbariansCanEnterPlayerTiles": 10000, @@ -34,7 +34,7 @@ "policyCostModifier": 0.67, "unhappinessModifier": 0.6, "barbarianBonus": 0.5, - "startingUnits": ["Settler", "Warrior"], + "playerBonusStartingUnits": [], "aiCityGrowthModifier": 1.3, "aiUnitCostModifier": 1.3, "aiBuildingCostModifier": 1.3, @@ -42,8 +42,8 @@ "aiBuildingMaintenanceModifier": 1, "aiUnitMaintenanceModifier": 1, "aiFreeTechs": [], - "aiMajorCivStartingUnits": ["Settler", "Warrior"], - "aiCityStateStartingUnits": ["Settler", "Warrior"], + "aiMajorCivBonusStartingUnits": [], + "aiCityStateBonusStartingUnits": [], "aiUnhappinessModifier": 1, "aisExchangeTechs": false, "turnBarbariansCanEnterPlayerTiles": 60, @@ -59,7 +59,7 @@ "policyCostModifier": 0.85, "unhappinessModifier": 0.75, "barbarianBonus": 0.4, - "startingUnits": ["Settler", "Warrior"], + "playerBonusStartingUnits": [], "aiCityGrowthModifier": 1.1, "aiUnitCostModifier": 1.1, "aiBuildingCostModifier": 1.1, @@ -67,8 +67,8 @@ "aiBuildingMaintenanceModifier": 1, "aiUnitMaintenanceModifier": 1, "aiFreeTechs": [], - "aiMajorCivStartingUnits": ["Settler", "Warrior"], - "aiCityStateStartingUnits": ["Settler", "Warrior"], + "aiMajorCivBonusStartingUnits": [], + "aiCityStateBonusStartingUnits": [], "aiUnhappinessModifier": 1, "aisExchangeTechs": false, "turnBarbariansCanEnterPlayerTiles": 20, @@ -84,7 +84,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0.33, - "startingUnits": ["Settler", "Warrior"], + "playerBonusStartingUnits": [], "aiCityGrowthModifier": 1, "aiUnitCostModifier": 1, "aiBuildingCostModifier": 1, @@ -92,8 +92,8 @@ "aiBuildingMaintenanceModifier": 1, "aiUnitMaintenanceModifier": 0.85, "aiFreeTechs": [], - "aiMajorCivStartingUnits": ["Settler", "Warrior"], - "aiCityStateStartingUnits": ["Settler", "Warrior"], + "aiMajorCivBonusStartingUnits": [], + "aiCityStateBonusStartingUnits": [], "aiUnhappinessModifier": 1, "aisExchangeTechs": true, "turnBarbariansCanEnterPlayerTiles": 0, @@ -109,7 +109,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0.25, - "startingUnits": ["Settler", "Warrior"], + "playerBonusStartingUnits": [], "aiCityGrowthModifier": 0.9, "aiUnitCostModifier": 0.85, "aiBuildingCostModifier": 0.85, @@ -117,8 +117,8 @@ "aiBuildingMaintenanceModifier": 0.85, "aiUnitMaintenanceModifier": 0.8, "aiFreeTechs": ["Pottery"], - "aiMajorCivStartingUnits": ["Settler", "Warrior", "Warrior"], - "aiCityStateStartingUnits": ["Settler", "Warrior"], + "aiMajorCivBonusStartingUnits": ["Era Starting Unit"], + "aiCityStateBonusStartingUnits": [], "aiUnhappinessModifier": 0.9, "aisExchangeTechs": true, "turnBarbariansCanEnterPlayerTiles": 0, @@ -134,7 +134,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0.2, - "startingUnits": ["Settler", "Warrior"], + "playerBonusStartingUnits": [], "aiCityGrowthModifier": 0.85, "aiUnitCostModifier": 0.8, "aiBuildingCostModifier": 0.8, @@ -142,8 +142,8 @@ "aiBuildingMaintenanceModifier": 0.8, "aiUnitMaintenanceModifier": 0.75, "aiFreeTechs": ["Pottery","Animal Husbandry"], - "aiMajorCivStartingUnits": ["Settler", "Warrior", "Warrior", "Scout"], - "aiCityStateStartingUnits": ["Settler", "Warrior"], + "aiMajorCivBonusStartingUnits": ["Era Starting Unit", "Scout"], + "aiCityStateBonusStartingUnits": [], "aiUnhappinessModifier": 0.85, "aisExchangeTechs": true, "turnBarbariansCanEnterPlayerTiles": 0, @@ -159,7 +159,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0.1, - "startingUnits": ["Settler", "Warrior"], + "playerBonusStartingUnits": [], "aiCityGrowthModifier": 0.75, "aiUnitCostModifier": 0.65, "aiBuildingCostModifier": 0.65, @@ -167,8 +167,8 @@ "aiBuildingMaintenanceModifier": 0.65, "aiUnitMaintenanceModifier": 0.65, "aiFreeTechs": ["Pottery","Animal Husbandry","Mining"], - "aiMajorCivStartingUnits": ["Settler", "Warrior", "Warrior", "Warrior", "Worker", "Scout"], - "aiCityStateStartingUnits": ["Settler", "Warrior"], + "aiMajorCivBonusStartingUnits": ["Era Starting Unit", "Era Starting Unit", "Worker", "Scout"], + "aiCityStateBonusStartingUnits": [], "aiUnhappinessModifier": 0.75, "aisExchangeTechs": true, "turnBarbariansCanEnterPlayerTiles": 0, @@ -184,7 +184,7 @@ "policyCostModifier": 1, "unhappinessModifier": 1, "barbarianBonus": 0, - "startingUnits": ["Settler", "Warrior"], + "playerBonusStartingUnits": [], "aiCityGrowthModifier": 0.6, "aiUnitCostModifier": 0.5, "aiBuildingCostModifier": 0.5, @@ -192,8 +192,8 @@ "aiBuildingMaintenanceModifier": 0.5, "aiUnitMaintenanceModifier": 0.5, "aiFreeTechs": ["Pottery","Animal Husbandry","Mining","The Wheel"], - "aiMajorCivStartingUnits": ["Settler", "Warrior", "Settler", "Warrior", "Warrior", "Worker", "Worker", "Scout"], - "aiCityStateStartingUnits": ["Settler", "Warrior"], + "aiMajorCivBonusStartingUnits": ["Settler", "Era Starting Unit", "Era Starting Unit", "Era Starting Unit", "Worker", "Scout"], + "aiCityStateBonusStartingUnits": [], "aiUnhappinessModifier": 0.6, "aisExchangeTechs": true, "turnBarbariansCanEnterPlayerTiles": 0, diff --git a/android/assets/jsons/Civ V - Vanilla/Eras.json b/android/assets/jsons/Civ V - Vanilla/Eras.json new file mode 100644 index 0000000000..c9d7b475c0 --- /dev/null +++ b/android/assets/jsons/Civ V - Vanilla/Eras.json @@ -0,0 +1,150 @@ +[ + // Source: the fandom, so take these with a grain of salt + // I also did some testing in the official version, but by far not all + + { + "name": "Ancient era", + "researchAgreementCost": 150, + "startingSettlerCount": 1, // These values should not include the values given in Difficulties.json + "startingWorkerCount": 0, + "startingMilitaryUnitCount": 1, + "startingMilitaryUnit": "Warrior", + "settlerPopulation": 1, + "iconRGB": [255, 87, 35] + }, + { + "name": "Classical era", + "researchAgreementCost": 150, + "startingSettlerCount": 1, + "startingWorkerCount": 0, + "startingMilitaryUnitCount": 2, + "startingMilitaryUnit": "Spearman", + "startingGold": 10, + "startingCulture": 100, + "settlerPopulation": 1, + "iconRGB": [233, 31, 99] + }, + { + "name": "Medieval era", + "researchAgreementCost": 250, + "startingSettlerCount": 2, + "startingWorkerCount": 1, + "startingMilitaryUnitCount": 3, + "startingMilitaryUnit": "Spearman", + "startingGold": 25, + "startingCulture": 200, + "settlerPopulation": 1, + "settlerBuildings": ["Shrine","Monument"], + "startingObsoleteWonders": ["Temple of Artemis", "Stonehenge", "The Great Library", "Mausoleum of Halicarnassus", "The Pyramids", "Statue of Zeus"], + "iconRGB": [157, 39, 176] + }, + { + "name": "Renaissance era", + "researchAgreementCost": 250, + "startingSettlerCount": 2, + "startingWorkerCount": 1, + "startingMilitaryUnitCount": 3, + "startingMilitaryUnit": "Pikeman", + "startingGold": 50, + "startingCulture": 300, + "settlerPopulation": 2, + "settlerBuildings": ["Shrine","Monument","Granary","Lighthouse"], + "startingObsoleteWonders": ["Temple of Artemis", "Stonehenge", "The Great Library", "Mausoleum of Halicarnassus", "The Pyramids", "Statue of Zeus", + "The Great Lighthouse", "Hanging Gardens", "Terracotta Army", "The Oracle", "Petra", "Great Wall", "Colossus"], + "iconRGB": [104, 58, 183] + }, + { + "name": "Industrial era", + "researchAgreementCost": 300, + "startingSettlerCount": 3, + "startingWorkerCount": 2, + "startingMilitaryUnitCount": 5, + "startingMilitaryUnit": "Musketman", + "startingGold": 100, + "startingCulture": 400, + "settlerPopulation": 3, + "settlerBuildings": ["Monument","Granary","Lighthouse","Market","Workshop","Amphitheater","Barracks","Library","Colosseum"], + "startingObsoleteWonders": ["Temple of Artemis", "Stonehenge", "The Great Library", "Mausoleum of Halicarnassus", "The Pyramids", "Statue of Zeus", + "The Great Lighthouse", "Hanging Gardens", "Terracotta Army", "The Oracle", "Petra", "Great Wall", "Colossus", + "Hagia Sophia", "Chichen Itza", "Machu Picchu", "Angkor Wat", "Alhambra", "Notre Dame"], + "iconRGB": [63, 81, 182] + }, + { + "name": "Modern era", + "researchAgreementCost": 350, + "startingSettlerCount": 3, + "startingWorkerCount": 2, + "startingMilitaryUnitCount": 5, + "startingMilitaryUnit": "Rifleman", + "startingGold": 200, + "startingCulture": 500, + "settlerPopulation": 3, + "settlerBuildings": ["Monument","Granary","Lighthouse","Market","Workshop","Amphitheater","Barracks","Library","Colosseum"], + "startingObsoleteWonders": ["Temple of Artemis", "Stonehenge", "The Great Library", "Mausoleum of Halicarnassus", "The Pyramids", "Statue of Zeus", + "The Great Lighthouse", "Hanging Gardens", "Terracotta Army", "The Oracle", "Petra", "Great Wall", "Colossus", + "Hagia Sophia", "Chichen Itza", "Machu Picchu", "Angkor Wat", "Alhambra", "Notre Dame", + "Sistine Chapel", "Forbidden Palace", "Leaning Tower of Pisa", "Himeji Castle", "Taj Mahal", "Porcelain Tower", "Kremlin"], + "iconRGB": [33, 150, 243] + }, + { + "name": "Atomic era", + "researchAgreementCost": 400, + "startingSettlerCount": 3, + "startingWorkerCount": 2, + "startingMilitaryUnitCount": 5, + "startingMilitaryUnit": "Infantry", + "startingGold": 200, + "startingCulture": 500, + "settlerPopulation": 4, + "settlerBuildings": ["Monument","Granary","Lighthouse","Market","Workshop","Amphitheater","Barracks","Library","Colosseum"], + "startingObsoleteWonders": ["Temple of Artemis", "Stonehenge", "The Great Library", "Mausoleum of Halicarnassus", "The Pyramids", "Statue of Zeus", + "The Great Lighthouse", "Hanging Gardens", "Terracotta Army", "The Oracle", "Petra", "Great Wall", "Colossus", + "Hagia Sophia", "Chichen Itza", "Machu Picchu", "Angkor Wat", "Alhambra", "Notre Dame", + "Sistine Chapel", "Forbidden Palace", "Leaning Tower of Pisa", "Himeji Castle", "Taj Mahal", "Porcelain Tower", "Kremlin", + "The Louvre", "Big Ben", "Brandenburg Gate"], + "iconRGB": [0, 150, 136] + }, + { + "name": "Information era", + "researchAgreementCost": 400, + "startingSettlerCount": 3, + "startingWorkerCount": 3, + "startingMilitaryUnitCount": 5, + "startingMilitaryUnit": "Marine", + "startingGold": 400, + "startingCulture": 600, + "settlerPopulation": 5, + "settlerBuildings": ["Monument","Granary","Lighthouse","Market","Workshop","Amphitheater","Barracks","Library","Colosseum","Theatre","Bank"], + "startingObsoleteWonders": ["Temple of Artemis", "Stonehenge", "The Great Library", "Mausoleum of Halicarnassus", "The Pyramids", "Statue of Zeus", + "The Great Lighthouse", "Hanging Gardens", "Terracotta Army", "The Oracle", "Petra", "Great Wall", "Colossus", + "Hagia Sophia", "Chichen Itza", "Machu Picchu", "Angkor Wat", "Alhambra", "Notre Dame", + "Sistine Chapel", "Forbidden Palace", "Leaning Tower of Pisa", "Himeji Castle", "Taj Mahal", "Porcelain Tower", "Kremlin", + "The Louvre", "Big Ben", "Brandenburg Gate", + "Eiffel Tower", "Statue of Liberty", "Neuschwanstein", "Cristo Redentor"], + // So theoretically this is always just all the wonders at least 2 eras old. So we could just use that. + // But where is the modularity? The excluding of very specific wonders? That is no fun. + // So we just write down the entire long list (sorted by era!) instead. + "iconRGB": [76, 176, 81] + }, + { // Technically, this Era doesn't exist in the original game. + // But as it is _really_ usefull to have for testing, I'd like to keep it. + // The stats are just copy-pasted from the information era. + "name": "Future era", + "researchAgreementCost": 400, + "startingSettlerCount": 3, + "startingWorkerCount": 3, + "startingMilitaryUnitCount": 5, + "startingMilitaryUnit": "Marine", + "startingGold": 400, + "startingCulture": 600, + "settlerPopulation": 5, + "settlerBuildings": ["Monument","Granary","Lighthouse","Market","Workshop","Amphitheater","Barracks","Library","Colosseum","Theatre","Bank"], + "startingObsoleteWonders": ["Temple of Artemis", "Stonehenge", "The Great Library", "Mausoleum of Halicarnassus", "The Pyramids", "Statue of Zeus", + "The Great Lighthouse", "Hanging Gardens", "Terracotta Army", "The Oracle", "Petra", "Great Wall", "Colossus", + "Hagia Sophia", "Chichen Itza", "Machu Picchu", "Angkor Wat", "Alhambra", "Notre Dame", + "Sistine Chapel", "Forbidden Palace", "Leaning Tower of Pisa", "Himeji Castle", "Taj Mahal", "Porcelain Tower", "Kremlin", + "The Louvre", "Big Ben", "Brandenburg Gate", + "Eiffel Tower", "Statue of Liberty", "Neuschwanstein", "Cristo Redentor"], + "iconRGB": [76, 176, 81] + } +] \ No newline at end of file diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index 3e3b0efaa1..cf1b937cdd 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -8,6 +8,7 @@ object Constants { // const val settler = "Settler" const val settlerUnique = "Founds a new city" + const val eraSpecificUnit = "Era Starting Unit" const val impassable = "Impassable" const val ocean = "Ocean" @@ -71,14 +72,6 @@ object Constants { const val disabled = "disabled" const val enabled = "enabled" - const val ancientEra = "Ancient era" - const val classicalEra = "Classical era" - const val medievalEra = "Medieval era" - const val renaissanceEra = "Renaissance era" - const val industrialEra = "Industrial era" - const val modernEra = "Modern era" - const val informationEra = "Information era" - const val futureEra = "Future era" const val barbarians = "Barbarians" const val spectator = "Spectator" const val custom = "Custom" diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index de124d81aa..59e3e82f25 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -8,10 +8,12 @@ import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap import com.unciv.logic.map.mapgenerator.MapGenerator import com.unciv.models.metadata.GameParameters +import com.unciv.models.ruleset.Era import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.newgamescreen.GameSetupInfo import java.util.* +import kotlin.NoSuchElementException import kotlin.collections.ArrayList import kotlin.math.max @@ -52,6 +54,8 @@ object GameStarter { gameInfo.setTransients() // needs to be before placeBarbarianUnit because it depends on the tilemap having its gameinfo set addCivTechs(gameInfo, ruleset, gameSetupInfo) + + addCivStats(gameInfo) // and only now do we add units for everyone, because otherwise both the gameInfo.setTransients() and the placeUnit will both add the unit to the civ's unit list! addCivStartingUnits(gameInfo) @@ -111,6 +115,21 @@ object GameStarter { } } + private fun addCivStats(gameInfo: GameInfo) { + val ruleSet = gameInfo.ruleSet + val startingEra = gameInfo.gameParameters.startingEra + val era = + if (startingEra in ruleSet.eras.keys) { + ruleSet.eras[startingEra]!! + } else { + Era() + } + for (civInfo in gameInfo.civilizations.filter { !it.isBarbarian() }) { + civInfo.addGold((era.startingGold * gameInfo.gameParameters.gameSpeed.modifier).toInt()) + civInfo.policies.addCulture((era.startingCulture * gameInfo.gameParameters.gameSpeed.modifier).toInt()) + } + } + private fun addCivilizations(newGameParameters: GameParameters, gameInfo: GameInfo, ruleset: Ruleset) { val availableCivNames = Stack() // CityState or Spectator civs are not available for Random pick @@ -160,16 +179,8 @@ object GameStarter { val startingLocations = getStartingLocations( gameInfo.civilizations.filter { !it.isBarbarian() }, gameInfo.tileMap) - - // For later starting eras, or for civs like Polynesia with a different Warrior, we need different starting units - fun getWarriorEquivalent(civ: CivilizationInfo): String? { - val availableMilitaryUnits = gameInfo.ruleSet.units.values.filter { - it.isBuildable(civ) - && it.unitType.isLandUnit() - && !it.unitType.isCivilian() - } - return availableMilitaryUnits.maxByOrNull { max(it.strength, it.rangedStrength) }?.name - } + + // no starting units for Barbarians and Spectators for (civ in gameInfo.civilizations.filter { !it.isBarbarian() && !it.isSpectator() }) { val startingLocation = startingLocations[civ]!! @@ -180,16 +191,76 @@ object GameStarter { fun placeNearStartingPosition(unitName: String) { civ.placeUnitNearTile(startingLocation.position, unitName) } + - val warriorEquivalent = getWarriorEquivalent(civ) - val startingUnits = when { - civ.isPlayerCivilization() -> gameInfo.getDifficulty().startingUnits - civ.isMajorCiv() -> gameInfo.getDifficulty().aiMajorCivStartingUnits - else -> gameInfo.getDifficulty().aiCityStateStartingUnits + // Determine starting units based on starting era + val ruleSet = gameInfo.ruleSet + val startingEra = gameInfo.gameParameters.startingEra + var startingUnits: MutableList + var eraUnitReplacement: String + + if (ruleSet.eras.isEmpty()) { // We are using an older mod, so we only look at the difficulty file + startingUnits = (when { + civ.isPlayerCivilization() -> gameInfo.getDifficulty().startingUnits + civ.isMajorCiv() -> gameInfo.getDifficulty().aiMajorCivStartingUnits + else -> gameInfo.getDifficulty().aiCityStateStartingUnits + }).toMutableList() + + val warriorEquivalent = ruleSet.units + .filter { it.value.unitType.isLandUnit() && it.value.unitType.isMilitary() && it.value.isBuildable(civ) } + .maxByOrNull {max(it.value.strength, it.value.rangedStrength)} + ?.key + + for (unit in startingUnits) { + val unitToAdd = if (unit == "Warrior") warriorEquivalent else unit + if (unitToAdd != null) placeNearStartingPosition(unitToAdd) + } + + continue + } + + + if (startingEra in ruleSet.eras.keys) { + startingUnits = ruleSet.eras[startingEra]!!.getStartingUnits().toMutableList() + eraUnitReplacement = ruleSet.eras[startingEra]!!.startingMilitaryUnit + } else { + startingUnits = Era().getStartingUnits().toMutableList() + eraUnitReplacement = Era().startingMilitaryUnit + } + + // Add extra units granted by difficulty + startingUnits.addAll(when { + civ.isPlayerCivilization() -> gameInfo.getDifficulty().playerBonusStartingUnits + civ.isMajorCiv() -> gameInfo.getDifficulty().aiMajorCivBonusStartingUnits + else -> gameInfo.getDifficulty().aiCityStateBonusStartingUnits + }) + + fun getEquivalentUnit(civ: CivilizationInfo, unitParam: String): String? { + var unit = unitParam // We want to change it and this is the easiest way to do so + if (unit == Constants.eraSpecificUnit) unit = eraUnitReplacement + if (unit == "Settler" && "Settler" !in ruleSet.units) { + val settlerLikeUnits = ruleSet.units.filter { + it.value.uniqueObjects.any { it.placeholderText == Constants.settlerUnique } + && it.value.isBuildable(civ) + && it.value.unitType.isCivilian() + } + if (settlerLikeUnits.isEmpty()) return null // No settlers in this mod + return civ.getEquivalentUnit(settlerLikeUnits.keys.random()).name + } + if (unit == "Worker" && "Worker" !in ruleSet.units) { + val workerLikeUnits = ruleSet.units.filter { + it.value.uniqueObjects.any { it.placeholderText == Constants.canBuildImprovements } + && it.value.isBuildable(civ) + && it.value.unitType.isCivilian() + } + if (workerLikeUnits.isEmpty()) return null // No workers in this mod + return civ.getEquivalentUnit(workerLikeUnits.keys.random()).name + } + return civ.getEquivalentUnit(unit).name } for (unit in startingUnits) { - val unitToAdd = if (unit == "Warrior") warriorEquivalent else unit + val unitToAdd = getEquivalentUnit(civ, unit) if (unitToAdd != null) placeNearStartingPosition(unitToAdd) } } diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index c68ccaaa89..5193497e9e 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -96,6 +96,18 @@ class CityInfo { cityConstructions.addBuilding(freeBuildingName) } } + + // Add buildings and pop we get from starting in this era + val ruleset = civInfo.gameInfo.ruleSet + val startingEra = civInfo.gameInfo.gameParameters.startingEra + if (startingEra in ruleset.eras) { + population.setPopulation(ruleset.eras[startingEra]!!.settlerPopulation) + for (building in ruleset.eras[startingEra]!!.settlerBuildings) { + if (ruleset.buildings[building]!!.isBuildable(cityConstructions)) { + cityConstructions.addBuilding(building) + } + } + } expansion.reset() diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index f571c1bf2d..6a73c4dfd9 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -717,14 +717,8 @@ class CivilizationInfo { fun getResearchAgreementCost(): Int { // https://forums.civfanatics.com/resources/research-agreements-bnw.25568/ - val basicGoldCostOfSignResearchAgreement = when (getEra()) { - Constants.medievalEra, Constants.renaissanceEra -> 250 - Constants.industrialEra -> 300 - Constants.modernEra -> 350 - Constants.informationEra, Constants.futureEra -> 400 - else -> 0 - } - return (basicGoldCostOfSignResearchAgreement * gameInfo.gameParameters.gameSpeed.modifier).toInt() + val era = if (getEra() in gameInfo.ruleSet.eras) gameInfo.ruleSet.eras[getEra()]!! else Era() + return (era.researchAgreementCost * gameInfo.gameParameters.gameSpeed.modifier).toInt() } fun gainMilitaryUnitFromCityState(otherCiv: CivilizationInfo) { diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 3b79ea5b26..2531cac491 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -749,15 +749,15 @@ class MapUnit { ) } - val researchableAncientEraTechs = tile.tileMap.gameInfo.ruleSet.technologies.values + val researchableFirstEraTechs = tile.tileMap.gameInfo.ruleSet.technologies.values .filter { !civInfo.tech.isResearched(it.name) && civInfo.tech.canBeResearched(it.name) - && it.era() == Constants.ancientEra + && civInfo.gameInfo.ruleSet.getEraNumber(it.era()) == 1 } - if (researchableAncientEraTechs.isNotEmpty()) + if (researchableFirstEraTechs.isNotEmpty()) actions.add { - val tech = researchableAncientEraTechs.random(tileBasedRandom).name + val tech = researchableFirstEraTechs.random(tileBasedRandom).name civInfo.tech.addTechnology(tech) civInfo.addNotification( "We have discovered the lost technology of [$tech] in the ruins!", @@ -767,10 +767,13 @@ class MapUnit { ) } + val militaryUnit = + if (civInfo.gameInfo.gameParameters.startingEra !in civInfo.gameInfo.ruleSet.eras) "Warrior" + else civInfo.gameInfo.ruleSet.eras[civInfo.gameInfo.gameParameters.startingEra]!!.startingMilitaryUnit val possibleUnits = ( //City-States and OCC don't get settler from ruins listOf(Constants.settler).filterNot { civInfo.isCityState() || civInfo.isOneCityChallenger() } - + listOf(Constants.worker, "Warrior") + + listOf(Constants.worker, militaryUnit) ).filter { civInfo.gameInfo.ruleSet.units.containsKey(it) } if (possibleUnits.isNotEmpty()) actions.add { diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt index a913db7678..11fe5fa7ca 100644 --- a/core/src/com/unciv/models/metadata/GameParameters.kt +++ b/core/src/com/unciv/models/metadata/GameParameters.kt @@ -24,7 +24,7 @@ class GameParameters { // Default values are the default new game var religionEnabled = false var victoryTypes: ArrayList = arrayListOf(VictoryType.Cultural, VictoryType.Domination, VictoryType.Scientific) // By default, all victory types - var startingEra = Constants.ancientEra + var startingEra = "Ancient Era" var isOnlineMultiplayer = false var baseRuleset: BaseRuleset = BaseRuleset.Civ_V_Vanilla diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index 09629c19d2..98c1e4fe86 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -309,6 +309,11 @@ class Building : NamedStats(), IConstruction { if (civInfo.isCityState()) return "No world wonders for city-states" + + val ruleSet = civInfo.gameInfo.ruleSet + val startingEra = civInfo.gameInfo.gameParameters.startingEra + if (startingEra in ruleSet.eras && name in ruleSet.eras[startingEra]!!.startingObsoleteWonders) + return "Wonder is disabled when starting in this era" } diff --git a/core/src/com/unciv/models/ruleset/Difficulty.kt b/core/src/com/unciv/models/ruleset/Difficulty.kt index e8b703bdcf..bd98227e2d 100644 --- a/core/src/com/unciv/models/ruleset/Difficulty.kt +++ b/core/src/com/unciv/models/ruleset/Difficulty.kt @@ -14,7 +14,8 @@ class Difficulty: INamed { var policyCostModifier:Float = 1f var unhappinessModifier:Float = 1f var barbarianBonus:Float = 0f - var startingUnits = ArrayList() + var startingUnits = ArrayList() // Deprecated since 3.15.8 + var playerBonusStartingUnits = ArrayList() var aiCityGrowthModifier:Float = 1f var aiUnitCostModifier:Float = 1f @@ -23,22 +24,14 @@ class Difficulty: INamed { var aiBuildingMaintenanceModifier:Float = 1f var aiUnitMaintenanceModifier = 1f var aiFreeTechs = ArrayList() - var aiMajorCivStartingUnits = ArrayList() - var aiCityStateStartingUnits = ArrayList() + var aiMajorCivStartingUnits = ArrayList() // Deprecated since 3.15.8 + var aiMajorCivBonusStartingUnits = ArrayList() + var aiCityStateStartingUnits = ArrayList() // Deprecated since 3.15.8 + var aiCityStateBonusStartingUnits = ArrayList() var aiUnhappinessModifier = 1f var turnBarbariansCanEnterPlayerTiles = 0 var clearBarbarianCampReward = 25 - - init { - // For compatibility with old mods that use deprecated var aiFreeUnits and do not have startingUnits, aiCityStateStartingUnits, aiMajorCivStartingUnits - if (startingUnits.isEmpty()) { - startingUnits.add(Constants.settler) - startingUnits.add("Warrior") - aiCityStateStartingUnits.addAll(startingUnits) - aiMajorCivStartingUnits.addAll(startingUnits) - } - } - + fun getDescription(): String { val lines = ArrayList() lines += "Player settings" diff --git a/core/src/com/unciv/models/ruleset/Era.kt b/core/src/com/unciv/models/ruleset/Era.kt new file mode 100644 index 0000000000..59031f1b0e --- /dev/null +++ b/core/src/com/unciv/models/ruleset/Era.kt @@ -0,0 +1,35 @@ +package com.unciv.models.ruleset + +import com.badlogic.gdx.graphics.Color +import com.unciv.models.stats.INamed +import com.unciv.ui.utils.colorFromRGB + +class Era : INamed { + override var name: String = "" + var researchAgreementCost = 300 + var startingSettlerCount = 1 + var startingSettlerUnit = "Settler" // For mods which have differently named settlers + var startingWorkerCount = 0 + var startingWorkerUnit = "Worker" + var startingMilitaryUnitCount = 1 + var startingMilitaryUnit = "Warrior" + var startingGold = 0 + var startingCulture = 0 + var settlerPopulation = 1 + var settlerBuildings = ArrayList() + var startingObsoleteWonders = ArrayList() + var iconRGB: List? = null + + fun getStartingUnits(): List { + val startingUnits = mutableListOf() + repeat(startingSettlerCount) {startingUnits.add(startingSettlerUnit)} + repeat(startingWorkerCount) {startingUnits.add(startingWorkerUnit)} + repeat(startingMilitaryUnitCount) {startingUnits.add(startingMilitaryUnit)} + return startingUnits + } + + fun getColor(): Color { + if (iconRGB == null) return Color.WHITE.cpy() + return colorFromRGB(iconRGB!![0], iconRGB!![1], iconRGB!![2]) + } +} \ No newline at end of file diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index 1fef9b5655..7057417bad 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -41,15 +41,16 @@ class ModOptions { class Ruleset { private val jsonParser = JsonParser() - + var modWithReligionLoaded = false - + var name = "" val buildings = LinkedHashMap() val terrains = LinkedHashMap() val tileResources = LinkedHashMap() val tileImprovements = LinkedHashMap() val technologies = LinkedHashMap() + val eras = LinkedHashMap() val units = LinkedHashMap() val unitPromotions = LinkedHashMap() val nations = LinkedHashMap() @@ -79,6 +80,7 @@ class Ruleset { buildings.putAll(ruleset.buildings) for (buildingToRemove in ruleset.modOptions.buildingsToRemove) buildings.remove(buildingToRemove) difficulties.putAll(ruleset.difficulties) + eras.putAll(ruleset.eras) nations.putAll(ruleset.nations) policyBranches.putAll(ruleset.policyBranches) policies.putAll(ruleset.policies) @@ -99,22 +101,22 @@ class Ruleset { } fun clear() { + beliefs.clear() buildings.clear() difficulties.clear() - nations.clear() + eras.clear() policyBranches.clear() + specialists.clear() + mods.clear() + nations.clear() policies.clear() - beliefs.clear() quests.clear() technologies.clear() - buildings.clear() terrains.clear() tileImprovements.clear() tileResources.clear() unitPromotions.clear() - specialists.clear() units.clear() - mods.clear() modWithReligionLoaded = false } @@ -127,7 +129,7 @@ class Ruleset { try { modOptions = jsonParser.getFromJson(ModOptions::class.java, modOptionsFile) } catch (ex: Exception) {} - } + } val techFile = folderHandle.child("Techs.json") if (techFile.exists()) { @@ -153,6 +155,8 @@ class Ruleset { val improvementsFile = folderHandle.child("TileImprovements.json") if (improvementsFile.exists()) tileImprovements += createHashmap(jsonParser.getFromJson(Array::class.java, improvementsFile)) + val erasFile = folderHandle.child("Eras.json") + if (erasFile.exists()) eras += createHashmap(jsonParser.getFromJson(Array::class.java, erasFile)) val unitsFile = folderHandle.child("Units.json") if (unitsFile.exists()) units += createHashmap(jsonParser.getFromJson(Array::class.java, unitsFile)) @@ -215,7 +219,7 @@ class Ruleset { } fun getEras(): List = technologies.values.map { it.column!!.era }.distinct() - fun hasReligion() = beliefs.any() && modWithReligionLoaded + fun hasReligion() = beliefs.any() && modWithReligionLoaded fun getEraNumber(era: String) = getEras().indexOf(era) fun getSummary(): String { @@ -236,7 +240,7 @@ class Ruleset { /** Result of a Mod RuleSet check */ // essentially a named Pair with a few shortcuts class CheckModLinksResult(val status: CheckModLinksStatus, val message: String) { - // Empty constructor just makes the Complex Mod Check on the new game screen shorter + // Empty constructor just makes the Complex Mod Check on the new game screen shorter constructor(): this(CheckModLinksStatus.OK, "") // Constructor that joins lines constructor(status: CheckModLinksStatus, lines: ArrayList): @@ -251,7 +255,7 @@ class Ruleset { else -> CheckModLinksStatus.Error }, lines) - // Allows $this in format strings + // Allows $this in format strings override fun toString() = message // Readability shortcuts fun isError() = status == CheckModLinksStatus.Error @@ -371,8 +375,27 @@ class Ruleset { warningCount++ } } + // eras.isNotEmpty() is only for mod compatibility, it should be removed at some point. + if (eras.isNotEmpty() && tech.era() !in eras) + lines += "Unknown era ${tech.era()} referenced in column of tech ${tech.name}" } + for (era in eras) { + for (wonder in era.value.startingObsoleteWonders) + if (wonder !in buildings) + lines += "Nonexistent wonder ${wonder} obsoleted when starting in ${era.key}!" + for (building in era.value.settlerBuildings) + if (building !in buildings) + lines += "Nonexistent building ${building} built by settlers when starting in ${era.key}" + if (era.value.startingMilitaryUnit !in units) + lines += "Nonexistent unit ${era.value.startingMilitaryUnit} marked as starting unit when starting in ${era.key}" + if (era.value.researchAgreementCost < 0 || era.value.startingSettlerCount < 0 || era.value.startingWorkerCount < 0 || era.value.startingMilitaryUnitCount < 0 || era.value.startingGold < 0 || era.value.startingCulture < 0) + lines += "Unexpected negative number found while parsing era ${era.key}" + if (era.value.settlerPopulation <= 0) + lines += "Population in cities from settlers must be strictly positive! Found value ${era.value.settlerPopulation} for era ${era.key}" + } + + return CheckModLinksResult(warningCount, lines) } } diff --git a/core/src/com/unciv/ui/newgamescreen/GameOptionsTable.kt b/core/src/com/unciv/ui/newgamescreen/GameOptionsTable.kt index d42e5d51ba..3d13cc448d 100644 --- a/core/src/com/unciv/ui/newgamescreen/GameOptionsTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/GameOptionsTable.kt @@ -137,6 +137,7 @@ class GameOptionsTable(val previousScreen: IPreviousScreen, val updatePlayerPick private fun Table.addEraSelectBox() { if (ruleset.technologies.isEmpty()) return // mod with no techs + // Should eventually be changed to use eras.json, but we'll keep it like this for now for mod compatibility val eras = ruleset.technologies.values.filter { !it.uniques.contains("Starting tech") }.map { it.era() }.distinct() addSelectBox("{Starting Era}:", eras, gameParameters.startingEra) { gameParameters.startingEra = it } diff --git a/core/src/com/unciv/ui/utils/ImageGetter.kt b/core/src/com/unciv/ui/utils/ImageGetter.kt index f2331f8fb8..4945094e39 100644 --- a/core/src/com/unciv/ui/utils/ImageGetter.kt +++ b/core/src/com/unciv/ui/utils/ImageGetter.kt @@ -16,6 +16,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.models.ruleset.Era import com.unciv.models.ruleset.Nation import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceType @@ -319,17 +320,10 @@ object ImageGetter { fun getTechIconGroup(techName: String, circleSize: Float) = getTechIcon(techName).surroundWithCircle(circleSize) fun getTechIcon(techName: String): Image { - val techIconColor = when (ruleset.technologies[techName]!!.era()) { - Constants.ancientEra -> colorFromRGB(255, 87, 35) - Constants.classicalEra -> colorFromRGB(233, 31, 99) - Constants.medievalEra -> colorFromRGB(157, 39, 176) - Constants.renaissanceEra -> colorFromRGB(104, 58, 183) - Constants.industrialEra -> colorFromRGB(63, 81, 182) - Constants.modernEra -> colorFromRGB(33, 150, 243) - Constants.informationEra -> colorFromRGB(0, 150, 136) - Constants.futureEra -> colorFromRGB(76, 176, 81) - else -> Color.WHITE.cpy() - } + val era = ruleset.technologies[techName]!!.era() + val techIconColor = + if (era !in ruleset.eras) Era().getColor() + else ruleset.eras[ruleset.technologies[techName]!!.era()]!!.getColor() return getImage("TechIcons/$techName").apply { color = techIconColor.lerp(Color.BLACK, 0.6f) } }