diff --git a/android/Images.Icons/OtherIcons/HiddenTutorialTask.png b/android/Images.Icons/OtherIcons/HiddenTutorialTask.png new file mode 100644 index 0000000000..b25fc10e75 Binary files /dev/null and b/android/Images.Icons/OtherIcons/HiddenTutorialTask.png differ diff --git a/android/Images.Icons/OtherIcons/Score.png b/android/Images.Icons/OtherIcons/Score.png new file mode 100644 index 0000000000..b25fc10e75 Binary files /dev/null and b/android/Images.Icons/OtherIcons/Score.png differ diff --git a/android/assets/ExtraImages/Tutorials/Conquer a city.png b/android/assets/ExtraImages/Tutorials/Conquer a city.png new file mode 100644 index 0000000000..f7a6f7ed0c Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Conquer a city.png differ diff --git a/android/assets/ExtraImages/Tutorials/Construct an improvement.png b/android/assets/ExtraImages/Tutorials/Construct an improvement.png new file mode 100644 index 0000000000..24aa85aa4b Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Construct an improvement.png differ diff --git a/android/assets/ExtraImages/Tutorials/Create a trade route.png b/android/assets/ExtraImages/Tutorials/Create a trade route.png new file mode 100644 index 0000000000..825c1e7dbd Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Create a trade route.png differ diff --git a/android/assets/ExtraImages/Tutorials/Enter city screen.png b/android/assets/ExtraImages/Tutorials/Enter city screen.png new file mode 100644 index 0000000000..1259888608 Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Enter city screen.png differ diff --git a/android/assets/ExtraImages/Tutorials/Found city.png b/android/assets/ExtraImages/Tutorials/Found city.png new file mode 100644 index 0000000000..9b0aecd350 Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Found city.png differ diff --git a/android/assets/ExtraImages/Tutorials/Meet another civilization.jpg b/android/assets/ExtraImages/Tutorials/Meet another civilization.jpg new file mode 100644 index 0000000000..d01fc9c2d5 Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Meet another civilization.jpg differ diff --git a/android/assets/ExtraImages/Tutorials/Move an air unit.png b/android/assets/ExtraImages/Tutorials/Move an air unit.png new file mode 100644 index 0000000000..274f4de22d Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Move an air unit.png differ diff --git a/android/assets/ExtraImages/Tutorials/Move unit.png b/android/assets/ExtraImages/Tutorials/Move unit.png new file mode 100644 index 0000000000..652e4ed2d7 Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Move unit.png differ diff --git a/android/assets/ExtraImages/Tutorials/Open the options table.png b/android/assets/ExtraImages/Tutorials/Open the options table.png new file mode 100644 index 0000000000..4a1826e20a Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Open the options table.png differ diff --git a/android/assets/ExtraImages/Tutorials/Pass a turn.png b/android/assets/ExtraImages/Tutorials/Pass a turn.png new file mode 100644 index 0000000000..fd9256d855 Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Pass a turn.png differ diff --git a/android/assets/ExtraImages/Tutorials/Pick construction.png b/android/assets/ExtraImages/Tutorials/Pick construction.png new file mode 100644 index 0000000000..449dd3b871 Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Pick construction.png differ diff --git a/android/assets/ExtraImages/Tutorials/Pick technology.png b/android/assets/ExtraImages/Tutorials/Pick technology.png new file mode 100644 index 0000000000..86aaee259d Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Pick technology.png differ diff --git a/android/assets/ExtraImages/Tutorials/Reassign worked tiles.png b/android/assets/ExtraImages/Tutorials/Reassign worked tiles.png new file mode 100644 index 0000000000..83148b4393 Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/Reassign worked tiles.png differ diff --git a/android/assets/ExtraImages/Tutorials/See your stats breakdown.png b/android/assets/ExtraImages/Tutorials/See your stats breakdown.png new file mode 100644 index 0000000000..7f455165f0 Binary files /dev/null and b/android/assets/ExtraImages/Tutorials/See your stats breakdown.png differ diff --git a/android/assets/Icons.atlas b/android/assets/Icons.atlas index 92a8fb4d78..90e972aad0 100644 --- a/android/assets/Icons.atlas +++ b/android/assets/Icons.atlas @@ -11,6 +11,20 @@ CityStateIcons/Cultured orig: 100, 100 offset: 0, 0 index: -1 +OtherIcons/HiddenTutorialTask + rotate: false + xy: 127, 1001 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 +OtherIcons/Score + rotate: false + xy: 127, 1001 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 CityStateIcons/Maritime rotate: false xy: 1819, 1821 @@ -431,27 +445,6 @@ StatIcons/Faith orig: 100, 100 offset: 0, 0 index: -1 -NotificationIcons/Loading - rotate: false - xy: 1063, 1820 - size: 100, 100 - orig: 100, 100 - offset: 0, 0 - index: -1 -NotificationIcons/Working - rotate: false - xy: 1063, 1820 - size: 100, 100 - orig: 100, 100 - offset: 0, 0 - index: -1 -OtherIcons/Loading - rotate: false - xy: 1063, 1820 - size: 100, 100 - orig: 100, 100 - offset: 0, 0 - index: -1 NotificationIcons/PickConstruction rotate: false xy: 1555, 1497 @@ -739,6 +732,27 @@ OtherIcons/Load orig: 100, 100 offset: 0, 0 index: -1 +OtherIcons/Loading + rotate: false + xy: 1063, 1820 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 +NotificationIcons/Loading + rotate: false + xy: 1063, 1820 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 +NotificationIcons/Working + rotate: false + xy: 1063, 1820 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 OtherIcons/LockSmall rotate: false xy: 725, 869 diff --git a/android/assets/jsons/Civ V - Gods & Kings/Events.json b/android/assets/jsons/Civ V - Gods & Kings/Events.json new file mode 100644 index 0000000000..df219e8b2a --- /dev/null +++ b/android/assets/jsons/Civ V - Gods & Kings/Events.json @@ -0,0 +1,190 @@ +[ + { + "name":"Tutorial Task: [Move unit]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Move a unit!", "centered":true}, + {"extraImage":"Tutorials/Move unit", "imageSize":140}, + {"text":"Click on a unit → Click on a destination → Click the arrow popup."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Found city]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Found a city!", "centered":true}, + {"extraImage":"Tutorials/Found city", "imageSize":140}, + {"text":"Select the Settler → Click on 'Found city'."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Enter city screen]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Enter the city screen!", "centered":true}, + {"extraImage":"Tutorials/Enter city screen", "imageSize":100}, + {"text":"Click the city button twice."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Pick technology]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Pick a technology to research!", "centered":true}, + {"extraImage":"Tutorials/Pick technology", "imageSize":180}, + {"text":"Click on the tech button → select technology → click 'Research' (bottom right)."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Pick construction]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Pick a construction!", "centered":true}, + {"extraImage":"Tutorials/Pick construction", "imageSize":120}, + {"text":"Enter city screen → Click on a unit or building → click 'add to queue'."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Pass a turn]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Pass a turn!", "centered":true}, + {"extraImage":"Tutorials/Pass a turn", "imageSize":180}, + {"text":"Cycle through units with 'Next unit' → Click 'Next turn'."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Reassign worked tiles]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Reassign worked tiles!", "centered":true}, + {"extraImage":"Tutorials/Reassign worked tiles", "imageSize":140}, + {"text":"Enter city screen → click the assigned tile to unassign → click an unassigned tile to assign population."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Meet another civilization]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Meet another civilization!", "centered": true}, + {"extraImage":"Tutorials/Meet another civilization", "imageSize":160}, + {"text":"Explore the map until you encounter another civilization!"}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ], + "choices": [ + { + "text":"Got it", + "triggeredUniques": ["Mark tutorial [Meet another civilization] complete"], + }, + ] + }, + { + "name":"Tutorial Task: [Open the options table]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Open the options dialog!", "centered":true}, + {"extraImage":"Tutorials/Open the options table", "imageSize":130}, + {"text":"Click the menu button (top left) → click 'Options'."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Construct an improvement]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Construct an improvement!", "centered":true}, + {"extraImage":"Tutorials/Construct an improvement", "imageSize":150}, + {"text":"Construct a Worker unit →> Move it to a Plains or Grassland tile → Click 'Construct improvement' → Choose the farm → Leave the worker there until it's finished."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Create a trade route]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Create a trade route!", "centered":true}, + {"extraImage":"Tutorials/Create a trade route", "imageSize":120}, + {"text":"Construct roads between your capital and another city. Or, automate your worker and let him get to that eventually."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Conquer a city]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Conquer a city!", "centered":true}, + {"extraImage":"Tutorials/Conquer a city", "imageSize":160}, + {"text":"Bring an enemy city down to low health → Enter the city with a melee unit."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [Move an air unit]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"Move an air unit!", "centered":true}, + {"extraImage":"Tutorials/Move an air unit", "imageSize":140}, + {"text":"Select an air unit →> select another city within range → Move the unit to the other city."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, + { + "name":"Tutorial Task: [See your stats breakdown]", + "presentation":"Floating", + "civilopediaText":[ + {"text":"See your stats breakdown!", "centered":true}, + {"extraImage":"Tutorials/See your stats breakdown", "imageSize":140}, + {"text":"Enter the Overview screen (top right corner) → Click on 'Stats'."}, + ], + "uniques": [ + "Only available ", + "Unavailable " + ] + }, +] diff --git a/core/src/com/unciv/models/ruleset/Event.kt b/core/src/com/unciv/models/ruleset/Event.kt index c99cf82bdd..874000ab2b 100644 --- a/core/src/com/unciv/models/ruleset/Event.kt +++ b/core/src/com/unciv/models/ruleset/Event.kt @@ -4,27 +4,38 @@ 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.UniqueTarget import com.unciv.models.ruleset.unique.UniqueTriggerActivation -import com.unciv.models.stats.INamed +import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.ICivilopediaText -class Event : INamed, ICivilopediaText { - - override var name = "" +class Event : RulesetObject() { + enum class Presentation { None, Alert, Floating } + val presentation = Presentation.Alert var text = "" - override var civilopediaText = listOf() + + override fun getUniqueTarget() = UniqueTarget.Event override fun makeLink() = "Event/$name" // todo: add unrepeatable events var choices = ArrayList() - /** @return `null` when no choice passes the condition tests, so client code can easily bail using Elvis `?:`. */ - fun getMatchingChoices(stateForConditionals: StateForConditionals) = - choices.filter { it.matchesConditions(stateForConditionals) }.ifEmpty { null } + /** @return `null` when no choice passes the condition tests, so client code can easily bail using Elvis `?:`. + * An empty list is possible when the Event definition contains no choices and the event's conditions are fulfilled. + */ + fun getMatchingChoices(stateForConditionals: StateForConditionals): Collection? { + if (!isAvailable(stateForConditionals)) return null + if (choices.isEmpty()) return emptyList() + return choices.filter { it.matchesConditions(stateForConditionals) }.ifEmpty { null } + } + + fun isAvailable(stateForConditionals: StateForConditionals) = + getMatchingUniques(UniqueType.OnlyAvailable, StateForConditionals.IgnoreConditionals).none { !it.conditionalsApply(stateForConditionals) } && + getMatchingUniques(UniqueType.Unavailable, stateForConditionals).none() } class EventChoice : ICivilopediaText { @@ -44,8 +55,10 @@ class EventChoice : ICivilopediaText { fun matchesConditions(stateForConditionals: StateForConditionals) = conditionObjects.all { Conditionals.conditionalApplies(null, it, stateForConditionals) } - fun triggerChoice(civ: Civilization) { + fun triggerChoice(civ: Civilization): Boolean { + var success = false for (unique in triggeredUniqueObjects) - UniqueTriggerActivation.triggerUnique(unique, civ) + if (UniqueTriggerActivation.triggerUnique(unique, civ)) success = true + return success } } diff --git a/core/src/com/unciv/models/ruleset/unique/Conditionals.kt b/core/src/com/unciv/models/ruleset/unique/Conditionals.kt index 8745944651..f9a422f139 100644 --- a/core/src/com/unciv/models/ruleset/unique/Conditionals.kt +++ b/core/src/com/unciv/models/ruleset/unique/Conditionals.kt @@ -1,5 +1,6 @@ package com.unciv.models.ruleset.unique +import com.unciv.UncivGame import com.unciv.logic.GameInfo import com.unciv.logic.battle.CombatAction import com.unciv.logic.city.City @@ -103,6 +104,8 @@ object Conditionals { UniqueType.ConditionalEveryTurns -> checkOnGameInfo { turns % condition.params[0].toInt() == 0 } UniqueType.ConditionalBeforeTurns -> checkOnGameInfo { turns < condition.params[0].toInt() } UniqueType.ConditionalAfterTurns -> checkOnGameInfo { turns >= condition.params[0].toInt() } + UniqueType.ConditionalTutorialsEnabled -> UncivGame.Current.settings.showTutorials + UniqueType.ConditionalTutorialCompleted -> condition.params[0] in UncivGame.Current.settings.tutorialTasksCompleted UniqueType.ConditionalCivFilter -> checkOnCiv { matchesFilter(condition.params[0]) } UniqueType.ConditionalWar -> checkOnCiv { isAtWar() } diff --git a/core/src/com/unciv/models/ruleset/unique/Countables.kt b/core/src/com/unciv/models/ruleset/unique/Countables.kt index 616d08980b..7440239c31 100644 --- a/core/src/com/unciv/models/ruleset/unique/Countables.kt +++ b/core/src/com/unciv/models/ruleset/unique/Countables.kt @@ -12,20 +12,27 @@ object Countables { val gameInfo = stateForConditionals.gameInfo ?: return null - if (countable == "year") return stateForConditionals.gameInfo!!.getYear(gameInfo.turns) + if (countable == "turns") return gameInfo.turns + if (countable == "year") return gameInfo.getYear(gameInfo.turns) val civInfo = stateForConditionals.relevantCiv ?: return null + if (countable == "Cities") return civInfo.cities.size + if (countable == "Units") return civInfo.units.getCivUnitsSize() + if (countable == "Air units") return civInfo.units.getCivUnits().count { it.baseUnit.movesLikeAirUnits() } + if (gameInfo.ruleset.tileResources.containsKey(countable)) return stateForConditionals.getResourceAmount(countable) - if (countable in gameInfo.ruleset.units){ - return civInfo.units.getCivUnits().count { it.name == countable } - } + val unitTypeName = countable.removeSuffix(" units").removeSurrounding("[", "]") + if (unitTypeName in gameInfo.ruleset.unitTypes) + return civInfo.units.getCivUnits().count { it.type.name == unitTypeName } - if (countable in gameInfo.ruleset.buildings){ + if (countable in gameInfo.ruleset.units) + return civInfo.units.getCivUnits().count { it.name == countable } + + if (countable in gameInfo.ruleset.buildings) return civInfo.cities.count { it.cityConstructions.containsBuildingOrEquivalent(countable) } - } return null } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index c9eaec7e84..9594dd1e7c 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -65,7 +65,7 @@ enum class UniqueParameterType( Countable("countable", "1000", "This indicates a number or a numeric variable") { // todo add more countables private val knownValues = setOf( - "year" + "year", "turns", "Cities", "Units" ) override fun isKnownValue(parameterText: String, ruleset: Ruleset) = when { @@ -74,6 +74,7 @@ enum class UniqueParameterType( Stat.isStat(parameterText) -> true parameterText in ruleset.tileResources -> true parameterText in ruleset.units -> true + parameterText.removeSuffix(" units").removeSurrounding("[", "]") in ruleset.unitTypes -> true else -> parameterText in ruleset.buildings } }, diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt index c14578f665..7e7ec0bb7e 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt @@ -58,6 +58,7 @@ enum class UniqueTarget( Tutorial, CityState(inheritsFrom = Global), ModOptions, + Event, // Modifiers Conditional("Modifiers that can be added to other uniques to limit when they will be active", modifierType = ModifierType.Conditional), diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index 4f9b6593b4..85794a0fb4 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -23,6 +23,7 @@ import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.tile.Tile import com.unciv.models.UpgradeUnitAction import com.unciv.models.ruleset.BeliefType +import com.unciv.models.ruleset.Event import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats @@ -115,11 +116,20 @@ object UniqueTriggerActivation { val event = ruleset.events[unique.params[0]] ?: return null val choices = event.getMatchingChoices(stateForConditionals) ?: return null - return { - if (civInfo.isAI()) choices.random().triggerChoice(civInfo) - else civInfo.popupAlerts.add(PopupAlert(AlertType.Event, event.name)) + if (civInfo.isAI() || event.presentation == Event.Presentation.None) return { + choices.randomOrNull()?.triggerChoice(civInfo) ?: false + } + if (event.presentation == Event.Presentation.Alert) return { + civInfo.popupAlerts.add(PopupAlert(AlertType.Event, event.name)) true } + // if (event.presentation == Event.Presentation.Floating) return { //todo: Park them in a Queue in GameInfo??? + throw NotImplementedError("Event ${event.name} has presentation type ${event.presentation} which is not implemented for use via TriggerEvent") + } + + UniqueType.MarkTutorialComplete -> return { + UncivGame.Current.settings.addCompletedTutorialTask(unique.params[0]) + true } UniqueType.OneTimeFreeUnit -> { diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index f08d033a71..48d005317b 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -276,15 +276,16 @@ enum class UniqueType( MaxNumberBuildable("Limited to [amount] per Civilization", UniqueTarget.Building, UniqueTarget.Unit), HiddenBeforeAmountPolicies("Hidden until [amount] social policy branches have been completed", UniqueTarget.Building, UniqueTarget.Unit), /** A special unique, as it only activates [RejectionReasonType] when it has conditionals that *do not* apply. - * Meant to be used together with conditionals, like "Buildable only ". + * Meant to be used together with conditionals, like `"Only available "`. * Restricts Upgrade/Transform pathways. * @See [CanOnlyBeBuiltWhen] */ OnlyAvailable("Only available", UniqueTarget.Unit, UniqueTarget.Building, UniqueTarget.Improvement, - UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion, UniqueTarget.Ruins, UniqueTarget.FollowerBelief, UniqueTarget.FounderBelief, + UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion, UniqueTarget.Ruins, + UniqueTarget.FollowerBelief, UniqueTarget.FounderBelief, UniqueTarget.Event, docDescription = "Meant to be used together with conditionals, like \"Only available \". Only allows Building when ALL conditionals are met. Will also block Upgrade and Transform actions. See also CanOnlyBeBuiltWhen"), Unavailable("Unavailable", UniqueTarget.Unit, UniqueTarget.Building, UniqueTarget.Improvement, - UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion, UniqueTarget.Ruins, + UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion, UniqueTarget.Ruins, UniqueTarget.Event, docDescription = "Meant to be used together with conditionals, like \"Unavailable \"."), ConvertFoodToProductionWhenConstructed("Excess Food converted to Production when under construction", UniqueTarget.Building, UniqueTarget.Unit), @@ -644,7 +645,8 @@ enum class UniqueType( ConditionalEveryTurns("every [positiveAmount] turns", UniqueTarget.Conditional), ConditionalBeforeTurns("before [amount] turns", UniqueTarget.Conditional), ConditionalAfterTurns("after [amount] turns", UniqueTarget.Conditional), - + ConditionalTutorialsEnabled("if tutorials are enabled", UniqueTarget.Conditional, flags = UniqueFlag.setOfHiddenToUsers), // Hidden as no translations needed for now + ConditionalTutorialCompleted("if tutorial [comment] is completed", UniqueTarget.Conditional, flags = UniqueFlag.setOfHiddenToUsers), // Hidden as no translations needed for now /////// civ conditionals ConditionalCivFilter("for [civFilter]", UniqueTarget.Conditional), @@ -890,6 +892,7 @@ enum class UniqueType( AllowRazeHolyCity("Allow raze holy city", UniqueTarget.ModOptions, flags = UniqueFlag.setOfNoConditionals), SuppressWarnings("Suppress warning [validationWarning]", *UniqueTarget.CanIncludeSuppression, flags = UniqueFlag.setOfHiddenNoConditionals, docDescription = Suppression.uniqueDocDescription), + MarkTutorialComplete("Mark tutorial [comment] complete", UniqueTarget.Event, flags = UniqueFlag.setOfHiddenNoConditionals), // Declarative Mod compatibility (see [ModCompatibility]): // Note there is currently no display for these, but UniqueFlag.HiddenToUsers is not set. diff --git a/core/src/com/unciv/models/translations/TranslationFileWriter.kt b/core/src/com/unciv/models/translations/TranslationFileWriter.kt index a58d5fcdac..789ee28014 100644 --- a/core/src/com/unciv/models/translations/TranslationFileWriter.kt +++ b/core/src/com/unciv/models/translations/TranslationFileWriter.kt @@ -428,7 +428,7 @@ object TranslationFileWriter { // Promotion names are not uniques but since we did the "[unitName] ability" // they need the "parameters" treatment too // Same for victory milestones - (field.name == "uniques" || field.name == "promotions" || field.name == "milestones") + (field.name in fieldsToProcessParameters) && (fieldValue is java.util.AbstractCollection<*>) -> for (item in fieldValue) if (item is String) submitString(item, Unique(item)) else serializeElement(item!!) @@ -464,6 +464,8 @@ object TranslationFileWriter { "RuinReward.uniques", "TerrainType.name", "CityStateType.friendBonusUniques", "CityStateType.allyBonusUniques", "Era.citySound", + "keyShortcut", + "Event.name" // Presently not shown anywhere ) /** Specifies Enums where the name property _is_ translatable, by Class name */ @@ -476,6 +478,11 @@ object TranslationFileWriter { UniqueParameterType.Comment ) + private val fieldsToProcessParameters = setOf( + "uniques", "promotions", "milestones", + "triggeredUniques", "conditions" + ) + private fun isFieldTypeRelevant(type: Class<*>) = type == String::class.java || type == java.util.ArrayList::class.java || diff --git a/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt b/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt index 7e45bb7d03..a254391381 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTab.kt @@ -225,7 +225,7 @@ class StatsOverviewTab( private fun updateScoreTable() = scoreTable.apply { clear() val scoreHeader = Table() - val scoreIcon = ImageGetter.getImage("CityStateIcons/Cultured") + val scoreIcon = ImageGetter.getImage("OtherIcons/Score") scoreIcon.color = Color.FIREBRICK scoreHeader.add(scoreIcon).padRight(1f).size(Constants.headingFontSize.toFloat()) scoreHeader.add("Score".toLabel(fontSize = Constants.headingFontSize)) diff --git a/core/src/com/unciv/ui/screens/victoryscreen/RankingType.kt b/core/src/com/unciv/ui/screens/victoryscreen/RankingType.kt index 6b13833928..475f6d29b0 100644 --- a/core/src/com/unciv/ui/screens/victoryscreen/RankingType.kt +++ b/core/src/com/unciv/ui/screens/victoryscreen/RankingType.kt @@ -10,7 +10,7 @@ enum class RankingType( val idForSerialization: String ) { // production, gold, happiness, and culture already have icons added when the line is `tr()`anslated - Score({ ImageGetter.getImage("CityStateIcons/Cultured").apply { color = Color.FIREBRICK } }, "S"), + Score({ ImageGetter.getImage("OtherIcons/Score").apply { color = Color.FIREBRICK } }, "S"), Population({ ImageGetter.getStatIcon("Population") }, "N"), CropYield("Crop Yield", { ImageGetter.getStatIcon("Food") }, "C"), Production("P"), diff --git a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt index 3fa2990c90..f33cbdff07 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/AlertPopup.kt @@ -18,26 +18,20 @@ 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.EventChoice -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 import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicTrackChooserFlags -import com.unciv.ui.components.UncivTooltip.Companion.addTooltip import com.unciv.ui.components.extensions.disable import com.unciv.ui.components.extensions.pad import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toTextButton -import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.keyShortcuts import com.unciv.ui.components.input.onActivation import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.Popup -import com.unciv.ui.screens.civilopediascreen.FormattedLine -import com.unciv.ui.screens.civilopediascreen.MarkupRenderer import com.unciv.ui.screens.diplomacyscreen.LeaderIntroTable import com.unciv.ui.screens.victoryscreen.VictoryScreen import java.util.EnumSet @@ -507,18 +501,9 @@ class AlertPopup( /** Returns if event was triggered correctly */ private fun addEvent(): Boolean { val event = gameInfo.ruleset.events[popupAlert.value] ?: return false - - val stateForConditionals = StateForConditionals(gameInfo.currentPlayerCiv) - val choices = event.getMatchingChoices(stateForConditionals) - ?: return false - - if (event.text.isNotEmpty()) - addGoodSizedLabel(event.text) - if (event.civilopediaText.isNotEmpty()) { - add(event.renderCivilopediaText(stageWidth * 0.5f, ::openCivilopedia)).row() - } - - for (choice in choices) addChoice(choice) + val render = RenderEvent(event, worldScreen) { close() } + if (!render.isValid) return false + add(render).pad(0f).row() return true } @@ -529,30 +514,4 @@ class AlertPopup( worldScreen.shouldUpdate = true super.close() } - - private fun addChoice(choice: EventChoice) { - addSeparator() - - val button = choice.text.toTextButton() - button.onActivation { - close() - choice.triggerChoice(gameInfo.currentPlayerCiv) - } - val key = KeyCharAndCode.parse(choice.keyShortcut) - if (key != KeyCharAndCode.UNKNOWN) { - button.keyShortcuts.add(key) - button.addTooltip(key) - } - add(button).row() - - val lines = ( - choice.civilopediaText.asSequence() - + choice.triggeredUniqueObjects.asSequence() - .filterNot { it.isHiddenToUsers() } - .map { FormattedLine(it) } - ).asIterable() - add(MarkupRenderer.render(lines, stageWidth * 0.5f, linkAction = ::openCivilopedia)).row() - } - - private fun openCivilopedia(link: String) = worldScreen.openCivilopedia(link) } diff --git a/core/src/com/unciv/ui/screens/worldscreen/RenderEvent.kt b/core/src/com/unciv/ui/screens/worldscreen/RenderEvent.kt new file mode 100644 index 0000000000..1072e0fb9f --- /dev/null +++ b/core/src/com/unciv/ui/screens/worldscreen/RenderEvent.kt @@ -0,0 +1,78 @@ +package com.unciv.ui.screens.worldscreen + +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.utils.Align +import com.unciv.models.ruleset.Event +import com.unciv.models.ruleset.EventChoice +import com.unciv.models.ruleset.unique.StateForConditionals +import com.unciv.ui.components.UncivTooltip.Companion.addTooltip +import com.unciv.ui.components.extensions.addSeparator +import com.unciv.ui.components.extensions.toTextButton +import com.unciv.ui.components.input.KeyCharAndCode +import com.unciv.ui.components.input.keyShortcuts +import com.unciv.ui.components.input.onActivation +import com.unciv.ui.components.widgets.WrappableLabel +import com.unciv.ui.screens.civilopediascreen.FormattedLine +import com.unciv.ui.screens.civilopediascreen.MarkupRenderer + +/** Renders an [Event] for [AlertPopup] or a floating tutorial task on [WorldScreen] */ +class RenderEvent( + event: Event, + val worldScreen: WorldScreen, + val onChoice: (EventChoice) -> Unit +) : Table() { + private val gameInfo get() = worldScreen.gameInfo + private val stageWidth get() = worldScreen.stage.width + + val isValid: Boolean + + //todo check generated translations + + init { + defaults().fillX().center().pad(5f) + + val stateForConditionals = StateForConditionals(gameInfo.currentPlayerCiv) + val choices = event.getMatchingChoices(stateForConditionals) + isValid = choices != null + if (isValid) { + if (event.text.isNotEmpty()) { + add(WrappableLabel(event.text, stageWidth * 0.5f).apply { + wrap = true + setAlignment(Align.center) + optimizePrefWidth() + }).row() + } + if (event.civilopediaText.isNotEmpty()) { + add(event.renderCivilopediaText(stageWidth * 0.5f, ::openCivilopedia)).row() + } + + for (choice in choices!!) addChoice(choice) + } + } + + private fun addChoice(choice: EventChoice) { + addSeparator() + + val button = choice.text.toTextButton() + button.onActivation { + onChoice(choice) + choice.triggerChoice(gameInfo.currentPlayerCiv) + } + val key = KeyCharAndCode.parse(choice.keyShortcut) + if (key != KeyCharAndCode.UNKNOWN) { + button.keyShortcuts.add(key) + button.addTooltip(key) + } + add(button).row() + + val lines = ( + choice.civilopediaText.asSequence() + + choice.triggeredUniqueObjects.asSequence() + .filterNot { it.isHiddenToUsers() } + .map { FormattedLine(it) } + ).asIterable() + add(MarkupRenderer.render(lines, stageWidth * 0.5f, linkAction = ::openCivilopedia)).row() + } + + private fun openCivilopedia(link: String) = worldScreen.openCivilopedia(link) +} diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index f57eec7248..cb75c33a88 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -21,11 +21,12 @@ import com.unciv.logic.multiplayer.storage.MultiplayerAuthException import com.unciv.logic.trade.TradeEvaluation import com.unciv.models.TutorialTrigger import com.unciv.models.metadata.GameSetupInfo +import com.unciv.models.ruleset.Event import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.components.extensions.centerX import com.unciv.ui.components.extensions.darken -import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.input.KeyShortcutDispatcherVeto import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.KeyboardPanningListener @@ -122,6 +123,7 @@ class WorldScreen( private val tutorialTaskTable = Table().apply { background = skinStrings.getUiBackground("WorldScreen/TutorialTaskTable", tintColor = skinStrings.skinConfig.baseColor.darken(0.5f)) } + private var tutorialTaskTableHash = 0 private var nextTurnUpdateJob: Job? = null @@ -149,10 +151,10 @@ class WorldScreen( stage.scrollFocus = mapHolder stage.addActor(notificationsScroll) // very low in z-order, so we're free to let it extend _below_ tile info and minimap if we want stage.addActor(minimapWrapper) + stage.addActor(tutorialTaskTable) // behind topBar! stage.addActor(topBar) stage.addActor(statusButtons) stage.addActor(techPolicyAndDiplomacy) - stage.addActor(tutorialTaskTable) stage.addActor(zoomController) zoomController.isVisible = UncivGame.Current.settings.showZoomButtons @@ -401,6 +403,8 @@ class WorldScreen( else mapHolder.updateTiles(viewingCiv) topBar.update(selectedCiv) + if (tutorialTaskTable.isVisible) + tutorialTaskTable.y = topBar.getYForTutorialTask() - tutorialTaskTable.height if (techPolicyAndDiplomacy.update()) displayTutorial(TutorialTrigger.OtherCivEncountered) @@ -450,50 +454,16 @@ class WorldScreen( zoomController.setPosition(stage.width - posZoomFromRight - 10f, 10f, Align.bottomRight) } - private fun getCurrentTutorialTask(): String { - val completedTasks = game.settings.tutorialTasksCompleted - if (!completedTasks.contains("Move unit")) - return "Move a unit!\nClick on a unit > Click on a destination > Click the arrow popup" - if (!completedTasks.contains("Found city")) - return "Found a city!\nSelect the Settler (flag unit) > Click on 'Found city' (bottom-left corner)" - if (!completedTasks.contains("Enter city screen")) - return "Enter the city screen!\nClick the city button twice" - if (!completedTasks.contains("Pick technology")) - return "Pick a technology to research!\nClick on the tech button (greenish, top left) > " + - "\n select technology > click 'Research' (bottom right)" - if (!completedTasks.contains("Pick construction")) - return "Pick a construction!\nEnter city screen > Click on a unit or building (bottom left side) >" + - " \n click 'add to queue'" - if (!completedTasks.contains("Pass a turn")) - return "Pass a turn!\nCycle through units with 'Next unit' > Click 'Next turn'" - if (!completedTasks.contains("Reassign worked tiles")) - return "Reassign worked tiles!\nEnter city screen > click the assigned (green) tile to unassign > " + - "\n click an unassigned tile to assign population" - if (!completedTasks.contains("Meet another civilization")) - return "Meet another civilization!\nExplore the map until you encounter another civilization!" - if (!completedTasks.contains("Open the options table")) - return "Open the options table!\nClick the menu button (top left) > click 'Options'" - if (!completedTasks.contains("Construct an improvement")) - return "Construct an improvement!\nConstruct a Worker unit > Move to a Plains or Grassland tile > " + - "\n Click 'Construct improvement' (above the unit table, bottom left)" + - "\n > Choose the farm > \n Leave the worker there until it's finished" - if (!completedTasks.contains("Create a trade route") - && viewingCiv.cache.citiesConnectedToCapitalToMediums.any { it.key.civ == viewingCiv }) - game.settings.addCompletedTutorialTask("Create a trade route") - if (viewingCiv.cities.size > 1 && !completedTasks.contains("Create a trade route")) - return "Create a trade route!\nConstruct roads between your capital and another city" + - "\nOr, automate your worker and let him get to that eventually" - if (viewingCiv.isAtWar() && !completedTasks.contains("Conquer a city")) - return "Conquer a city!\nBring an enemy city down to low health > " + - "\nEnter the city with a melee unit" - if (viewingCiv.units.getCivUnits().any { it.baseUnit.movesLikeAirUnits() } && !completedTasks.contains("Move an air unit")) - return "Move an air unit!\nSelect an air unit > select another city within range > " + - "\nMove the unit to the other city" - if (!completedTasks.contains("See your stats breakdown")) - return "See your stats breakdown!\nEnter the Overview screen (top right corner) >" + - "\nClick on 'Stats'" - - return "" + private fun getCurrentTutorialTask(): Event? { + if (!game.settings.tutorialTasksCompleted.contains("Create a trade route")) { + if (viewingCiv.cache.citiesConnectedToCapitalToMediums.any { it.key.civ == viewingCiv }) + game.settings.addCompletedTutorialTask("Create a trade route") + } + val stateForConditionals = StateForConditionals(viewingCiv) + return gameInfo.ruleset.events.values.firstOrNull { + it.presentation == Event.Presentation.Floating && + it.isAvailable(stateForConditionals) + } } private fun displayTutorialsOnUpdate() { @@ -524,27 +494,38 @@ class WorldScreen( } private fun displayTutorialTaskOnUpdate() { - tutorialTaskTable.clear() - val tutorialTask = getCurrentTutorialTask() - if (tutorialTask == "" || !game.settings.showTutorials || viewingCiv.isDefeated()) { + fun setInvisible() { tutorialTaskTable.isVisible = false - return + tutorialTaskTable.clear() + tutorialTaskTableHash = 0 } + if (!game.settings.showTutorials || viewingCiv.isDefeated()) return setInvisible() + val tutorialTask = getCurrentTutorialTask() ?: return setInvisible() - tutorialTaskTable.isVisible = true if (!UncivGame.Current.isTutorialTaskCollapsed) { - tutorialTaskTable.add(tutorialTask.toLabel() - .apply { setAlignment(Align.center) }).pad(10f) + val hash = tutorialTask.hashCode() // Default implementation is OK - we see the same instance or not + if (hash != tutorialTaskTableHash) { + val renderEvent = RenderEvent(tutorialTask, this) { + shouldUpdate = true + } + if (!renderEvent.isValid) return setInvisible() + tutorialTaskTable.clear() + tutorialTaskTable.add(renderEvent).pad(10f) + tutorialTaskTableHash = hash + } } else { - tutorialTaskTable.add(ImageGetter.getImage("CityStateIcons/Cultured").apply { setSize(30f,30f) }).pad(5f) + tutorialTaskTable.clear() + tutorialTaskTable.add(ImageGetter.getImage("OtherIcons/HiddenTutorialTask").apply { setSize(30f,30f) }).pad(5f) + tutorialTaskTableHash = 0 } tutorialTaskTable.pack() tutorialTaskTable.centerX(stage) - tutorialTaskTable.y = topBar.y - tutorialTaskTable.height + tutorialTaskTable.y = topBar.getYForTutorialTask() - tutorialTaskTable.height tutorialTaskTable.onClick { UncivGame.Current.isTutorialTaskCollapsed = !UncivGame.Current.isTutorialTaskCollapsed displayTutorialTaskOnUpdate() } + tutorialTaskTable.isVisible = true } private fun updateSelectedCiv() { @@ -794,7 +775,7 @@ class WorldScreen( shouldUpdate = true return } - + if (bottomUnitTable.selectedSpy != null) { bottomUnitTable.selectSpy(null) shouldUpdate = true diff --git a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt index 1a709a2169..73cd24a59f 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt @@ -67,6 +67,7 @@ class WorldScreenTopBar(internal val worldScreen: WorldScreen) : Table() { private val overviewButton = OverviewAndSupplyTable(worldScreen) private val leftFiller: BackgroundActor private val rightFiller: BackgroundActor + private var baseHeight = 0f companion object { /** When the "fillers" are used, this is added to the required height, alleviating the "gap" problem a little. */ @@ -100,6 +101,8 @@ class WorldScreenTopBar(internal val worldScreen: WorldScreen) : Table() { setLayoutEnabled(true) } + internal fun getYForTutorialTask(): Float = y + height - baseHeight + /** Performs the layout tricks mentioned in the class Kdoc */ private fun updateLayout() { val targetWidth = stage.width @@ -122,7 +125,7 @@ class WorldScreenTopBar(internal val worldScreen: WorldScreen) : Table() { add(resourceTable).colspan(3).growX().width(targetWidth).row() layout() // force rowHeight calculation - validate is not enough - Table quirks val statsRowHeight = getRowHeight(0) - val baseHeight = statsRowHeight + getRowHeight(1) + baseHeight = statsRowHeight + getRowHeight(1) fun addFillers(fillerHeight: Float) { add(leftFiller).size(selectedCivWidth, fillerHeight + gapFillingExtraHeight) diff --git a/docs/Credits.md b/docs/Credits.md index 21b4cb13ba..2f8ea07f7c 100644 --- a/docs/Credits.md +++ b/docs/Credits.md @@ -745,6 +745,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https: - [down](https://thenounproject.com/icon/down-39378/) by Cengiz SARI for Show unit destination - [Cat](https://thenounproject.com/icon/cat-158942/) by Josi for Politics overview diagram legend - [Bell](https://thenounproject.com/icon/bell-2054409) by Lyhn, transparency modified, for Notifications (overview, unhide button) +- [Galileo Donatello](https://en.wikipedia.org/wiki/File:Galileo_Donato.jpg) for the "Meet another civilization" tutorial: Public domain ### Main menu diff --git a/docs/Modders/Unique-parameters.md b/docs/Modders/Unique-parameters.md index 0bcb84b91b..df2d04479f 100644 --- a/docs/Modders/Unique-parameters.md +++ b/docs/Modders/Unique-parameters.md @@ -287,8 +287,10 @@ Allowed values are: Indicates *something that can be counted*, used both for comparisons and for multiplying uniques Allowed values: -- `year` +- `year`, `turns` +- `Cities`, `Units`, `Air units` - these count your total number - Unit name (counts your existing units) +- ` units` (e.g. `Mounted units`) - counts your units by their type (this is not a filter, use the unitType verbatim) - Building name (counts your existing buildings) - Stat name - gets the stat *reserve*, not the amount per turn (can be city stats or civilization stats, depending on where the unique is used) - Resource name (can be city stats or civilization stats, depending on where the unique is used) diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index d0f3d8cb4c..d4c3f48502 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -1007,11 +1007,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl ??? example "Only available" Meant to be used together with conditionals, like "Only available ". Only allows Building when ALL conditionals are met. Will also block Upgrade and Transform actions. See also CanOnlyBeBuiltWhen - Applicable to: Tech, Policy, FounderBelief, FollowerBelief, Building, Unit, Promotion, Improvement, Ruins + Applicable to: Tech, Policy, FounderBelief, FollowerBelief, Building, Unit, Promotion, Improvement, Ruins, Event ??? example "Unavailable" Meant to be used together with conditionals, like "Unavailable ". - Applicable to: Tech, Policy, Building, Unit, Promotion, Improvement, Ruins + Applicable to: Tech, Policy, Building, Unit, Promotion, Improvement, Ruins, Event ??? example "Cannot be hurried" Applicable to: Tech, Building @@ -1915,6 +1915,12 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl Applicable to: ModOptions +## Event uniques +??? example "Mark tutorial [comment] complete" + Example: "Mark tutorial [comment] complete" + + Applicable to: Event + ## Conditional uniques !!! note "" @@ -1945,6 +1951,14 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl Applicable to: Conditional +??? example "<if tutorials are enabled>" + Applicable to: Conditional + +??? example "<if tutorial [comment] is completed>" + Example: "<if tutorial [comment] is completed>" + + Applicable to: Conditional + ??? example "<for [civFilter]>" Example: "<for [City-States]>"