diff --git a/core/src/com/unciv/logic/civilization/PopupAlert.kt b/core/src/com/unciv/logic/civilization/PopupAlert.kt index afcbd3ab40..b74e2d1265 100644 --- a/core/src/com/unciv/logic/civilization/PopupAlert.kt +++ b/core/src/com/unciv/logic/civilization/PopupAlert.kt @@ -20,7 +20,8 @@ enum class AlertType : IsPartOfGameInfoSerialization { BulliedProtectedMinor, AttackedProtectedMinor, RecapturedCivilian, - GameHasBeenWon + GameHasBeenWon, + Event } class PopupAlert : IsPartOfGameInfoSerialization { diff --git a/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt b/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt index b6b72620d1..6bdfdc65e1 100644 --- a/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt +++ b/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt @@ -12,6 +12,10 @@ class UnitTurnManager(val unit: MapUnit) { fun endTurn() { unit.movement.clearPathfindingCache() + + for (unique in unit.getTriggeredUniques(UniqueType.TriggerUponTurnEnd)) + UniqueTriggerActivation.triggerUnique(unique, unit) + if (unit.currentMovement > 0 && unit.getTile().improvementInProgress != null && unit.canBuildImprovement(unit.getTile().getTileImprovementInProgress()!!) diff --git a/core/src/com/unciv/models/ruleset/Event.kt b/core/src/com/unciv/models/ruleset/Event.kt new file mode 100644 index 0000000000..14f3bd0195 --- /dev/null +++ b/core/src/com/unciv/models/ruleset/Event.kt @@ -0,0 +1,35 @@ +package com.unciv.models.ruleset + +import com.unciv.logic.civilization.Civilization +import com.unciv.models.ruleset.unique.Conditionals +import com.unciv.models.ruleset.unique.StateForConditionals +import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueTriggerActivation +import com.unciv.models.stats.INamed + + + +class Event : INamed { + + override var name = "" + var text = "" + // todo: add unrepeatable events + + var choices = ArrayList() +} + +class EventChoice { + var text = "" + var triggeredUniques = ArrayList() + val triggerredUniqueObjects by lazy { triggeredUniques.map { Unique(it) } } + + var conditions = ArrayList() + val conditionObjects by lazy { conditions.map { Unique(it) } } + fun matchesConditions(stateForConditionals: StateForConditionals) = + conditionObjects.all { Conditionals.conditionalApplies(null, it, stateForConditionals) } + + fun triggerChoice(civ: Civilization) { + for (unique in triggerredUniqueObjects) + UniqueTriggerActivation.triggerUnique(unique, civ) + } +} diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index 4b7529c65f..4af271c910 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -71,6 +71,7 @@ class Ruleset { var victories = LinkedHashMap() var cityStateTypes = LinkedHashMap() val personalities = LinkedHashMap() + val events = LinkedHashMap() val greatGeneralUnits by lazy { units.values.filter { it.hasUnique(UniqueType.GreatPersonFromCombat, StateForConditionals.IgnoreConditionals) } @@ -162,6 +163,7 @@ class Ruleset { } units.putAll(ruleset.units) personalities.putAll(ruleset.personalities) + events.putAll(ruleset.events) modOptions.uniques.addAll(ruleset.modOptions.uniques) modOptions.constants.merge(ruleset.modOptions.constants) @@ -196,6 +198,7 @@ class Ruleset { victories.clear() cityStateTypes.clear() personalities.clear() + events.clear() } fun allRulesetObjects(): Sequence = @@ -384,6 +387,11 @@ class Ruleset { personalities += createHashmap(json().fromJsonFile(Array::class.java, personalitiesFile)) } + val eventsFile = folderHandle.child("Events.json") + if (eventsFile.exists()) { + events += createHashmap(json().fromJsonFile(Array::class.java, eventsFile)) + } + // Add objects that might not be present in base ruleset mods, but are required diff --git a/core/src/com/unciv/models/ruleset/unique/Conditionals.kt b/core/src/com/unciv/models/ruleset/unique/Conditionals.kt index 2005b7d307..b0a52c94b2 100644 --- a/core/src/com/unciv/models/ruleset/unique/Conditionals.kt +++ b/core/src/com/unciv/models/ruleset/unique/Conditionals.kt @@ -12,7 +12,7 @@ import kotlin.random.Random object Conditionals { fun conditionalApplies( - unique: Unique, + unique: Unique?, condition: Unique, state: StateForConditionals ): Boolean { @@ -80,9 +80,9 @@ object Conditionals { /** Helper for ConditionalWhenAboveAmountStatResource and its below counterpart */ fun checkResourceOrStatAmount( - resourceOrStatName: String, - lowerLimit: Float, - upperLimit: Float, + resourceOrStatName: String, + lowerLimit: Float, + upperLimit: Float, modifyByGameSpeed: Boolean = false, compare: (current: Int, lowerLimit: Float, upperLimit: Float) -> Boolean ): Boolean { @@ -115,22 +115,22 @@ object Conditionals { UniqueType.ConditionalWithoutResource -> getResourceAmount(condition.params[0]) <= 0 UniqueType.ConditionalWhenAboveAmountStatResource -> - checkResourceOrStatAmount(condition.params[1], condition.params[0].toFloat(), Float.MAX_VALUE) + checkResourceOrStatAmount(condition.params[1], condition.params[0].toFloat(), Float.MAX_VALUE) { current, lowerLimit, _ -> current > lowerLimit } UniqueType.ConditionalWhenBelowAmountStatResource -> - checkResourceOrStatAmount(condition.params[1], Float.MIN_VALUE, condition.params[0].toFloat()) + checkResourceOrStatAmount(condition.params[1], Float.MIN_VALUE, condition.params[0].toFloat()) { current, _, upperLimit -> current < upperLimit } UniqueType.ConditionalWhenBetweenStatResource -> - checkResourceOrStatAmount(condition.params[2], condition.params[0].toFloat(), condition.params[1].toFloat()) + checkResourceOrStatAmount(condition.params[2], condition.params[0].toFloat(), condition.params[1].toFloat()) { current, lowerLimit, upperLimit -> current >= lowerLimit && current <= upperLimit } UniqueType.ConditionalWhenAboveAmountStatResourceSpeed -> - checkResourceOrStatAmount(condition.params[1], condition.params[0].toFloat(), Float.MAX_VALUE, true) + checkResourceOrStatAmount(condition.params[1], condition.params[0].toFloat(), Float.MAX_VALUE, true) { current, lowerLimit, _ -> current > lowerLimit } UniqueType.ConditionalWhenBelowAmountStatResourceSpeed -> - checkResourceOrStatAmount(condition.params[1], Float.MIN_VALUE, condition.params[0].toFloat(), true) + checkResourceOrStatAmount(condition.params[1], Float.MIN_VALUE, condition.params[0].toFloat(), true) { current, _, upperLimit -> current < upperLimit } - UniqueType.ConditionalWhenBetweenStatResourceSpeed -> - checkResourceOrStatAmount(condition.params[2], condition.params[0].toFloat(), condition.params[1].toFloat(), true) + UniqueType.ConditionalWhenBetweenStatResourceSpeed -> + checkResourceOrStatAmount(condition.params[2], condition.params[0].toFloat(), condition.params[1].toFloat(), true) { current, lowerLimit, upperLimit -> current >= lowerLimit && current <= upperLimit } UniqueType.ConditionalHappy -> checkOnCiv { stats.happiness >= 0 } @@ -270,14 +270,16 @@ object Conditionals { UniqueType.ConditionalInRegionExceptOfType -> state.region?.type != condition.params[0] UniqueType.ConditionalFirstCivToResearch -> - unique.sourceObjectType == UniqueTarget.Tech + unique != null + && unique.sourceObjectType == UniqueTarget.Tech && checkOnGameInfo { civilizations.none { it != relevantCiv && it.isMajorCiv() && it.tech.isResearched(unique.sourceObjectName!!) // guarded by the sourceObjectType check } } UniqueType.ConditionalFirstCivToAdopt -> - unique.sourceObjectType == UniqueTarget.Policy + unique != null + && unique.sourceObjectType == UniqueTarget.Policy && checkOnGameInfo { civilizations.none { it != relevantCiv && it.isMajorCiv() && it.policies.isAdopted(unique.sourceObjectName!!) // guarded by the sourceObjectType check diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index 1fbf5c3a4e..9883daa973 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -509,13 +509,23 @@ enum class UniqueParameterType( // Used in FreeExtraBeliefs, FreeExtraAnyBeliefs private val knownValues = setOf("founding", "enhancing") override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): - UniqueType.UniqueParameterErrorSeverity? = when (parameterText) { + UniqueType.UniqueParameterErrorSeverity? = when (parameterText) { in knownValues -> null else -> UniqueType.UniqueParameterErrorSeverity.RulesetInvariant } override fun getTranslationWriterStringsForOutput() = knownValues }, + /** [UniqueType.ConditionalTech] and others, no central implementation */ + Event("event", "Inspiration", "The name of any event") { + override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): + UniqueType.UniqueParameterErrorSeverity? = when (parameterText) { + in ruleset.events -> null + else -> UniqueType.UniqueParameterErrorSeverity.RulesetSpecific + } + }, + + /** [UniqueType.ConditionalTech] and others, no central implementation */ Technology("tech", "Agriculture", "The name of any tech") { override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index 67f17003a8..6268ad45e7 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -5,6 +5,7 @@ import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.automation.civilization.NextTurnAutomation import com.unciv.logic.city.City +import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.CivFlags import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.LocationAction @@ -13,6 +14,7 @@ import com.unciv.logic.civilization.NotificationAction import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.PolicyAction +import com.unciv.logic.civilization.PopupAlert import com.unciv.logic.civilization.TechAction import com.unciv.logic.civilization.managers.ReligionState import com.unciv.logic.map.mapgenerator.NaturalWonderGenerator @@ -87,7 +89,9 @@ object UniqueTriggerActivation { return { civInfo.temporaryUniques.add(TemporaryUnique(unique, timingConditional.params[0].toInt())) } } - if (!unique.conditionalsApply(StateForConditionals(civInfo, city, unit, tile))) return null + val stateForConditionals = StateForConditionals(civInfo, city, unit, tile) + + if (!unique.conditionalsApply(stateForConditionals)) return null val chosenCity = relevantCity ?: civInfo.cities.firstOrNull { it.isCapital() } @@ -98,6 +102,17 @@ object UniqueTriggerActivation { val ruleSet = civInfo.gameInfo.ruleset when (unique.type) { + UniqueType.TriggerEvent -> { + val event = ruleSet.events[unique.params[0]] ?: return null + val choices = event.choices.filter { it.matchesConditions(stateForConditionals) } + if (choices.isEmpty()) return null + return { + if (civInfo.isAI()) choices.random().triggerChoice(civInfo) + else civInfo.popupAlerts.add(PopupAlert(AlertType.Event, event.name)) + true + } + } + UniqueType.OneTimeFreeUnit -> { val unitName = unique.params[0] val baseUnit = ruleSet.units[unitName] ?: return null @@ -950,6 +965,7 @@ object UniqueTriggerActivation { true } } + else -> return null } } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 39bbfbc353..cc79c9d2ab 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -772,6 +772,7 @@ enum class UniqueType( UnitsGainPromotion("[mapUnitFilter] units gain the [promotion] promotion", UniqueTarget.Triggerable), // Not used in Vanilla FreeStatBuildings("Provides the cheapest [stat] building in your first [positiveAmount] cities for free", UniqueTarget.Triggerable), // used in Policy FreeSpecificBuildings("Provides a [buildingName] in your first [positiveAmount] cities for free", UniqueTarget.Triggerable), // used in Policy + TriggerEvent("Triggers a [event] event", UniqueTarget.Triggerable), //endregion diff --git a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt index aec70a1c23..8aeaa7b08a 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt @@ -18,6 +18,7 @@ import com.unciv.logic.civilization.PopupAlert import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.diplomacy.RelationshipLevel +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.tr @@ -103,6 +104,7 @@ class AlertPopup( AlertType.BulliedProtectedMinor, AlertType.AttackedProtectedMinor -> addBulliedOrAttackedProtectedMinor() AlertType.RecapturedCivilian -> skipThisAlert = addRecapturedCivilian() AlertType.GameHasBeenWon -> addGameHasBeenWon() + AlertType.Event -> skipThisAlert = !addEvent() } if (!skipThisAlert) open() } @@ -497,6 +499,24 @@ class AlertPopup( } } + /** Returns if event was triggered correctly */ + private fun addEvent(): Boolean { + val event = gameInfo.ruleset.events[popupAlert.value] ?: return false + + val civ = gameInfo.currentPlayerCiv + val choices = event.choices.filter { it.matchesConditions(StateForConditionals(civ)) } + if (choices.isEmpty()) return false + + addGoodSizedLabel(event.text) + for (choice in choices){ + addSeparator() + add(choice.text.toTextButton().onActivation { close(); choice.triggerChoice(civ) }).row() + for (triggeredUnique in choice.triggeredUniques) + addGoodSizedLabel(triggeredUnique) + } + return true + } + //endregion override fun close() { diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index f3298c3a86..c7f01dc568 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -159,6 +159,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl Applicable to: Triggerable +??? example "Triggers a [event] event" + Example: "Triggers a [Inspiration] event" + + Applicable to: Triggerable + ??? example "Suppress warning [validationWarning]" Allows suppressing specific validation warnings. Errors, deprecation warnings, or warnings about untyped and non-filtering uniques should be heeded, not suppressed, and are therefore not accepted. Note that this can be used in ModOptions, in the uniques a warning is about, or as modifier on the unique triggering a warning - but you still need to be specific. Even in the modifier case you will need to specify a sufficiently selective portion of the warning text as parameter. Example: "Suppress warning [Tinman is supposed to automatically upgrade at tech Clockwork, and therefore Servos for its upgrade Mecha may not yet be researched! -or- *is supposed to automatically upgrade*]" @@ -2374,6 +2379,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl *[combatantFilter]: This indicates a combatant, which can either be a unit or a city (when bombarding). Must either be `City` or a `mapUnitFilter`. *[costOrStrength]: `Cost` or `Strength`. *[era]: The name of any era. +*[event]: The name of any event. *[foundingOrEnhancing]: `founding` or `enhancing`. *[fraction]: Indicates a fractional number, which can be negative. *[improvementName]: The name of any improvement.