From 43b0c9bbad71e8da82e8d8cf2fbc5069be61a93f Mon Sep 17 00:00:00 2001 From: yairm210 Date: Wed, 21 Aug 2024 09:32:07 +0300 Subject: [PATCH] Temporary unit statuses! Can be applied with "This Unit gains the [promotion] status for [positiveAmount] turn(s)" trigger unique Example: "This Unit gains the [Morale] status for [1] turn(s) " --- .../assets/jsons/Civ V - Vanilla/Units.json | 1 + .../com/unciv/logic/map/mapunit/MapUnit.kt | 35 ++++++++++++++++++- .../logic/map/mapunit/UnitTurnManager.kt | 6 ++++ .../models/ruleset/unique/Conditionals.kt | 8 +++-- .../ruleset/unique/UniqueTriggerActivation.kt | 8 +++++ .../unciv/models/ruleset/unique/UniqueType.kt | 7 ++-- .../ui/screens/worldscreen/unit/UnitTable.kt | 19 ++++++---- .../unit/actions/UnitActionsTable.kt | 1 + docs/Modders/uniques.md | 8 +++++ 9 files changed, 81 insertions(+), 12 deletions(-) diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index 6ae5f4ca71..f93ef5f159 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -808,6 +808,7 @@ "requiredTech": "Rifling", "obsoleteTech": "Replaceable Parts", "upgradesTo": "Infantry", + "uniques": ["This Unit gains the [Morale] status for [2] turn(s) "], "attackSound": "shot" }, { diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index d0430b9f69..ccbda57877 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -95,6 +95,21 @@ class MapUnit : IsPartOfGameInfoSerialization { /** Array list of all the tiles that this unit has attacked since the start of its most recent turn. Used in movement arrow overlay. */ var attacksSinceTurnStart = ArrayList() + + class UnitStatus { + var name:String = "" + /** Decreses at *start on next turn* so defensive statuses persist on enemy turns */ + var turnsLeft = 1 + + @Transient + lateinit var uniques: List + + fun setTransients(unit: MapUnit) { + uniques = unit.civ.gameInfo.ruleset.unitPromotions[name]?.uniqueObjects ?: emptyList() + } + } + + var statuses = ArrayList() //endregion //region Transient fields @@ -198,6 +213,7 @@ class MapUnit : IsPartOfGameInfoSerialization { toReturn.religion = religion toReturn.religiousStrengthLost = religiousStrengthLost toReturn.movementMemories = movementMemories.copy() + toReturn.statuses = ArrayList(statuses) toReturn.mostRecentMoveType = mostRecentMoveType toReturn.attacksSinceTurnStart = ArrayList(attacksSinceTurnStart.map { Vector2(it) }) return toReturn @@ -628,6 +644,7 @@ class MapUnit : IsPartOfGameInfoSerialization { promotions.setTransients(this) baseUnit = ruleset.units[name] ?: throw java.lang.Exception("Unit $name is not found!") + for (status in statuses) status.setTransients(this) updateUniques() if (action == UnitActionType.Automate.value){ @@ -640,7 +657,9 @@ class MapUnit : IsPartOfGameInfoSerialization { val uniqueSources = baseUnit.uniqueObjects.asSequence() + type.uniqueObjects + - promotions.getPromotions().flatMap { it.uniqueObjects } + promotions.getPromotions().flatMap { it.uniqueObjects } + + statuses.flatMap { it.uniques } + tempUniquesMap = UniqueMap(uniqueSources) cache.updateUniques() } @@ -1007,6 +1026,20 @@ class MapUnit : IsPartOfGameInfoSerialization { movementMemories.removeFirst() } } + + fun setStatus(name:String, turns:Int){ + val existingStatus = statuses.firstOrNull { it.name == name } + if (existingStatus != null){ + if (turns > existingStatus.turnsLeft) existingStatus.turnsLeft = turns + return + } + + val status = UnitStatus() + status.name = name + status.turnsLeft = turns + status.setTransients(this) + statuses.add(status) + } fun isNuclearWeapon() = hasUnique(UniqueType.NuclearWeapon) diff --git a/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt b/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt index baffafb6ad..5f160373c9 100644 --- a/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt +++ b/core/src/com/unciv/logic/map/mapunit/UnitTurnManager.kt @@ -166,5 +166,11 @@ class UnitTurnManager(val unit: MapUnit) { unit.addMovementMemory() unit.attacksSinceTurnStart.clear() + + for (status in unit.statuses.toList()){ + status.turnsLeft-- + if (status.turnsLeft <= 0) unit.statuses.remove(status) + } + unit.updateUniques() } } diff --git a/core/src/com/unciv/models/ruleset/unique/Conditionals.kt b/core/src/com/unciv/models/ruleset/unique/Conditionals.kt index 21228c2682..12069376f7 100644 --- a/core/src/com/unciv/models/ruleset/unique/Conditionals.kt +++ b/core/src/com/unciv/models/ruleset/unique/Conditionals.kt @@ -202,8 +202,12 @@ object Conditionals { UniqueType.ConditionalVsUnits, UniqueType.ConditionalVsCombatant -> state.theirCombatant?.matchesFilter(conditional.params[0]) == true UniqueType.ConditionalOurUnit, UniqueType.ConditionalOurUnitOnUnit -> state.relevantUnit?.matchesFilter(conditional.params[0]) == true - UniqueType.ConditionalUnitWithPromotion -> state.relevantUnit?.promotions?.promotions?.contains(conditional.params[0]) == true - UniqueType.ConditionalUnitWithoutPromotion -> state.relevantUnit?.promotions?.promotions?.contains(conditional.params[0]) == false + UniqueType.ConditionalUnitWithPromotion -> state.relevantUnit != null && + (state.relevantUnit!!.promotions.promotions.contains(conditional.params[0]) + || state.relevantUnit!!.statuses.any { it.name == conditional.params[0] } ) + UniqueType.ConditionalUnitWithoutPromotion -> state.relevantUnit != null && + !(state.relevantUnit!!.promotions.promotions.contains(conditional.params[0]) + || state.relevantUnit!!.statuses.any { it.name == conditional.params[0] } ) UniqueType.ConditionalAttacking -> state.combatAction == CombatAction.Attack UniqueType.ConditionalDefending -> state.combatAction == CombatAction.Defend UniqueType.ConditionalAboveHP -> state.relevantUnit != null && state.relevantUnit!!.health > conditional.params[0].toInt() diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index b6fe6c9d8d..c88162b907 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -962,6 +962,14 @@ object UniqueTriggerActivation { true } } + UniqueType.OneTimeUnitGainStatus -> { + if (unit == null) return null + if (unique.params[0] !in unit.civ.gameInfo.ruleset.unitPromotions) return null + return { + unit.setStatus(unique.params[0], unique.params[1].toInt()) + true + } + } UniqueType.OneTimeUnitUpgrade, UniqueType.OneTimeUnitSpecialUpgrade -> { if (unit == null) return null val upgradeAction = diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 999316b9e5..aab9a7d60a 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -737,8 +737,8 @@ enum class UniqueType( /////// unit conditionals ConditionalOurUnit("for [mapUnitFilter] units", UniqueTarget.Conditional), ConditionalOurUnitOnUnit("when [mapUnitFilter]", UniqueTarget.Conditional), // Same but for the unit itself - ConditionalUnitWithPromotion("for units with [promotion]", UniqueTarget.Conditional), - ConditionalUnitWithoutPromotion("for units without [promotion]", UniqueTarget.Conditional), + ConditionalUnitWithPromotion("for units with [promotion]", UniqueTarget.Conditional, docDescription = "Also applies to units with temporary status"), + ConditionalUnitWithoutPromotion("for units without [promotion]", UniqueTarget.Conditional, docDescription = "Also applies to units with temporary status"), ConditionalVsCity("vs cities", UniqueTarget.Conditional), ConditionalVsUnits("vs [mapUnitFilter] units", UniqueTarget.Conditional), ConditionalVsCombatant("vs [combatantFilter]", UniqueTarget.Conditional), @@ -837,6 +837,9 @@ enum class UniqueType( OneTimeUnitRemovePromotion("This Unit loses the [promotion] promotion", UniqueTarget.UnitTriggerable), OneTimeUnitGainMovement("This Unit gains [amount] movement", UniqueTarget.UnitTriggerable), OneTimeUnitLoseMovement("This Unit loses [amount] movement", UniqueTarget.UnitTriggerable), + OneTimeUnitGainStatus("This Unit gains the [promotion] status for [positiveAmount] turn(s)", UniqueTarget.UnitTriggerable, + docDescription = "Statuses are temporary promotions. They do not stack, and reapplying a specific status take the highest number - so reapplying a 3-turn on a 1-turn makes it 3, but doing the opposite will have no effect. " + + "Turns left on the status decrease at the *start of turn*, so bonuses applied for 1 turn are stll applied during other civ's turns."), SkipPromotion("Doing so will consume this opportunity to choose a Promotion", UniqueTarget.Promotion), FreePromotion("This Promotion is free", UniqueTarget.Promotion), diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/UnitTable.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/UnitTable.kt index 75e75cf97c..d404bce090 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/UnitTable.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/UnitTable.kt @@ -6,18 +6,15 @@ import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.ui.Image import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.utils.Align import com.unciv.logic.battle.CityCombatant import com.unciv.logic.city.City import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.tile.Tile import com.unciv.models.Spy import com.unciv.models.translations.tr -import com.unciv.ui.components.extensions.addSeparator -import com.unciv.ui.components.extensions.center -import com.unciv.ui.components.extensions.darken -import com.unciv.ui.components.extensions.getCloseButton -import com.unciv.ui.components.extensions.isShiftKeyPressed -import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.components.extensions.* +import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.components.input.onClick import com.unciv.ui.components.widgets.UnitIconGroup @@ -65,7 +62,7 @@ class UnitTable(val worldScreen: WorldScreen) : Table() { // This is so that not on every update(), we will update the unit table. // Most of the time it's the same unit with the same stats so why waste precious time? - private var selectedUnitHasChanged = false + var selectedUnitHasChanged = false val separator: Actor var selectedSpy: Spy? = null @@ -287,6 +284,14 @@ class UnitTable(val worldScreen: WorldScreen) : Table() { for (promotion in selectedUnit!!.promotions.getPromotions(true)) promotionsTable.add(ImageGetter.getPromotionPortrait(promotion.name)).padBottom(2f) + + for (status in selectedUnit!!.statuses) { + val group = ImageGetter.getPromotionPortrait(status.name) + val turnsLeft = "${status.turnsLeft}${Fonts.turn}".toLabel(fontSize = 8).surroundWithCircle(15f, color = Color.BLACK) + group.addActor(turnsLeft) + turnsLeft.setPosition(group.width, 0f, Align.bottomRight) + promotionsTable.add(group).padBottom(2f) + } // Since Clear also clears the listeners, we need to re-add them every time promotionsTable.onClick { diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt index e270ee7743..81cab20c08 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt @@ -181,5 +181,6 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { if (!UncivGame.Current.settings.autoUnitCycle) return if (unit.isDestroyed || unitAction.type.isSkippingToNextUnit && (unit.isMoving() && !unit.hasMovement() || !unit.isMoving())) worldScreen.switchToNextUnit() + else worldScreen.bottomUnitTable.selectedUnitHasChanged = true } } diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index 6cdffb8284..3b95ade190 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -237,6 +237,12 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl Applicable to: UnitTriggerable +??? example "This Unit gains the [promotion] status for [positiveAmount] turn(s)" + Statuses are temporary promotions. They do not stack, and reapplying a specific status take the highest number - so reapplying a 3-turn on a 1-turn makes it 3, but doing the opposite will have no effect. Turns left on the status decrease at the *start of turn*, so bonuses applied for 1 turn are stll applied during other civ's turns. + Example: "This Unit gains the [Shock I] status for [3] turn(s)" + + Applicable to: UnitTriggerable + ## Global uniques !!! note "" @@ -2966,11 +2972,13 @@ If your mod renames Coast or Lakes, do not use this with one of these as paramet Applicable to: Conditional ??? example "<for units with [promotion]>" + Also applies to units with temporary status Example: "<for units with [Shock I]>" Applicable to: Conditional ??? example "<for units without [promotion]>" + Also applies to units with temporary status Example: "<for units without [Shock I]>" Applicable to: Conditional