From 92d3fa65e364015d425038be153e6defde8032f6 Mon Sep 17 00:00:00 2001 From: Xander Lenstra <71121390+xlenstra@users.noreply.github.com> Date: Sun, 8 Aug 2021 16:31:08 +0200 Subject: [PATCH] Ruins now have their own file (#4771) * Ruins now have their own file * Added religious rewards * Added an option for only enabling rewards after a certain amount of turns * You can now weigh rewards making some more likely than others * Cleaned up some code * Make new changes compatible with old mods * Implemented proposed changes * Implemented requested changes * Implemented requested changes --- .../assets/jsons/Civ V - Vanilla/Ruins.json | 64 +++ .../Civ V - Vanilla/TileImprovements.json | 22 +- .../assets/jsons/Civ V - Vanilla/Units.json | 3 +- .../jsons/translations/template.properties | 4 + core/src/com/unciv/Constants.kt | 1 - core/src/com/unciv/logic/GameStarter.kt | 9 +- .../unciv/logic/automation/UnitAutomation.kt | 7 +- .../logic/civilization/CivilizationInfo.kt | 4 + .../unciv/logic/civilization/Notification.kt | 7 + .../logic/civilization/ReligionManager.kt | 10 +- .../civilization/RuinsManager/RuinsManager.kt | 63 +++ core/src/com/unciv/logic/map/MapUnit.kt | 146 +----- .../src/com/unciv/logic/map/UnitPromotions.kt | 13 +- .../logic/map/mapgenerator/MapGenerator.kt | 5 +- .../com/unciv/models/ruleset/RuinReward.kt | 12 + core/src/com/unciv/models/ruleset/Ruleset.kt | 15 +- core/src/com/unciv/models/ruleset/Unique.kt | 468 ++++++++++++++++-- .../models/ruleset/tile/TileImprovement.kt | 1 + .../com/unciv/models/ruleset/unit/BaseUnit.kt | 26 +- core/src/com/unciv/models/stats/Stat.kt | 17 +- .../unciv/models/translations/Translations.kt | 2 + .../ui/cityscreen/CityConstructionsTable.kt | 2 +- .../unciv/ui/newgamescreen/NewGameScreen.kt | 1 + .../unciv/ui/worldscreen/WorldMapHolder.kt | 2 +- .../unciv/ui/worldscreen/unit/UnitActions.kt | 72 ++- 25 files changed, 754 insertions(+), 222 deletions(-) create mode 100644 android/assets/jsons/Civ V - Vanilla/Ruins.json create mode 100644 core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt create mode 100644 core/src/com/unciv/models/ruleset/RuinReward.kt diff --git a/android/assets/jsons/Civ V - Vanilla/Ruins.json b/android/assets/jsons/Civ V - Vanilla/Ruins.json new file mode 100644 index 0000000000..05bbfb577d --- /dev/null +++ b/android/assets/jsons/Civ V - Vanilla/Ruins.json @@ -0,0 +1,64 @@ +[ + { + "name": "freeCulture", + "notification": "We have discovered cultural artifacts in the ruins! (+20 culture)", + "uniques": ["Gain [20] [Culture]"] + }, + { + "name": "joinWorker", + "notification": "A [Worker] has joined us!", + "uniques": ["Free [Worker] found in the ruins"], + "excludedDifficulties": ["Prince", "King", "Emperor", "Immortal", "Deity"] + }, + { + "name": "joinSettler", + "notification": "A [Settler] has joined us!", + "uniques": ["Free [Settler] found in the ruins"], + "excludedDifficulties": ["Warlord","Prince","King","Emperor","Immortal","Deity"] + }, + { + "name": "freeXP", + "notification": "An ancient tribe trained us in their ways of combat!", + "uniques": ["This Unit gains [10] XP"] + }, + { + "name": "freePop", + "notification": "We have found survivors in the ruins! Population added to [cityName].", + "uniques": ["[+1] population in a random city"] // This can't be easily added to cityFilter, as it is non-deterministic + }, + { + "name": "freeGold", + "notification": "We have found a stash of [goldAmount] Gold in the ruins!", + "uniques": ["Gain [50]-[100] [Gold]"] + }, + { + "name": "freeAncientTech", + "notification": "We have discovered the lost technology of [techName] in the ruins!", + "uniques": ["[1] free random researchable Tech(s) from the [Ancient era]"] + }, + { + "name": "unitUpgrade", + "notification": "Our unit finds advanced weaponry hidden in the ruins!", + "uniques": ["This Unit upgrades for free including special upgrades"] + }, + { + "name": "barbCampsRevealed", + "notification": "You find evidence of Barbarian activity. Nearby Barbarian camps are revealed!", + "uniques": ["Reveal up to [All] [Barbarian encampment] within a [10] tile radius"] + }, + { + "name": "crudelyDrawnMap", + "notification": "We have found a crudely-drawn map in the ruins!", + "uniques": ["From a randomly chosen tile [4] tiles away, reveal tiles up to [4] tiles away with [80]% chance"] + }, + { + "name": "holySymbols", + "notification": "We have found holy symbols in the ruins, giving us a deeper understanding of religion! (+[faithAmount] Faith)", + "uniques": ["Hidden when religion is disabled", "Gain enough Faith for a Pantheon"] + }, + { + "name": "prophecy", + "notification": "We have found an ancient prophecy in the ruins, greatly increasing our spiritual connection! (+[faithAmount] Faith)", + "uniques": ["Hidden when religion is disabled", "Gain enough Faith for [33]% of a Great Prophet", "Hidden after generating a Great Prophet"] + } +] \ No newline at end of file diff --git a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json index 0bc5b4b2f7..f3d904400c 100644 --- a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json +++ b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json @@ -234,7 +234,9 @@ "shortcutKey": "F" }, - { "name": "Ancient ruins", "uniques": ["Unpillagable"], + { + "name": "Ancient ruins", + "uniques": ["Unpillagable", "Provides a random bonus when entered"], "civilopediaText": [ {"text":"Ancient ruins provide a one-time random bonus when explored"}, {}, @@ -248,14 +250,22 @@ {"text":"find a crudely-drawn map", "indent":1, "starred":true} ] }, - { "name": "City ruins", "uniques": ["Unpillagable"], - "civilopediaText": [{"text":"A bleak reminder of the destruction wreaked by War"}] }, - { "name": "City center", "uniques": ["Unpillagable", "Indestructible"], + { + "name": "City ruins", + "uniques": ["Unpillagable"], + "civilopediaText": [{"text":"A bleak reminder of the destruction wreaked by War"}] + }, + { + "name": "City center", + "uniques": ["Unpillagable", "Indestructible"], "civilopediaText": [ {"text":"Marks the center of a city"}, {"text":"Appearance changes with the technological era of the owning civilization"} ] }, - { "name": "Barbarian encampment", "uniques": ["Unpillagable"], - "civilopediaText": [{"text":"Home to uncivilized barbarians, will spawn a hostile unit from time to time"}] } + { + "name": "Barbarian encampment", + "uniques": ["Unpillagable"], + "civilopediaText": [{"text":"Home to uncivilized barbarians, will spawn a hostile unit from time to time"}] + } ] diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index ffe3fb26e5..068f706674 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -26,7 +26,7 @@ "strength": 5, "cost": 25, "obsoleteTech": "Scientific Theory", - "uniques": ["Ignores terrain cost"], + "uniques": ["Ignores terrain cost", "May upgrade to [Archer] through ruins-like effects"], "attackSound": "nonmetalhit" }, { @@ -37,6 +37,7 @@ "cost": 40, "obsoleteTech": "Metal Casting", "upgradesTo": "Swordsman", + "uniques" : ["May upgrade to [Spearman] through ruins-like effects"], "attackSound": "nonmetalhit", "civilopediaText": [ {"text": "This is your basic, club-swinging fighter."} diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 26591ab074..c4516a9828 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1141,4 +1141,8 @@ in all cities with a world wonder = in all cities connected to capital = in all cities with a garrison = +# Uniques not found in JSON files + +Only available after [] turns = +This Unit upgrades for free = diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index b72b8b105f..19392990bb 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -45,7 +45,6 @@ object Constants { const val fountainOfYouth = "Fountain of Youth" const val barbarianEncampment = "Barbarian encampment" - const val ancientRuins = "Ancient ruins" const val peaceTreaty = "Peace Treaty" const val researchAgreement = "Research Agreement" diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 35b3246076..8854ffa3d3 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -240,9 +240,14 @@ object GameStarter { if(civ.isCityState()) addCityStateLuxury(gameInfo, startingLocation) - for (tile in startingLocation.getTilesInDistance(3)) - if (tile.improvement == Constants.ancientRuins) + for (tile in startingLocation.getTilesInDistance(3)) { + if (tile.improvement != null + && !tile.improvement!!.startsWith("StartingLocation") + && tile.getTileImprovement()!!.isAncientRuinsEquivalent() + ) { tile.improvement = null // Remove ancient ruins in immediate vicinity + } + } fun placeNearStartingPosition(unitName: String) { civ.placeUnitNearTile(startingLocation.position, unitName) diff --git a/core/src/com/unciv/logic/automation/UnitAutomation.kt b/core/src/com/unciv/logic/automation/UnitAutomation.kt index ad75592065..4134228131 100644 --- a/core/src/com/unciv/logic/automation/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/UnitAutomation.kt @@ -48,8 +48,11 @@ object UnitAutomation { val unitDistanceToTiles = unit.movement.getDistanceToTiles() val tileWithRuinOrEncampment = unitDistanceToTiles.keys .firstOrNull { - (it.improvement == Constants.ancientRuins || it.improvement == Constants.barbarianEncampment) - && unit.movement.canMoveTo(it) + ( + (it.improvement != null && it.getTileImprovement()!!.isAncientRuinsEquivalent()) + || it.improvement == Constants.barbarianEncampment + ) + && unit.movement.canMoveTo(it) } if (tileWithRuinOrEncampment == null) return false diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 2af535de0f..97c1269e6b 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -7,6 +7,7 @@ import com.unciv.logic.GameInfo import com.unciv.logic.UncivShowableException import com.unciv.logic.automation.NextTurnAutomation import com.unciv.logic.city.CityInfo +import com.unciv.logic.civilization.RuinsManager.RuinsManager import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyManager import com.unciv.logic.civilization.diplomacy.DiplomaticStatus @@ -86,6 +87,7 @@ class CivilizationInfo { var goldenAges = GoldenAgeManager() var greatPeople = GreatPersonManager() var victoryManager = VictoryManager() + var ruinsManager = RuinsManager() var diplomacy = HashMap() var notifications = ArrayList() val popupAlerts = ArrayList() @@ -133,6 +135,7 @@ class CivilizationInfo { toReturn.questManager = questManager.clone() toReturn.goldenAges = goldenAges.clone() toReturn.greatPeople = greatPeople.clone() + toReturn.ruinsManager = ruinsManager.clone() toReturn.victoryManager = victoryManager.clone() toReturn.allyCivName = allyCivName for (diplomacyManager in diplomacy.values.map { it.clone() }) @@ -510,6 +513,7 @@ class CivilizationInfo { tech.civInfo = this tech.setTransients() + ruinsManager.setTransients(this) for (diplomacyManager in diplomacy.values) { diplomacyManager.civInfo = this diff --git a/core/src/com/unciv/logic/civilization/Notification.kt b/core/src/com/unciv/logic/civilization/Notification.kt index 337979e097..66515babca 100644 --- a/core/src/com/unciv/logic/civilization/Notification.kt +++ b/core/src/com/unciv/logic/civilization/Notification.kt @@ -1,6 +1,7 @@ package com.unciv.logic.civilization import com.badlogic.gdx.math.Vector2 +import com.unciv.models.stats.Stat import com.unciv.ui.cityscreen.CityScreen import com.unciv.ui.pickerscreens.TechPickerScreen import com.unciv.ui.trade.DiplomacyScreen @@ -18,6 +19,12 @@ object NotificationIcon { const val Diplomacy = "OtherIcons/Diplomacy" const val City = "ImprovementIcons/City center" const val Citadel = "ImprovementIcons/Citadel" + const val Happiness = "StatIcons/Happiness" + const val Population = "StatIcons/Population" + const val CityState = "NationIcons/CityState" + const val Production = "StatIcons/Production" + const val Food = "StatIcons/Food" + const val Faith = "StatIcons/Faith" } /** diff --git a/core/src/com/unciv/logic/civilization/ReligionManager.kt b/core/src/com/unciv/logic/civilization/ReligionManager.kt index a44154a63d..d1d0af6c49 100644 --- a/core/src/com/unciv/logic/civilization/ReligionManager.kt +++ b/core/src/com/unciv/logic/civilization/ReligionManager.kt @@ -24,7 +24,8 @@ class ReligionManager { // But the other one should still be _somewhere_. So our only option is to have the GameInfo // contain the master list, and the ReligionManagers retrieve it from there every time the game loads. - private var greatProphetsEarned = 0 + var greatProphetsEarned = 0 + private set var religionState = ReligionState.None private set @@ -62,8 +63,9 @@ class ReligionManager { storedFaith += faithFromNewTurn } - private fun faithForPantheon() = 10 + civInfo.gameInfo.civilizations.count { it.isMajorCiv() && it.religionManager.religion != null } * 5 - + fun faithForPantheon(additionalCivs: Int = 0) = + 10 + (civInfo.gameInfo.civilizations.count { it.isMajorCiv() && it.religionManager.religion != null } + additionalCivs) * 5 + fun canFoundPantheon(): Boolean { if (!civInfo.gameInfo.hasReligionEnabled()) return false if (religionState != ReligionState.None) return false @@ -92,7 +94,7 @@ class ReligionManager { // https://www.reddit.com/r/civ/comments/2m82wu/can_anyone_detail_the_finer_points_of_great/ // Game files (globaldefines.xml) - private fun faithForNextGreatProphet() = ( + fun faithForNextGreatProphet() = ( (200 + 100 * greatProphetsEarned * (greatProphetsEarned + 1) / 2) * civInfo.gameInfo.gameParameters.gameSpeed.modifier ).toInt() diff --git a/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt b/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt new file mode 100644 index 0000000000..ae54f4ca06 --- /dev/null +++ b/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt @@ -0,0 +1,63 @@ +package com.unciv.logic.civilization.RuinsManager + +import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.map.MapUnit +import com.unciv.models.ruleset.RuinReward +import com.unciv.models.ruleset.UniqueTriggerActivation +import kotlin.random.Random + +class RuinsManager { + var lastChosenRewards: MutableList = mutableListOf("", "") + private fun rememberReward(reward: String) { + lastChosenRewards[0] = lastChosenRewards[1] + lastChosenRewards[1] = reward + } + + @Transient + lateinit var civInfo: CivilizationInfo + @Transient + lateinit var validRewards: List + + fun clone(): RuinsManager { + val toReturn = RuinsManager() + toReturn.lastChosenRewards = lastChosenRewards + return toReturn + } + + fun setTransients(civInfo: CivilizationInfo) { + this.civInfo = civInfo + validRewards = civInfo.gameInfo.ruleSet.ruinRewards.values.toList() + } + + fun selectNextRuinsReward(triggeringUnit: MapUnit) { + val tileBasedRandom = Random(triggeringUnit.getTile().position.toString().hashCode()) + val availableRewards = validRewards.filter { it.name !in lastChosenRewards } + + // This might be a dirty way to do this, but it works. + // For each possible reward, this creates a list with reward.weight amount of copies of this reward + // These lists are then combined into a single list, and the result is shuffled. + val possibleRewards = availableRewards.flatMap { reward -> List(reward.weight) { reward } }.shuffled(tileBasedRandom) + + for (possibleReward in possibleRewards) { + if (civInfo.gameInfo.difficulty in possibleReward.excludedDifficulties) continue + if ("Hidden when religion is disabled" in possibleReward.uniques && !civInfo.gameInfo.hasReligionEnabled()) continue + if ("Hidden after generating a Great Prophet" in possibleReward.uniques && civInfo.religionManager.greatProphetsEarned > 0) continue + if (possibleReward.uniqueObjects.any { unique -> + unique.placeholderText == "Only available after [] turns" + && unique.params[0].toInt() < civInfo.gameInfo.turns + }) continue + + var atLeastOneUniqueHadEffect = false + for (unique in possibleReward.uniqueObjects) { + atLeastOneUniqueHadEffect = + atLeastOneUniqueHadEffect + || UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo, tile = triggeringUnit.getTile(), notification = possibleReward.notification) + || UniqueTriggerActivation.triggerUnitwideUnique(unique, triggeringUnit, notification = possibleReward.notification) + } + if (atLeastOneUniqueHadEffect) { + rememberReward(possibleReward.name) + break + } + } + } +} diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index e51e920c90..ca19fb5663 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -14,7 +14,6 @@ import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.UnitType import java.text.DecimalFormat -import kotlin.random.Random /** * The immutable properties and mutable game state of an individual unit present on the map @@ -114,13 +113,7 @@ class MapUnit { var abilityUsedCount: HashMap = hashMapOf() var religion: String? = null - - companion object { - private const val ANCIENT_RUIN_MAP_REVEAL_OFFSET = 4 - private const val ANCIENT_RUIN_MAP_REVEAL_RANGE = 4 - private const val ANCIENT_RUIN_MAP_REVEAL_CHANCE = 0.8f - } - + //region pure functions fun clone(): MapUnit { val toReturn = MapUnit() @@ -212,6 +205,18 @@ class MapUnit { return getUniques().any { it.placeholderText == unique } } + fun copyStatisticsTo(newUnit: MapUnit) { + newUnit.health = health + newUnit.instanceName = instanceName + newUnit.currentMovement = currentMovement + newUnit.attacksThisTurn = attacksThisTurn + newUnit.isTransported = isTransported + newUnit.promotions = promotions.clone() + + newUnit.updateUniques() + newUnit.updateVisibleTiles() + } + /** * Determines this (land or sea) unit's current maximum vision range from unit properties, civ uniques and terrain. * @return Maximum distance of tiles this unit may possibly see @@ -346,16 +351,20 @@ class MapUnit { return unit } - fun canUpgrade(): Boolean { + /** @param ignoreRequired: Ignore possible tech/policy/building requirements. + * Used for upgrading units via ancient ruins. + */ + fun canUpgrade(unitToUpgradeTo: BaseUnit = getUnitToUpgradeTo(), ignoreRequired: Boolean = false): Boolean { // 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, // if we don't remove the unit before the check it's return false! - val unitToUpgradeTo = getUnitToUpgradeTo() if (name == unitToUpgradeTo.name) return false civInfo.removeUnit(this) - val canUpgrade = unitToUpgradeTo.isBuildable(civInfo) + val canUpgrade = + if (ignoreRequired) unitToUpgradeTo.isBuildableIgnoringTechs(civInfo) + else unitToUpgradeTo.isBuildable(civInfo) civInfo.addUnit(this) return canUpgrade } @@ -678,7 +687,11 @@ class MapUnit { // getAncientRuinBonus, if it places a new unit, does too currentTile = tile - if (tile.improvement == Constants.ancientRuins && civInfo.isMajorCiv()) + if (civInfo.isMajorCiv() + && tile.improvement != null + && !tile.improvement!!.startsWith("StartingLocation ") + && tile.getTileImprovement()!!.isAncientRuinsEquivalent() + ) getAncientRuinBonus(tile) if (tile.improvement == Constants.barbarianEncampment && !civInfo.isBarbarian()) clearEncampment(tile) @@ -754,114 +767,7 @@ class MapUnit { private fun getAncientRuinBonus(tile: TileInfo) { tile.improvement = null - val tileBasedRandom = Random(tile.position.toString().hashCode()) - val actions: ArrayList<() -> Unit> = ArrayList() - - fun goldBonus() { - val amount = listOf(25, 60, 100).random(tileBasedRandom) - civInfo.addGold(amount) - civInfo.addNotification( - "We have found a stash of [$amount] gold in the ruins!", - tile.position, - NotificationIcon.Gold - ) - } - - if (civInfo.cities.isNotEmpty()) actions.add { - val city = civInfo.cities.random(tileBasedRandom) - city.population.addPopulation(1) - val locations = LocationAction(listOf(tile.position, city.location)) - civInfo.addNotification( - "We have found survivors in the ruins - population added to [" + city.name + "]", - locations, - NotificationIcon.Growth - ) - } - - val researchableFirstEraTechs = tile.tileMap.gameInfo.ruleSet.technologies.values - .filter { - !civInfo.tech.isResearched(it.name) - && civInfo.tech.canBeResearched(it.name) - && civInfo.gameInfo.ruleSet.getEraNumber(it.era()) == 1 - } - if (researchableFirstEraTechs.isNotEmpty()) - actions.add { - val tech = researchableFirstEraTechs.random(tileBasedRandom).name - civInfo.tech.addTechnology(tech) - civInfo.addNotification( - "We have discovered the lost technology of [$tech] in the ruins!", - tile.position, - NotificationIcon.Science, - tech - ) - } - - 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, militaryUnit) - ).filter { civInfo.gameInfo.ruleSet.units.containsKey(it) } - if (possibleUnits.isNotEmpty()) - actions.add { - val chosenUnit = possibleUnits.random(tileBasedRandom) - // placeUnitNearTile _can_ fail, and since this code can run behind a try with empty - // catch inside nested thread switches - petter play it safe - if (civInfo.placeUnitNearTile(tile.position, chosenUnit) == null) { - goldBonus() - } else { - civInfo.addNotification( - "A [$chosenUnit] has joined us!", - tile.position, - chosenUnit - ) - } - } - - if (!isCivilian()) - actions.add { - promotions.XP += 10 - civInfo.addNotification( - "An ancient tribe trains our [$name] in their ways of combat!", - tile.position, - name - ) - } - - actions.add { goldBonus() } - - actions.add { - civInfo.policies.addCulture(20) - civInfo.addNotification( - "We have discovered cultural artifacts in the ruins! (+20 Culture)", - tile.position, - NotificationIcon.Culture - ) - } - - // Map of the surrounding area - val revealCenter = tile.getTilesAtDistance(ANCIENT_RUIN_MAP_REVEAL_OFFSET) - .filter { it.position !in civInfo.exploredTiles } - .toList() - .randomOrNull(tileBasedRandom) - if (revealCenter != null) - actions.add { - val tilesToReveal = revealCenter - .getTilesInDistance(ANCIENT_RUIN_MAP_REVEAL_RANGE) - .filter { Random.nextFloat() < ANCIENT_RUIN_MAP_REVEAL_CHANCE } - .map { it.position } - civInfo.exploredTiles.addAll(tilesToReveal) - civInfo.updateViewableTiles() - civInfo.addNotification( - "We have found a crudely-drawn map in the ruins!", - tile.position, - "ImprovementIcons/Ancient ruins" - ) - } - - (actions.random(tileBasedRandom))() + civInfo.ruinsManager.selectNextRuinsReward(this) } fun assignOwner(civInfo: CivilizationInfo, updateCivInfo: Boolean = true) { diff --git a/core/src/com/unciv/logic/map/UnitPromotions.kt b/core/src/com/unciv/logic/map/UnitPromotions.kt index ca79136e10..6388e9f46c 100644 --- a/core/src/com/unciv/logic/map/UnitPromotions.kt +++ b/core/src/com/unciv/logic/map/UnitPromotions.kt @@ -1,10 +1,12 @@ package com.unciv.logic.map +import com.unciv.models.ruleset.UniqueTriggerActivation import com.unciv.models.ruleset.unit.Promotion class UnitPromotions{ @Transient lateinit var unit:MapUnit - var XP=0 + @Suppress("PropertyName") + var XP = 0 var promotions = HashSet() // The number of times this unit has been promoted // some promotions don't come from being promoted but from other things, @@ -40,8 +42,9 @@ class UnitPromotions{ } fun doDirectPromotionEffects(promotion: Promotion) { - for (unique in promotion.uniqueObjects.filter { it.placeholderText == "Heal this unit by [] HP"}) - unit.healBy(unique.params[0].toInt()) + for (unique in promotion.uniqueObjects) { + UniqueTriggerActivation.triggerUnitwideUnique(unique, unit) + } } fun getAvailablePromotions(): List { @@ -52,9 +55,9 @@ class UnitPromotions{ fun clone(): UnitPromotions { val toReturn = UnitPromotions() - toReturn.XP=XP + toReturn.XP = XP toReturn.promotions.addAll(promotions) - toReturn.numberOfPromotions=numberOfPromotions + toReturn.numberOfPromotions = numberOfPromotions return toReturn } diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 519e2ebb7d..87ef87a821 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -105,13 +105,14 @@ class MapGenerator(val ruleset: Ruleset) { } private fun spreadAncientRuins(map: TileMap) { - if (map.mapParameters.noRuins || !ruleset.tileImprovements.containsKey(Constants.ancientRuins)) + val ruinsEquivalents = ruleset.tileImprovements.filter { it.value.isAncientRuinsEquivalent() } + if (map.mapParameters.noRuins || ruinsEquivalents.isEmpty() ) return val suitableTiles = map.values.filter { it.isLand && !it.isImpassible() } val locations = randomness.chooseSpreadOutLocations(suitableTiles.size / 50, suitableTiles, 10) for (tile in locations) - tile.improvement = Constants.ancientRuins + tile.improvement = ruinsEquivalents.keys.random() } private fun spreadResources(tileMap: TileMap) { diff --git a/core/src/com/unciv/models/ruleset/RuinReward.kt b/core/src/com/unciv/models/ruleset/RuinReward.kt new file mode 100644 index 0000000000..60b7a17e3c --- /dev/null +++ b/core/src/com/unciv/models/ruleset/RuinReward.kt @@ -0,0 +1,12 @@ +package com.unciv.models.ruleset + +import com.unciv.models.stats.INamed + +class RuinReward : INamed { + override lateinit var name: String + val notification: String = "" + val uniques: List = listOf() + val uniqueObjects: List by lazy { uniques.map { Unique(it) } } + val excludedDifficulties: List = listOf() + val weight: Int = 1 +} \ 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 558508e2be..3f9be944e0 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -52,13 +52,14 @@ class Ruleset { var name = "" val beliefs = LinkedHashMap() - val religions = ArrayList() val buildings = LinkedHashMap() val difficulties = LinkedHashMap() val eras = LinkedHashMap() val nations = LinkedHashMap() val policies = LinkedHashMap() val policyBranches = LinkedHashMap() + val religions = ArrayList() + val ruinRewards = LinkedHashMap() val quests = LinkedHashMap() val specialists = LinkedHashMap() val technologies = LinkedHashMap() @@ -97,6 +98,7 @@ class Ruleset { beliefs.putAll(ruleset.beliefs) quests.putAll(ruleset.quests) religions.addAll(ruleset.religions) + ruinRewards.putAll(ruleset.ruinRewards) specialists.putAll(ruleset.specialists) technologies.putAll(ruleset.technologies) for (techToRemove in ruleset.modOptions.techsToRemove) technologies.remove(techToRemove) @@ -122,6 +124,7 @@ class Ruleset { nations.clear() policies.clear() religions.clear() + ruinRewards.clear() quests.clear() technologies.clear() terrains.clear() @@ -210,6 +213,10 @@ class Ruleset { if (religionsFile.exists()) religions += jsonParser.getFromJson(Array::class.java, religionsFile).toList() + val ruinRewardsFile = folderHandle.child("Ruins.json") + if (ruinRewardsFile.exists()) + ruinRewards += createHashmap(jsonParser.getFromJson(Array::class.java, ruinRewardsFile)) + val nationsFile = folderHandle.child("Nations.json") if (nationsFile.exists()) { nations += createHashmap(jsonParser.getFromJson(Array::class.java, nationsFile)) @@ -488,10 +495,16 @@ object RulesetCache : HashMap() { } newRuleset.updateBuildingCosts() // only after we've added all the mods can we calculate the building costs + // This one should be temporary if (newRuleset.unitTypes.isEmpty()) { newRuleset.unitTypes.putAll(getBaseRuleset().unitTypes) } + // This one should be permanent + if (newRuleset.ruinRewards.isEmpty()) { + newRuleset.ruinRewards.putAll(getBaseRuleset().ruinRewards) + } + return newRuleset } diff --git a/core/src/com/unciv/models/ruleset/Unique.kt b/core/src/com/unciv/models/ruleset/Unique.kt index f8c58c983f..a8b333566f 100644 --- a/core/src/com/unciv/models/ruleset/Unique.kt +++ b/core/src/com/unciv/models/ruleset/Unique.kt @@ -1,11 +1,18 @@ package com.unciv.models.ruleset +import com.badlogic.gdx.math.Vector2 import com.unciv.logic.city.CityInfo -import com.unciv.logic.civilization.CivFlags -import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.* +import com.unciv.logic.map.MapUnit +import com.unciv.logic.map.TileInfo +import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats +import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText +import com.unciv.models.translations.hasPlaceholderParameters +import com.unciv.ui.worldscreen.unit.UnitActions +import kotlin.random.Random class Unique(val text:String){ val placeholderText = text.getPlaceholderText() @@ -34,83 +41,285 @@ class UniqueMap:HashMap>() { fun getAllUniques() = this.asSequence().flatMap { it.value.asSequence() } } -// Buildings, techs and policies can have 'triggered' effects +// Buildings, techs, policies, ancient ruins and promotions can have 'triggered' effects object UniqueTriggerActivation { - fun triggerCivwideUnique(unique: Unique, civInfo: CivilizationInfo, cityInfo: CityInfo? = null) { - val chosenCity = if (cityInfo != null) cityInfo else civInfo.cities.firstOrNull { it.isCapital() } + /** @return boolean whether an action was successfully preformed */ + fun triggerCivwideUnique( + unique: Unique, + civInfo: CivilizationInfo, + cityInfo: CityInfo? = null, + tile: TileInfo? = null, + notification: String? = null + ): Boolean { + val chosenCity = + if (cityInfo != null) cityInfo + else civInfo.cities.firstOrNull { it.isCapital() } + val tileBasedRandom = + if (tile != null) Random(tile.position.toString().hashCode()) + else Random(-550) // Very random indeed when (unique.placeholderText) { "Free [] appears" -> { val unitName = unique.params[0] val unit = civInfo.gameInfo.ruleSet.units[unitName] - if (chosenCity != null && unit != null && (!unit.uniques.contains("Founds a new city") || !civInfo.isOneCityChallenger())) - civInfo.addUnit(unitName, chosenCity) + if (chosenCity == null || unit == null || (unit.uniques.contains("Founds a new city") && civInfo.isOneCityChallenger())) + return false + + val placedUnit = civInfo.addUnit(unitName, chosenCity) + if (notification != null && placedUnit != null) { + civInfo.addNotification( + notification, + placedUnit.getTile().position, + placedUnit.name + ) + } + return true } "[] free [] units appear" -> { val unitName = unique.params[1] val unit = civInfo.gameInfo.ruleSet.units[unitName] - if (chosenCity != null && unit != null && (!unit.uniques.contains("Founds a new city") || !civInfo.isOneCityChallenger())) - for (i in 1..unique.params[0].toInt()) - civInfo.addUnit(unitName, chosenCity) + if (chosenCity == null || unit == null || (unit.uniques.contains("Founds a new city") && civInfo.isOneCityChallenger())) + return false + + val tilesUnitsWerePlacedOn: MutableList = mutableListOf() + for (i in 1..unique.params[0].toInt()) { + val placedUnit = civInfo.addUnit(unitName, chosenCity) + if (placedUnit != null) + tilesUnitsWerePlacedOn.add(placedUnit.getTile().position) + } + if (notification != null && tilesUnitsWerePlacedOn.isNotEmpty()) { + civInfo.addNotification( + notification, + LocationAction(tilesUnitsWerePlacedOn), + civInfo.getEquivalentUnit(unit).name + ) + } + return true + } + // Differs from "Free [] appears" in that it spawns near the ruins instead of in a city + "Free [] found in the ruins" -> { + val unit = civInfo.getEquivalentUnit(unique.params[0]) + val placingTile = + tile ?: civInfo.cities.random().getCenterTile() + + val placedUnit = civInfo.placeUnitNearTile(placingTile.position, unit.name) + if (notification != null && placedUnit != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(unique.params[0]) + else notification + civInfo.addNotification( + notificationText, + placedUnit.getTile().position, + placedUnit.name + ) + } + + return placedUnit != null + } + + // spectators get all techs at start of game, and if (in a mod) a tech gives a free policy, the game gets stuck on the policy picker screen + "Free Social Policy" -> { + if (civInfo.isSpectator()) return false + civInfo.policies.freePolicies++ + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Culture) + } + return true + } + "[] Free Social Policies" -> { + if (civInfo.isSpectator()) return false + civInfo.policies.freePolicies += unique.params[0].toInt() + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Culture) + } + return true + } + "Empire enters golden age" -> { + civInfo.goldenAges.enterGoldenAge() + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Happiness) + } + return true } - // spectators get all techs at start of game, and if (in a mod) a tech gives a free policy, the game stucks on the policy picker screen - "Free Social Policy" -> if (!civInfo.isSpectator()) civInfo.policies.freePolicies++ - "[] Free Social Policies" -> if (!civInfo.isSpectator()) civInfo.policies.freePolicies += unique.params[0].toInt() - "Empire enters golden age" -> civInfo.goldenAges.enterGoldenAge() "Free Great Person" -> { - if (civInfo.isSpectator()) return - if (civInfo.isPlayerCivilization()) civInfo.greatPeople.freeGreatPeople++ - else { + if (civInfo.isSpectator()) return false + if (civInfo.isPlayerCivilization()) { + civInfo.greatPeople.freeGreatPeople++ + if (notification != null) + civInfo.addNotification(notification) // Anyone an idea for a good icon? + return true + } else { val greatPeople = civInfo.getGreatPeople() - if (greatPeople.isEmpty()) return + if (greatPeople.isEmpty()) return false var greatPerson = civInfo.getGreatPeople().random() val preferredVictoryType = civInfo.victoryType() if (preferredVictoryType == VictoryType.Cultural) { - val culturalGP = greatPeople.firstOrNull { it.uniques.contains("Great Person - [Culture]") } + val culturalGP = + greatPeople.firstOrNull { it.uniques.contains("Great Person - [Culture]") } if (culturalGP != null) greatPerson = culturalGP } if (preferredVictoryType == VictoryType.Scientific) { - val scientificGP = greatPeople.firstOrNull { it.uniques.contains("Great Person - [Science]") } + val scientificGP = + greatPeople.firstOrNull { it.uniques.contains("Great Person - [Science]") } if (scientificGP != null) greatPerson = scientificGP } - civInfo.addUnit(greatPerson.name, chosenCity) + return civInfo.addUnit(greatPerson.name, chosenCity) != null } } // Deprecated since 3.15.4 - "+1 population in each city" -> - for (city in civInfo.cities) { - city.population.addPopulation(1) - } - // - "[] population []" -> + "+1 population in each city" -> { + for (city in civInfo.cities) { + city.population.addPopulation(1) + } + if (notification != null) { + civInfo.addNotification( + notification, + LocationAction(civInfo.cities.map { it.location }), + NotificationIcon.Population + ) + } + return true + } + // + "[] population []" -> { + val citiesWithPopulationChanged: MutableList = mutableListOf() for (city in civInfo.cities) { if (city.matchesFilter(unique.params[1])) { city.population.addPopulation(unique.params[0].toInt()) + citiesWithPopulationChanged.add(city.location) } } - "Free Technology" -> if (!civInfo.isSpectator()) civInfo.tech.freeTechs += 1 - "[] Free Technologies" -> if (!civInfo.isSpectator()) civInfo.tech.freeTechs += unique.params[0].toInt() + if (notification != null && citiesWithPopulationChanged.isNotEmpty()) + civInfo.addNotification( + notification, + LocationAction(citiesWithPopulationChanged), + NotificationIcon.Population + ) + return citiesWithPopulationChanged.isNotEmpty() + } + "[] population in a random city" -> { + if (civInfo.cities.isEmpty()) return false + val randomCity = civInfo.cities.random(tileBasedRandom) + randomCity.population.addPopulation(unique.params[0].toInt()) + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(randomCity.name) + else notification + civInfo.addNotification( + notificationText, + randomCity.location, + NotificationIcon.Population + ) + } + return true + } + + "Free Technology" -> { + if (civInfo.isSpectator()) return false + civInfo.tech.freeTechs += 1 + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Science) + } + return true + } + "[] Free Technologies" -> { + if (civInfo.isSpectator()) return false + civInfo.tech.freeTechs += unique.params[0].toInt() + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.Science) + } + return true + } + "[] free random researchable Tech(s) from the []" -> { + val researchableTechsFromThatEra = civInfo.gameInfo.ruleSet.technologies.values + .filter { + (it.column!!.era == unique.params[1] || unique.params[1] == "any era") + && civInfo.tech.canBeResearched(it.name) + } + if (researchableTechsFromThatEra.isEmpty()) return false - "Quantity of strategic resources produced by the empire increased by 100%" -> civInfo.updateDetailedCivResources() - "+[]% attack strength to all [] Units for [] turns" -> civInfo.temporaryUniques.add(Pair(unique, unique.params[2].toInt())) + val techsToResearch = researchableTechsFromThatEra.shuffled(tileBasedRandom) + .take(unique.params[0].toInt()) + for (tech in techsToResearch) + civInfo.tech.addTechnology(tech.name) - "Reveals the entire map" -> civInfo.exploredTiles.addAll(civInfo.gameInfo.tileMap.values.asSequence().map { it.position }) + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(*(techsToResearch.map { it.name } + .toTypedArray())) + else notification + civInfo.addNotification(notificationText, NotificationIcon.Science) + } + + return true + } + + "Quantity of strategic resources produced by the empire increased by 100%" -> { + civInfo.updateDetailedCivResources() + if (notification != null) { + civInfo.addNotification( + notification, + NotificationIcon.War + ) // I'm open for better icons + } + return true + } + "+[]% attack strength to all [] Units for [] turns" -> { + civInfo.temporaryUniques.add(Pair(unique, unique.params[2].toInt())) + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.War) + } + return true + } + + "Reveals the entire map" -> { + if (notification != null) { + civInfo.addNotification(notification, "UnitIcons/Scout") + } + return civInfo.exploredTiles.addAll( + civInfo.gameInfo.tileMap.values.asSequence().map { it.position }) + } "[] units gain the [] promotion" -> { val filter = unique.params[0] val promotion = unique.params[1] - for (unit in civInfo.getCivUnits()) + + val promotedUnitLocations: MutableList = mutableListOf() + for (unit in civInfo.getCivUnits()) { if (unit.matchesFilter(filter) - || civInfo.gameInfo.ruleSet.unitPromotions.values.any { - it.name == promotion && unit.type!!.name in it.unitTypes + && civInfo.gameInfo.ruleSet.unitPromotions.values.any { + it.name == promotion && unit.type.name in it.unitTypes } ) { unit.promotions.addPromotion(promotion, isFree = true) + promotedUnitLocations.add(unit.getTile().position) } + } + + if (notification != null) { + civInfo.addNotification( + notification, + LocationAction(promotedUnitLocations), + "unitPromotionIcons/${unique.params[1]}" + ) + } + return promotedUnitLocations.isNotEmpty() + } + + "Allied City-States will occasionally gift Great People" -> { + civInfo.addFlag( + CivFlags.CityStateGreatPersonGift.name, + civInfo.turnsForGreatPersonFromCityState() / 2 + ) + if (notification != null) { + civInfo.addNotification(notification, NotificationIcon.CityState) + } + return true } - "Allied City-States will occasionally gift Great People" -> - civInfo.addFlag(CivFlags.CityStateGreatPersonGift.name, civInfo.turnsForGreatPersonFromCityState() / 2) // The mechanics for granting great people are wonky, but basically the following happens: // Based on the game speed, a timer with some amount of turns is set, 40 on regular speed // Every turn, 1 is subtracted from this timer, as long as you have at least 1 city state ally @@ -126,10 +335,191 @@ object UniqueTriggerActivation { // Note that the way this is implemented now, this unique does NOT stack // I could parametrize the [Allied], but eh. - "Triggers voting for the Diplomatic Victory" -> + + "Gain [] []" -> { + if (Stat.values().none { it.name == unique.params[1] }) return false + val stat = Stat.valueOf(unique.params[1]) + + if (stat !in listOf(Stat.Gold, Stat.Faith, Stat.Science, Stat.Culture) + || unique.params[0].toIntOrNull() == null + ) return false + + civInfo.addStat(stat, unique.params[0].toInt()) + if (notification != null) + civInfo.addNotification(notification, stat.notificationIcon) + return true + } + "Gain []-[] []" -> { + if (Stat.values().none { it.name == unique.params[2] }) return false + val stat = Stat.valueOf(unique.params[2]) + + if (stat !in listOf(Stat.Gold, Stat.Faith, Stat.Science, Stat.Culture) + || unique.params[0].toIntOrNull() == null + || unique.params[1].toIntOrNull() == null + ) return false + + val foundStatAmount = + (tileBasedRandom.nextInt(unique.params[0].toInt(), unique.params[1].toInt()) * + civInfo.gameInfo.gameParameters.gameSpeed.modifier + ).toInt() + + civInfo.addStat( + Stat.valueOf(unique.params[2]), + foundStatAmount + ) + + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) { + notification.fillPlaceholders(foundStatAmount.toString()) + } else notification + civInfo.addNotification(notificationText, stat.notificationIcon) + } + + return true + } + "Gain enough Faith for a Pantheon" -> { + if (civInfo.religionManager.religionState != ReligionState.None) return false + val gainedFaith = civInfo.religionManager.faithForPantheon(2) + if (gainedFaith == 0) return false + + civInfo.addStat(Stat.Faith, gainedFaith) + + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(gainedFaith.toString()) + else notification + civInfo.addNotification(notificationText, NotificationIcon.Faith) + } + + return true + } + "Gain enough Faith for []% of a Great Prophet" -> { + val gainedFaith = + (civInfo.religionManager.faithForNextGreatProphet() * (unique.params[0].toFloat() / 100f)).toInt() + if (gainedFaith == 0) return false + + civInfo.addStat(Stat.Faith, gainedFaith) + + if (notification != null) { + val notificationText = + if (notification.hasPlaceholderParameters()) + notification.fillPlaceholders(gainedFaith.toString()) + else notification + civInfo.addNotification(notificationText, NotificationIcon.Faith) + } + + return true + } + + "Reveal up to [] [] within a [] tile radius" -> { + if (tile == null) return false + val nearbyRevealableTiles = tile + .getTilesInDistance(unique.params[2].toInt()) + .filter { + !civInfo.exploredTiles.contains(it.position) && it.matchesFilter( + unique.params[1] + ) + } + .map { it.position } + if (nearbyRevealableTiles.none()) return false + civInfo.exploredTiles.addAll(nearbyRevealableTiles + .shuffled(tileBasedRandom) + .apply { + if (unique.params[0] != "All") this.take(unique.params[0].toInt()) + } + ) + + if (notification != null) { + civInfo.addNotification( + notification, + LocationAction(nearbyRevealableTiles.toList()) + ) // We really need a barbarian icon + } + + return true + } + "From a randomly chosen tile [] tiles away from the ruins, reveal tiles up to [] tiles away with []% chance" -> { + if (tile == null) return false + val revealCenter = tile.getTilesAtDistance(unique.params[0].toInt()) + .filter { it.position !in civInfo.exploredTiles } + .toList() + .randomOrNull(tileBasedRandom) + if (revealCenter == null) return false + val tilesToReveal = revealCenter + .getTilesInDistance(unique.params[1].toInt()) + .map { it.position } + .filter { tileBasedRandom.nextFloat() < unique.params[2].toFloat() / 100f } + civInfo.exploredTiles.addAll(tilesToReveal) + civInfo.updateViewableTiles() + if (notification != null) + civInfo.addNotification( + notification, + tile.position, + "ImprovementIcons/Ancient ruins" + ) + } + "Triggers voting for the Diplomatic Victory" -> { for (civ in civInfo.gameInfo.civilizations) if (!civ.isBarbarian() && !civ.isSpectator()) - civ.addFlag(CivFlags.TurnsTillNextDiplomaticVote.name, civInfo.getTurnsBetweenDiplomaticVotings()) + civ.addFlag( + CivFlags.TurnsTillNextDiplomaticVote.name, + civInfo.getTurnsBetweenDiplomaticVotings() + ) + if (notification != null) + civInfo.addNotification(notification, NotificationIcon.Diplomacy) + return true + } } + return false + } + + /** @return boolean whether an action was successfully preformed */ + fun triggerUnitwideUnique( + unique: Unique, + unit: MapUnit, + notification: String? = null + ): Boolean { + when (unique.placeholderText) { + "Heal this unit by [] HP" -> { + unit.healBy(unique.params[0].toInt()) + if (notification != null) + unit.civInfo.addNotification(notification, unit.getTile().position) // Do we have a heal icon? + return true + } + "This Unit gains [] XP" -> { + if (!unit.baseUnit.isMilitary()) return false + unit.promotions.XP += unique.params[0].toInt() + if (notification != null) + unit.civInfo.addNotification(notification, unit.getTile().position) + return true + } + "This Unit upgrades for free" -> { + val upgradeAction = UnitActions.getUpgradeAction(unit, true) + ?: return false + upgradeAction.action!!() + if (notification != null) + unit.civInfo.addNotification(notification, unit.getTile().position) + return true + } + "This Unit upgrades for free including special upgrades" -> { + val upgradeAction = UnitActions.getAncientRuinsUpgradeAction(unit) + ?: return false + upgradeAction.action!!() + if (notification != null) + unit.civInfo.addNotification(notification, unit.getTile().position) + return true + } + "This Unit gains the [] promotion" -> { + val promotion = unit.civInfo.gameInfo.ruleSet.unitPromotions.keys.firstOrNull { it == unique.params[0] } + if (promotion == null) return false + unit.promotions.addPromotion(promotion, true) + if (notification != null) + unit.civInfo.addNotification(notification, unit.name) + return true + } + } + return false } } \ No newline at end of file diff --git a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt index 270c0bc9ea..4c354843a9 100644 --- a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt +++ b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt @@ -75,6 +75,7 @@ class TileImprovement : NamedStats(), ICivilopediaText { fun hasUnique(unique: String) = uniques.contains(unique) fun isGreatImprovement() = hasUnique("Great Improvement") fun isRoad() = RoadStatus.values().any { it != RoadStatus.None && it.name == this.name } + fun isAncientRuinsEquivalent() = hasUnique("Provides a random bonus when entered") /** * Check: Is this improvement allowed on a [given][name] terrain feature? diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 727e9f8b5a..d0926c51e3 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -45,6 +45,12 @@ class BaseUnit : INamed, INonPerpetualConstruction, CivilopediaText() { var promotions = HashSet() var obsoleteTech: String? = null var upgradesTo: String? = null + val specialUpgradesTo: String? by lazy { + uniqueObjects + .filter { it.placeholderText == "May upgrade to [] through ruins-like effects"} + .map { it.params[0] } + .firstOrNull() + } var replaces: String? = null var uniqueTo: String? = null var attackSound: String? = null @@ -262,10 +268,14 @@ class BaseUnit : INamed, INonPerpetualConstruction, CivilopediaText() { return "" } - fun getRejectionReason(civInfo: CivilizationInfo): String { + /** @param ignoreTechPolicyRequirements: its `true` value is used when upgrading via ancient ruins, + * as there we don't care whether we have the required tech, policy or building for the unit, + * but do still care whether we have the resources required for the unit + */ + fun getRejectionReason(civInfo: CivilizationInfo, ignoreTechPolicyRequirements: Boolean = false): String { if (uniques.contains("Unbuildable")) return "Unbuildable" - if (requiredTech != null && !civInfo.tech.isResearched(requiredTech!!)) return "$requiredTech not researched" - if (obsoleteTech != null && civInfo.tech.isResearched(obsoleteTech!!)) return "Obsolete by $obsoleteTech" + if (!ignoreTechPolicyRequirements && requiredTech != null && !civInfo.tech.isResearched(requiredTech!!)) return "$requiredTech not researched" + if (!ignoreTechPolicyRequirements && obsoleteTech != null && civInfo.tech.isResearched(obsoleteTech!!)) return "Obsolete by $obsoleteTech" if (uniqueTo != null && uniqueTo != civInfo.civName) return "Unique to $uniqueTo" if (civInfo.gameInfo.ruleSet.units.values.any { it.uniqueTo == civInfo.civName && it.replaces == name }) return "Our unique unit replaces this" @@ -279,9 +289,9 @@ class BaseUnit : INamed, INonPerpetualConstruction, CivilopediaText() { for (unique in uniqueObjects.filter { it.placeholderText == "Requires []" }) { val filter = unique.params[0] - if (filter in civInfo.gameInfo.ruleSet.buildings) { + if (!ignoreTechPolicyRequirements && filter in civInfo.gameInfo.ruleSet.buildings) { if (civInfo.cities.none { it.cityConstructions.containsBuildingOrEquivalent(filter) }) return unique.text // Wonder is not built - } else if (!civInfo.policies.adoptedPolicies.contains(filter)) return "Policy is not adopted" + } else if (!ignoreTechPolicyRequirements && !civInfo.policies.adoptedPolicies.contains(filter)) return "Policy is not adopted" } for ((resource, amount) in getResourceRequirements()) @@ -301,6 +311,12 @@ class BaseUnit : INamed, INonPerpetualConstruction, CivilopediaText() { return getRejectionReason(cityConstructions) == "" } + /** Preemptively as in: buildable without actually having the tech and/or policy required for it. + * Still checks for resource use and other things + */ + fun isBuildableIgnoringTechs(civInfo: CivilizationInfo) = + getRejectionReason(civInfo, true) == "" + override fun postBuildEvent(cityConstructions: CityConstructions, wasBought: Boolean): Boolean { val civInfo = cityConstructions.cityInfo.civInfo val unit = civInfo.placeUnitNearTile(cityConstructions.cityInfo.location, name) diff --git a/core/src/com/unciv/models/stats/Stat.kt b/core/src/com/unciv/models/stats/Stat.kt index 4855f4c902..7c8b0c39d3 100644 --- a/core/src/com/unciv/models/stats/Stat.kt +++ b/core/src/com/unciv/models/stats/Stat.kt @@ -1,13 +1,14 @@ package com.unciv.models.stats +import com.unciv.logic.civilization.NotificationIcon import com.unciv.models.UncivSound -enum class Stat (val sound: UncivSound) { - Production(UncivSound.Click), - Food(UncivSound.Click), - Gold(UncivSound.Coin), - Science(UncivSound.Chimes), - Culture(UncivSound.Paper), - Happiness(UncivSound.Click), - Faith(UncivSound.Choir), +enum class Stat(val notificationIcon: String, val purchaseSound: UncivSound) { + Production(NotificationIcon.Production, UncivSound.Click), + Food(NotificationIcon.Food, UncivSound.Click), + Gold(NotificationIcon.Gold, UncivSound.Coin), + Science(NotificationIcon.Science, UncivSound.Chimes), + Culture(NotificationIcon.Culture, UncivSound.Paper), + Happiness(NotificationIcon.Happiness, UncivSound.Click), + Faith(NotificationIcon.Faith, UncivSound.Choir); } \ No newline at end of file diff --git a/core/src/com/unciv/models/translations/Translations.kt b/core/src/com/unciv/models/translations/Translations.kt index ac40e1f71b..3033be2e39 100644 --- a/core/src/com/unciv/models/translations/Translations.kt +++ b/core/src/com/unciv/models/translations/Translations.kt @@ -276,6 +276,8 @@ fun String.equalsPlaceholderText(str:String): Boolean { return this.getPlaceholderText() == str } +fun String.hasPlaceholderParameters() = squareBraceRegex.containsMatchIn(this) + fun String.getPlaceholderParameters() = squareBraceRegex.findAll(this).map { it.groups[1]!!.value }.toList() /** Substitutes placeholders with [strings], respecting order of appearance. */ diff --git a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt index 30edbc1ae6..a06ebd7c44 100644 --- a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt @@ -419,7 +419,7 @@ class CityConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBase button.setText("Buy".tr() + " " + constructionBuyCost) button.add(ImageGetter.getStatIcon(stat.name)).size(20f).padBottom(2f) - button.onClick(stat.sound) { + button.onClick(stat.purchaseSound) { button.disable() cityScreen.closeAllPopups() diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index 23384ee962..0cc835da92 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -182,6 +182,7 @@ class NewGameScreen( try { newGame = GameStarter.startNewGame(gameSetupInfo) } catch (exception: Exception) { + exception.printStackTrace() Gdx.app.postRunnable { Popup(this).apply { addGoodSizedLabel("It looks like we can't make a map with the parameters you requested!".tr()).row() diff --git a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt index 9532a3ed77..4849f0080b 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt @@ -492,7 +492,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap for (tile in allWorldTileGroups) { if (tile.icons.populationIcon != null) tile.icons.populationIcon!!.color.a = fadeout if (tile.icons.improvementIcon != null && tile.tileInfo.improvement != Constants.barbarianEncampment - && tile.tileInfo.improvement != Constants.ancientRuins) + && tile.tileInfo.getTileImprovement()!!.isAncientRuinsEquivalent()) tile.icons.improvementIcon!!.color.a = fadeout if (tile.resourceImage != null) tile.resourceImage!!.color.a = fadeout } diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 67bee77fd6..ccf0a75760 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -298,35 +298,59 @@ object UnitActions { if (upgradeAction != null) actionList += upgradeAction } - fun getUpgradeAction(unit: MapUnit): UnitAction? { + fun getUpgradeAction(unit: MapUnit, isFree: Boolean = false): UnitAction? { val tile = unit.currentTile - if (unit.baseUnit().upgradesTo == null || tile.getOwner() != unit.civInfo - || !unit.canUpgrade()) return null - val goldCostOfUpgrade = unit.getCostOfUpgrade() + if (unit.baseUnit().upgradesTo == null || !unit.canUpgrade()) return null + if (tile.getOwner() != unit.civInfo && !isFree) return null + val goldCostOfUpgrade = + if (isFree) 0 + else unit.getCostOfUpgrade() val upgradedUnit = unit.getUnitToUpgradeTo() return UnitAction(UnitActionType.Upgrade, - title = "Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)", - action = { - unit.civInfo.addGold(-goldCostOfUpgrade) - val unitTile = unit.getTile() - unit.destroy() - val newUnit = unit.civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name)!! - newUnit.health = unit.health - newUnit.promotions = unit.promotions - newUnit.instanceName = unit.instanceName - - for (promotion in newUnit.baseUnit.promotions) - if (promotion !in newUnit.promotions.promotions) - newUnit.promotions.addPromotion(promotion, true) - - newUnit.updateUniques() - newUnit.updateVisibleTiles() - newUnit.currentMovement = 0f - }.takeIf { + title = "Upgrade to [${upgradedUnit.name}] ([$goldCostOfUpgrade] gold)", + action = { + unit.civInfo.addGold(-goldCostOfUpgrade) + val unitTile = unit.getTile() + unit.destroy() + val newUnit = unit.civInfo.placeUnitNearTile(unitTile.position, upgradedUnit.name)!! + unit.copyStatisticsTo(newUnit) + + newUnit.currentMovement = 0f + }.takeIf { + isFree || + ( unit.civInfo.gold >= goldCostOfUpgrade && !unit.isEmbarked() - && unit.currentMovement == unit.getMaxMovement().toFloat() - }) + && unit.currentMovement == unit.getMaxMovement().toFloat() + ) + } + ) + } + + 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 + } + ) } private fun addBuildingImprovementsAction(unit: MapUnit, actionList: ArrayList, tile: TileInfo, worldScreen: WorldScreen, unitTable: UnitTable) {