Use Events for the floating "Tutorials" (#11717)

* Split off reuses of CityStateIcons/Cultured to allow modding separately

* Reposition floating tutorials in case the TopBar moved its buttons

* Event definition, art and basic support

* Split off Event rendering from AlertPopup

* Support Event presentation modes and replace hardcoded floating tutorials

* "Meet another civilization" art - can't find any better

* Tweak TranslationFileWriter and some polishing
This commit is contained in:
SomeTroglodyte 2024-06-10 21:22:18 +02:00 committed by GitHub
parent b496784ab5
commit d39c7a97bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 439 additions and 152 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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

View File

@ -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 <if tutorials are enabled> <when number of [Units] is greater than [0]>",
"Unavailable <if tutorial [Move unit] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [Settler] is greater than [0]>",
"Unavailable <if tutorial [Found city] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [Cities] is greater than [0]>",
"Unavailable <if tutorial [Enter city screen] is completed>"
]
},
{
"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 <if tutorials are enabled>",
"Unavailable <if tutorial [Pick technology] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [Cities] is greater than [0]>",
"Unavailable <if tutorial [Pick construction] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [turns] is less than [3]>",
"Unavailable <if tutorial [Pass a turn] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [Cities] is greater than [0]>",
"Unavailable <if tutorial [Reassign worked tiles] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [turns] is greater than [1]>",
"Unavailable <if tutorial [Meet another civilization] is completed>"
],
"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 <if tutorials are enabled>",
"Unavailable <if tutorial [Open the options table] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [Cities] is greater than [0]>",
"Unavailable <if tutorial [Construct an improvement] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [Cities] is greater than [1]>",
"Unavailable <if tutorial [Create a trade route] is completed>"
]
},
{
"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 <if tutorials are enabled> <when at war>",
"Unavailable <if tutorial [Conquer a city] is completed>"
]
},
{
"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 <if tutorials are enabled> <when number of [Air units] is greater than [0]>",
"Unavailable <if tutorial [Move an air unit] is completed>"
]
},
{
"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 <if tutorials are enabled>",
"Unavailable <if tutorial [See your stats breakdown] is completed>"
]
},
]

View File

@ -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<FormattedLine>()
override fun getUniqueTarget() = UniqueTarget.Event
override fun makeLink() = "Event/$name"
// todo: add unrepeatable events
var choices = ArrayList<EventChoice>()
/** @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<EventChoice>? {
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
}
}

View File

@ -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() }

View File

@ -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
}

View File

@ -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
}
},

View File

@ -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),

View File

@ -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 -> {

View File

@ -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 <after adopting [policy]> <while the empire is happy>".
* Meant to be used together with conditionals, like `"Only available <after adopting [Piety]> <while the empire is happy>"`.
* 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 <after adopting [policy]> <while the empire is happy>\". 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 <after generating a Great Prophet>\"."),
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.

View File

@ -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 ||

View File

@ -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))

View File

@ -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"),

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)
- `<unit type> 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)

View File

@ -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 <after adopting [policy]> <while the empire is happy>". 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 <after generating a Great Prophet>".
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 "&lt;if tutorials are enabled&gt;"
Applicable to: Conditional
??? example "&lt;if tutorial [comment] is completed&gt;"
Example: "&lt;if tutorial [comment] is completed&gt;"
Applicable to: Conditional
??? example "&lt;for [civFilter]&gt;"
Example: "&lt;for [City-States]&gt;"