From b992144ecd49a8793565a9f543207f87c64dae35 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 30 Jul 2023 16:38:26 +0200 Subject: [PATCH] Fix a few now flagged but working Unique targets (#9845) * Allow a few working UniqueType-UniqueTarget combos in RulesetValidator * RulesetValidator checks GlobalUniques * Alternate Settlers and Workers recognized via Unique - same rules in RulesetValidator and GameStarter, wiki clarification --- core/src/com/unciv/logic/GameStarter.kt | 2 +- .../unciv/models/ruleset/RulesetValidator.kt | 12 +++-- core/src/com/unciv/models/ruleset/tech/Era.kt | 19 +++++-- .../models/ruleset/unique/UniqueTarget.kt | 2 +- .../unciv/models/ruleset/unique/UniqueType.kt | 4 +- .../5-Miscellaneous-JSON-files.md | 50 ++++++++++--------- docs/Modders/uniques.md | 4 +- 7 files changed, 56 insertions(+), 37 deletions(-) diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 21487d53c5..8519de358b 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -435,7 +435,7 @@ object GameStarter { } private fun getStartingUnitsForEraAndDifficulty(civ: Civilization, gameInfo: GameInfo, ruleset: Ruleset, startingEra: String): MutableList { - val startingUnits = ruleset.eras[startingEra]!!.getStartingUnits().toMutableList() + val startingUnits = ruleset.eras[startingEra]!!.getStartingUnits(ruleset) // Add extra units granted by difficulty startingUnits.addAll(when { diff --git a/core/src/com/unciv/models/ruleset/RulesetValidator.kt b/core/src/com/unciv/models/ruleset/RulesetValidator.kt index b8d83b4891..25423bd615 100644 --- a/core/src/com/unciv/models/ruleset/RulesetValidator.kt +++ b/core/src/com/unciv/models/ruleset/RulesetValidator.kt @@ -40,6 +40,8 @@ class RulesetValidator(val ruleset: Ruleset) { val rulesetInvariant = UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant val rulesetSpecific = UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific + checkUniques(ruleset.globalUniques, lines, rulesetInvariant, tryFixUnknownUniques) + for (unit in ruleset.units.values) { if (unit.upgradesTo == unit.name || (unit.upgradesTo != null && unit.upgradesTo == unit.replaces)) lines += "${unit.name} upgrades to itself!" @@ -145,6 +147,7 @@ class RulesetValidator(val ruleset: Ruleset) { val vanillaRuleset = RulesetCache.getVanillaRuleset() // for UnitTypes fallback + checkUniques(ruleset.globalUniques, lines, rulesetSpecific, tryFixUnknownUniques) if (ruleset.units.values.none { it.hasUnique(UniqueType.FoundCity, StateForConditionals.IgnoreConditionals) }) lines += "No city-founding units in ruleset!" @@ -313,13 +316,14 @@ class RulesetValidator(val ruleset: Ruleset) { if (building !in ruleset.buildings) lines += "Nonexistent building $building built by settlers when starting in ${era.name}" // todo the whole 'starting unit' thing needs to be redone, there's no reason we can't have a single list containing all the starting units. - if (era.startingSettlerUnit !in ruleset.units && (era.startingSettlerUnit!= Constants.settler || ruleset.units.values.none { it.hasUnique( - UniqueType.FoundCity) })) + if (era.startingSettlerUnit !in ruleset.units + && ruleset.units.values.none { it.hasUnique(UniqueType.FoundCity) }) lines += "Nonexistent unit ${era.startingSettlerUnit} marked as starting unit when starting in ${era.name}" - if (era.startingWorkerCount!=0 && era.startingWorkerUnit !in ruleset.units) + if (era.startingWorkerCount != 0 && era.startingWorkerUnit !in ruleset.units + && ruleset.units.values.none { it.hasUnique(UniqueType.BuildImprovements) }) lines += "Nonexistent unit ${era.startingWorkerUnit} marked as starting unit when starting in ${era.name}" - if ((era.startingMilitaryUnitCount !=0 || allDifficultiesStartingUnits.contains( + if ((era.startingMilitaryUnitCount != 0 || allDifficultiesStartingUnits.contains( Constants.eraSpecificUnit)) && era.startingMilitaryUnit !in ruleset.units) lines += "Nonexistent unit ${era.startingMilitaryUnit} marked as starting unit when starting in ${era.name}" if (era.researchAgreementCost < 0 || era.startingSettlerCount < 0 || era.startingWorkerCount < 0 || era.startingMilitaryUnitCount < 0 || era.startingGold < 0 || era.startingCulture < 0) diff --git a/core/src/com/unciv/models/ruleset/tech/Era.kt b/core/src/com/unciv/models/ruleset/tech/Era.kt index 34b0a15f43..3b21ba895b 100644 --- a/core/src/com/unciv/models/ruleset/tech/Era.kt +++ b/core/src/com/unciv/models/ruleset/tech/Era.kt @@ -1,6 +1,7 @@ package com.unciv.models.ruleset.tech import com.badlogic.gdx.graphics.Color +import com.unciv.logic.UncivShowableException import com.unciv.models.ruleset.IRulesetObject import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetObject @@ -82,10 +83,22 @@ class Era : RulesetObject() { }.map { it.first }.distinct() } - fun getStartingUnits(): List { + fun getStartingUnits(ruleset: Ruleset): MutableList { val startingUnits = mutableListOf() - repeat(startingSettlerCount) { startingUnits.add(startingSettlerUnit) } - repeat(startingWorkerCount) { startingUnits.add(startingWorkerUnit) } + val startingSettlerName: String = + if (startingSettlerUnit in ruleset.units) startingSettlerUnit + else ruleset.units.values + .firstOrNull { it.hasUnique(UniqueType.FoundCity) } + ?.name + ?: throw UncivShowableException("No Settler unit found for era $name") + val startingWorkerName: String = + if (startingWorkerCount == 0 || startingWorkerUnit in ruleset.units) startingWorkerUnit + else ruleset.units.values + .firstOrNull { it.hasUnique(UniqueType.BuildImprovements) } + ?.name + ?: throw UncivShowableException("No Worker unit found for era $name") + repeat(startingSettlerCount) { startingUnits.add(startingSettlerName) } + repeat(startingWorkerCount) { startingUnits.add(startingWorkerName) } repeat(startingMilitaryUnitCount) { startingUnits.add(startingMilitaryUnit) } return startingUnits } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt index ff6afe36d5..befe26fb29 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt @@ -34,7 +34,7 @@ enum class UniqueTarget( Tech(inheritsFrom = Global), Policy(inheritsFrom = Global), FounderBelief("Uniques for Founder and Enhancer type Beliefs, that will apply to the founder of this religion", inheritsFrom = Global), - FollowerBelief("Uniques for Pantheon and Follower type beliefs, that will apply to each city where the religion is the majority religion"), + FollowerBelief("Uniques for Pantheon and Follower type beliefs, that will apply to each city where the religion is the majority religion", inheritsFrom = Triggerable), // City-specific Building(inheritsFrom = Global), diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 75af3393da..ab9c9ab1e5 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -184,7 +184,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: /// Resource production & consumption ConsumesResources("Consumes [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Building, UniqueTarget.Unit), - ProvidesResources("Provides [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Global), + ProvidesResources("Provides [amount] [resource]", UniqueTarget.Global, UniqueTarget.Improvement, UniqueTarget.FollowerBelief), CostsResources("Costs [amount] [stockpiledResource]", UniqueTarget.Improvement, UniqueTarget.Building, UniqueTarget.Unit), // Todo: Get rid of forced sign (+[relativeAmount]) and unify these two, e.g.: "[relativeAmount]% [resource/resourceType] production" // Note that the parameter type 'resourceType' (strategic, luxury, bonus) currently doesn't exist and should then be added as well @@ -497,7 +497,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: TerrainGrantsPromotion("Grants [promotion] ([comment]) to adjacent [mapUnitFilter] units for the rest of the game", UniqueTarget.Terrain), GrantsCityStrength("[amount] Strength for cities built on this terrain", UniqueTarget.Terrain), ProductionBonusWhenRemoved("Provides a one-time Production bonus to the closest city when cut down", UniqueTarget.Terrain), - Vegetation("Vegetation", UniqueTarget.Terrain, flags = UniqueFlag.setOfHiddenToUsers), + Vegetation("Vegetation", UniqueTarget.Terrain, UniqueTarget.Improvement, flags = UniqueFlag.setOfHiddenToUsers), // Improvement included because use as tileFilter works TileProvidesYieldWithoutPopulation("Tile provides yield without assigned population", UniqueTarget.Terrain, UniqueTarget.Improvement), diff --git a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md index 5609f75e39..197fd671db 100644 --- a/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md +++ b/docs/Modders/Mod-file-structure/5-Miscellaneous-JSON-files.md @@ -8,9 +8,9 @@ This file defines the difficulty levels a player can choose when starting a new Each difficulty level can have the following attributes: -| Attribute | Type | Optional | Notes | -| --------- | ---- | -------- | ----- | -| name | String | Required | Name of the difficulty level | +| Attribute | Type | Optional | Notes | +| --------- | ---- | -------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | String | Required | Name of the difficulty level | | baseHappiness | Integer | Default 0 | | extraHappinessPerLuxury | Float | Default 0 | | researchCostModifier | Float | Default 1 | @@ -19,7 +19,7 @@ Each difficulty level can have the following attributes: | policyCostModifier | Float | Default 1 | | unhappinessModifier | Float | Default 1 | | barbarianBonus | Float | Default 0 | -| playerBonusStartingUnits | List of Units | Default empty | Can also be 'Era Starting Unit', maps to `startingMilitaryUnit` of the Eras file. All other units must be in [Units.json](4-Unit-related-JSON-files.md#Units.json)] | +| playerBonusStartingUnits | List of Units | Default empty | Can also be 'Era Starting Unit', maps to `startingMilitaryUnit` of the Eras file. All other units must be in [Units.json](4-Unit-related-JSON-files.md#Units.json)]. Applies only to human player civs | | aiCityGrowthModifier | Float | Default 1 | | aiUnitCostModifier | Float | Default 1 | | aiBuildingCostModifier | Float | Default 1 | @@ -27,10 +27,10 @@ Each difficulty level can have the following attributes: | aiBuildingMaintenanceModifier | Float | Default 1 | | aiUnitMaintenanceModifier | Float | Default 1 | | aiFreeTechs | List of Techs | Default empty | -| aiMajorCivBonusStartingUnits | List of Units | Default empty | See above | -| aiCityStateBonusStartingUnits | List of Units | Default empty | See above | +| aiMajorCivBonusStartingUnits | List of Units | Default empty | Same rules as playerBonusStartingUnits, See above. Applies only to AI major civs | +| aiCityStateBonusStartingUnits | List of Units | Default empty | Same rules as playerBonusStartingUnits, See above. Applies only to city-state civs | | aiUnhappinessModifier | Float | Default 1 | -| aisExchangeTechs | Boolean | | Unimplemented | +| aisExchangeTechs | Boolean | | Unimplemented | | turnBarbariansCanEnterPlayerTiles | Integer | Default 0 | | clearBarbarianCampReward | Integer | Default 25 | @@ -42,23 +42,25 @@ This file should contain all the era's you want to use in your mod. Each era can have the following attributes: -| Attribute | Type | Optional | Notes | -| --------- | ---- | -------- | ----- | -| name | String | required | Name of the era | -| researchAgreementCost | Integer (≥0) | defaults to 300 | Cost of research agreements were the most technologically advanced civ is in this era | -| iconRGB | List of 3 Integers | defaults to [255, 255, 255] | RGB color that icons for technologies of this era should have in the Tech screen | -| unitBaseBuyCost | Integer (≥0) | defaults to 200 | Base cost of buying units with Faith, Food, Science or Culture when no other cost is provided | -| startingSettlerCount | Integer (≥0) | defaults to 1 | Amount of settler units that should be spawned when starting a game in this era | -| startingSettlerUnit | String | defaults to "Settler" | Name of the unit that should be used for the previous field. Must be in [Units.json](4-Unit-related-JSON-files.md#unitsjson) | -| startingWorkerCount | Integer (≥0) | defaults to 0 | Amount of worker units that should be spawned when starting a game in this era | -| startingWorkerUnit | String | defaults to "Worker" | Name of the unit that should be used for the previous field. Must be in [Units.json](4-Unit-related-JSON-files.md#unitsjson) | -| startingMilitaryUnitCount | Integer (≥0) | defaults to 1 | Amount of military units that should be spawned when starting a game in this era | -| startingMilitaryUnit | String | defaults to "Warrior" | Name of the unit that should be used for the previous field. Must be in [Units.json](4-Unit-related-JSON-files.md#unitsjson)| -| startingGold | Integer (≥0) | defaults to 0 | Amount of gold each civ should receive when starting a game in this era | -| startingCulture | Integer (≥0) | defaults to 0 | Amount of culture each civ should receive when starting a game in this era | -| settlerPopulation | Integer (>0) | defaults to 1 | Default amount of population each city should have when settled when starting a game in this era | -| settlerBuildings | List of Strings | defaults to none | Buildings that should automatically be built whenever a city is settled when starting a game in this era | -| startingObsoleteWonders | List of Strings | defaults to none | Wonders (and technically buildings) that should be impossible to built when starting a game in this era. Used in the base game to remove all wonders older than 2 era's | +| Attribute | Type | Optional | Notes | +| --------- | ---- | -------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | String | required | Name of the era | +| researchAgreementCost | Integer (≥0) | defaults to 300 | Cost of research agreements were the most technologically advanced civ is in this era | +| iconRGB | List of 3 Integers | defaults to [255, 255, 255] | RGB color that icons for technologies of this era should have in the Tech screen | +| unitBaseBuyCost | Integer (≥0) | defaults to 200 | Base cost of buying units with Faith, Food, Science or Culture when no other cost is provided | +| startingSettlerCount | Integer (≥0) | defaults to 1 | Amount of settler units that should be spawned when starting a game in this era (setting this to zero is discouraged [^1]) | +| startingSettlerUnit | String | defaults to "Settler" | Name of the unit that should be used for the previous field. Must be in [Units.json](4-Unit-related-JSON-files.md#unitsjson), or a unit with the "Founds a new city" unique must exist | +| startingWorkerCount | Integer (≥0) | defaults to 0 | Amount of worker units that should be spawned when starting a game in this era | +| startingWorkerUnit | String | defaults to "Worker" | Name of the unit that should be used for the previous field. If startingWorkerCount>0, then it must exist in [Units.json](4-Unit-related-JSON-files.md#unitsjson), or a unit with the "Can build [filter] improvements on tiles" unique must exist | +| startingMilitaryUnitCount | Integer (≥0) | defaults to 1 | Amount of military units that should be spawned when starting a game in this era | +| startingMilitaryUnit | String | defaults to "Warrior" | Name of the unit that should be used for the previous field. Must be in [Units.json](4-Unit-related-JSON-files.md#unitsjson) | +| startingGold | Integer (≥0) | defaults to 0 | Amount of gold each civ should receive when starting a game in this era | +| startingCulture | Integer (≥0) | defaults to 0 | Amount of culture each civ should receive when starting a game in this era | +| settlerPopulation | Integer (>0) | defaults to 1 | Default amount of population each city should have when settled when starting a game in this era | +| settlerBuildings | List of Strings | defaults to none | Buildings that should automatically be built whenever a city is settled when starting a game in this era | +| startingObsoleteWonders | List of Strings | defaults to none | Wonders (and technically buildings) that should be impossible to built when starting a game in this era. Used in the base game to remove all wonders older than 2 era's | + +[^1]: Successfully setting startingSettlerCount to zero in a mod (idea: conquer or die) is not easy. Some player-controlled settings require at least one Settler, through any source (see difficulties for other possible settler sources), or you won't be able to start a game: Once City Challenge requires one for all players, and allowing any city-states requires one for those. Would also affect defeat rules. ## Speeds.json diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index 2aa27ce454..473e70fa5c 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -606,7 +606,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl ??? example "Provides [amount] [resource]" Example: "Provides [3] [Iron]" - Applicable to: Global, Improvement + Applicable to: Global, FollowerBelief, Improvement ??? example "Quantity of strategic resources produced by the empire +[relativeAmount]%" Example: "Quantity of strategic resources produced by the empire +[+20]%" @@ -1450,7 +1450,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl Applicable to: Terrain ??? example "Vegetation" - Applicable to: Terrain + Applicable to: Terrain, Improvement ??? example "Tile provides yield without assigned population" Applicable to: Terrain, Improvement