mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
Moddable prettier Tutorials - Step 1 (#7064)
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package com.unciv.models
|
||||
|
||||
enum class Tutorial(val value: String, val isCivilopedia: Boolean = !value.startsWith("_")) {
|
||||
|
||||
enum class TutorialTrigger(val value: String, val isCivilopedia: Boolean = !value.startsWith("_")) {
|
||||
|
||||
Introduction("Introduction"),
|
||||
NewGame("New_Game"),
|
||||
@ -44,9 +45,4 @@ enum class Tutorial(val value: String, val isCivilopedia: Boolean = !value.start
|
||||
Inquisitors("Inquisitors"),
|
||||
MayanCalendar("Maya_Long_Count_calendar_cycle"),
|
||||
WeLoveTheKingDay("We_Love_The_King_Day"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun findByName(name: String): Tutorial? = values().find { it.value == name }
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import com.unciv.ui.civilopedia.FormattedLine
|
||||
import com.unciv.ui.utils.Fonts
|
||||
import com.unciv.ui.utils.colorFromRGB
|
||||
|
||||
class Era : RulesetObject(), IHasUniques {
|
||||
class Era : RulesetObject() {
|
||||
var eraNumber: Int = -1
|
||||
var researchAgreementCost = 300
|
||||
var startingSettlerCount = 1
|
||||
|
10
core/src/com/unciv/models/ruleset/Tutorial.kt
Normal file
10
core/src/com/unciv/models/ruleset/Tutorial.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package com.unciv.models.ruleset
|
||||
|
||||
import com.unciv.models.ruleset.unique.UniqueTarget
|
||||
|
||||
class Tutorial : RulesetObject() {
|
||||
//todo migrate to civilopediaText then remove or deprecate
|
||||
val steps: Array<String>? = null
|
||||
override fun getUniqueTarget() = UniqueTarget.Tutorial
|
||||
override fun makeLink() = "Tutorial/$name"
|
||||
}
|
@ -47,6 +47,7 @@ enum class UniqueTarget(val inheritsFrom: UniqueTarget? = null) {
|
||||
Ruins(Triggerable),
|
||||
|
||||
// Other
|
||||
Tutorial,
|
||||
CityState,
|
||||
ModOptions,
|
||||
Conditional,
|
||||
@ -1072,4 +1073,3 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
|
||||
|
||||
// I didn't put this is a companion object because APPARENTLY doing that means you can't use it in the init function.
|
||||
val numberRegex = Regex("\\d+$") // Any number of trailing digits
|
||||
|
||||
|
@ -5,7 +5,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Button
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.models.TutorialTrigger
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.ruleset.Policy
|
||||
import com.unciv.models.ruleset.PolicyBranch
|
||||
@ -24,7 +24,7 @@ class PolicyPickerScreen(val worldScreen: WorldScreen, civInfo: CivilizationInfo
|
||||
|
||||
init {
|
||||
val policies = viewingCiv.policies
|
||||
displayTutorial(Tutorial.CultureAndPolicies)
|
||||
displayTutorial(TutorialTrigger.CultureAndPolicies)
|
||||
|
||||
rightSideButton.setText(when {
|
||||
policies.allPoliciesAdopted(checkEra = false) ->
|
||||
|
@ -5,7 +5,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.map.MapUnit
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.models.TutorialTrigger
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.ruleset.unit.Promotion
|
||||
@ -49,7 +49,7 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen() {
|
||||
|
||||
val unitType = unit.type
|
||||
val promotionsForUnitType = unit.civInfo.gameInfo.ruleSet.unitPromotions.values.filter {
|
||||
it.unitTypes.contains(unitType.name) || unit.promotions.promotions.contains(it.name)
|
||||
it.unitTypes.contains(unitType.name) || unit.promotions.promotions.contains(it.name)
|
||||
}
|
||||
val unitAvailablePromotions = unit.promotions.getAvailablePromotions()
|
||||
|
||||
@ -63,7 +63,7 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen() {
|
||||
icon = ImageGetter.getUnitIcon(unit.name).surroundWithCircle(80f),
|
||||
defaultText = unit.name,
|
||||
validate = { it != unit.name},
|
||||
actionOnOk = { userInput ->
|
||||
actionOnOk = { userInput ->
|
||||
unit.instanceName = userInput
|
||||
this.game.setScreen(PromotionPickerScreen(unit))
|
||||
}
|
||||
@ -106,7 +106,7 @@ class PromotionPickerScreen(val unit: MapUnit) : PickerScreen() {
|
||||
}
|
||||
topTable.add(availablePromotionsGroup)
|
||||
|
||||
displayTutorial(Tutorial.Experience)
|
||||
displayTutorial(TutorialTrigger.Experience)
|
||||
}
|
||||
|
||||
private fun setScrollY(scrollY: Float): PromotionPickerScreen {
|
||||
|
@ -1,29 +1,35 @@
|
||||
package com.unciv.ui.tutorials
|
||||
|
||||
import com.badlogic.gdx.utils.Array
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.json.fromJsonFile
|
||||
import com.unciv.json.json
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.models.TutorialTrigger
|
||||
import com.unciv.models.ruleset.Tutorial
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.stats.INamed
|
||||
import com.unciv.ui.civilopedia.FormattedLine
|
||||
import com.unciv.ui.civilopedia.SimpleCivilopediaText
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
|
||||
|
||||
class TutorialController(screen: BaseScreen) {
|
||||
|
||||
private val tutorialQueue = mutableSetOf<Tutorial>()
|
||||
private val tutorialQueue = mutableSetOf<TutorialTrigger>()
|
||||
private var isTutorialShowing = false
|
||||
var allTutorialsShowedCallback: (() -> Unit)? = null
|
||||
private val tutorialRender = TutorialRender(screen)
|
||||
private val tutorials = json().fromJsonFile(LinkedHashMap<String, Array<String>>().javaClass, "jsons/Tutorials.json")
|
||||
|
||||
fun showTutorial(tutorial: Tutorial) {
|
||||
//todo These should live in a ruleset allowing moddability
|
||||
private val tutorials: LinkedHashMap<String, Tutorial> =
|
||||
json().fromJsonFile(Array<Tutorial>::class.java, "jsons/Tutorials.json")
|
||||
.associateByTo(linkedMapOf()) { it.name }
|
||||
|
||||
fun showTutorial(tutorial: TutorialTrigger) {
|
||||
tutorialQueue.add(tutorial)
|
||||
showTutorialIfNeeded()
|
||||
}
|
||||
|
||||
private fun removeTutorial(tutorial: Tutorial) {
|
||||
private fun removeTutorial(tutorial: TutorialTrigger) {
|
||||
isTutorialShowing = false
|
||||
tutorialQueue.remove(tutorial)
|
||||
with(UncivGame.Current.settings) {
|
||||
@ -49,32 +55,32 @@ class TutorialController(screen: BaseScreen) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTutorial(tutorial: Tutorial): Array<String> {
|
||||
return tutorials[tutorial.value] ?: Array()
|
||||
private fun getTutorial(tutorial: TutorialTrigger): Array<String> {
|
||||
val name = tutorial.value.replace('_', ' ').trimStart()
|
||||
return tutorials[name]?.steps ?: emptyArray()
|
||||
}
|
||||
|
||||
/** Wrapper for a Tutorial, supports INamed and ICivilopediaText,
|
||||
* and already provisions for the display of an ExtraImage on top.
|
||||
* @param rawName from Tutorial.value, with underscores (this wrapper replaces them with spaces)
|
||||
* @param lines Array of lines exactly as stored in a TutorialController.tutorials MapEntry
|
||||
* @param name from Tutorial.name, also used for ExtraImage (with spaces replaced by underscores)
|
||||
* @param tutorial provides [Tutorial.civilopediaText] and [Tutorial.steps] for display
|
||||
*/
|
||||
//todo Replace - Civilopedia should display Tutorials directly as the RulesetObjects they are
|
||||
class CivilopediaTutorial(
|
||||
rawName: String,
|
||||
lines: Array<String>
|
||||
override var name: String,
|
||||
tutorial: Tutorial
|
||||
) : INamed, SimpleCivilopediaText(
|
||||
sequenceOf(FormattedLine(extraImage = rawName)),
|
||||
lines.asSequence()
|
||||
) {
|
||||
override var name = rawName.replace("_", " ")
|
||||
}
|
||||
sequenceOf(FormattedLine(extraImage = name.replace(' ', '_'))) + tutorial.civilopediaText.asSequence(),
|
||||
tutorial.steps?.asSequence() ?: emptySequence()
|
||||
)
|
||||
|
||||
/** Get all Tutorials intended to be displayed in the Civilopedia
|
||||
* as a List of wrappers supporting INamed and ICivilopediaText
|
||||
*/
|
||||
fun getCivilopediaTutorials(): List<CivilopediaTutorial> {
|
||||
val civilopediaTutorials = tutorials.filter {
|
||||
Tutorial.findByName(it.key)!!.isCivilopedia
|
||||
}.map {
|
||||
!it.value.hasUnique(UniqueType.HiddenFromCivilopedia)
|
||||
}.map {
|
||||
tutorial -> CivilopediaTutorial(tutorial.key, tutorial.value)
|
||||
}
|
||||
return civilopediaTutorials
|
||||
|
@ -1,14 +1,14 @@
|
||||
package com.unciv.ui.tutorials
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.utils.Array
|
||||
import com.unciv.Constants
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.models.TutorialTrigger
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popup.Popup
|
||||
import com.unciv.ui.utils.*
|
||||
|
||||
data class TutorialForRender(val tutorial: Tutorial, val texts: Array<String>)
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class TutorialForRender(val tutorial: TutorialTrigger, val texts: Array<String>)
|
||||
|
||||
class TutorialRender(private val screen: BaseScreen) {
|
||||
|
||||
@ -17,25 +17,21 @@ class TutorialRender(private val screen: BaseScreen) {
|
||||
}
|
||||
|
||||
private fun showDialog(tutorialName: String, texts: Array<String>, closeAction: () -> Unit) {
|
||||
val text = texts.firstOrNull()
|
||||
if (text == null) {
|
||||
closeAction()
|
||||
} else {
|
||||
val tutorialPopup = Popup(screen)
|
||||
tutorialPopup.name = Constants.tutorialPopupNamePrefix + tutorialName
|
||||
if (texts.isEmpty()) return closeAction()
|
||||
|
||||
if (Gdx.files.internal("ExtraImages/$tutorialName").exists()) {
|
||||
tutorialPopup.add(ImageGetter.getExternalImage(tutorialName)).row()
|
||||
}
|
||||
val tutorialPopup = Popup(screen)
|
||||
tutorialPopup.name = Constants.tutorialPopupNamePrefix + tutorialName
|
||||
|
||||
tutorialPopup.addGoodSizedLabel(texts[0]).row()
|
||||
|
||||
tutorialPopup.addCloseButton(additionalKey = KeyCharAndCode.SPACE) {
|
||||
tutorialPopup.remove()
|
||||
texts.removeIndex(0)
|
||||
showDialog(tutorialName, texts, closeAction)
|
||||
}
|
||||
tutorialPopup.open()
|
||||
if (Gdx.files.internal("ExtraImages/$tutorialName").exists()) {
|
||||
tutorialPopup.add(ImageGetter.getExternalImage(tutorialName)).row()
|
||||
}
|
||||
|
||||
tutorialPopup.addGoodSizedLabel(texts[0]).row()
|
||||
|
||||
tutorialPopup.addCloseButton(additionalKey = KeyCharAndCode.SPACE) {
|
||||
tutorialPopup.remove()
|
||||
showDialog(tutorialName, texts.sliceArray(1 until texts.size), closeAction)
|
||||
}
|
||||
tutorialPopup.open()
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.*
|
||||
import com.badlogic.gdx.scenes.scene2d.utils.Drawable
|
||||
import com.badlogic.gdx.utils.viewport.ExtendViewport
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.models.TutorialTrigger
|
||||
import com.unciv.ui.UncivStage
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.popup.hasOpenPopups
|
||||
@ -67,7 +67,7 @@ abstract class BaseScreen : Screen {
|
||||
keyPressDispatcher.uninstall()
|
||||
}
|
||||
|
||||
fun displayTutorial(tutorial: Tutorial, test: (() -> Boolean)? = null) {
|
||||
fun displayTutorial(tutorial: TutorialTrigger, test: (() -> Boolean)? = null) {
|
||||
if (!game.settings.showTutorials) return
|
||||
if (game.settings.tutorialsShown.contains(tutorial.name)) return
|
||||
if (test != null && !test()) return
|
||||
|
@ -26,7 +26,7 @@ import com.unciv.logic.map.MapVisualization
|
||||
import com.unciv.logic.multiplayer.MultiplayerGameUpdated
|
||||
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
|
||||
import com.unciv.logic.trade.TradeEvaluation
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.models.TutorialTrigger
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.ruleset.tile.ResourceType
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
@ -58,7 +58,6 @@ import com.unciv.ui.trade.DiplomacyScreen
|
||||
import com.unciv.ui.utils.BaseScreen
|
||||
import com.unciv.ui.utils.Fonts
|
||||
import com.unciv.ui.utils.KeyCharAndCode
|
||||
import com.unciv.ui.utils.UncivDateFormat.formatDate
|
||||
import com.unciv.ui.utils.centerX
|
||||
import com.unciv.ui.utils.colorFromRGB
|
||||
import com.unciv.ui.utils.darken
|
||||
@ -80,7 +79,6 @@ import com.unciv.ui.worldscreen.status.StatusButtons
|
||||
import com.unciv.ui.worldscreen.unit.UnitActionsTable
|
||||
import com.unciv.ui.worldscreen.unit.UnitTable
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Unciv's world screen
|
||||
@ -522,9 +520,9 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
|
||||
private fun displayTutorialsOnUpdate() {
|
||||
|
||||
displayTutorial(Tutorial.Introduction)
|
||||
displayTutorial(TutorialTrigger.Introduction)
|
||||
|
||||
displayTutorial(Tutorial.EnemyCityNeedsConqueringWithMeleeUnit) {
|
||||
displayTutorial(TutorialTrigger.EnemyCityNeedsConqueringWithMeleeUnit) {
|
||||
viewingCiv.diplomacy.values.asSequence()
|
||||
.filter { it.diplomaticStatus == DiplomaticStatus.War }
|
||||
.map { it.otherCiv() } // we're now lazily enumerating over CivilizationInfo's we're at war with
|
||||
@ -536,11 +534,11 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
// no matter whether civilian, air or ranged, tell user he needs melee
|
||||
.any { it.getUnits().any { unit -> unit.civInfo == viewingCiv } }
|
||||
}
|
||||
displayTutorial(Tutorial.AfterConquering) { viewingCiv.cities.any { it.hasJustBeenConquered } }
|
||||
displayTutorial(TutorialTrigger.AfterConquering) { viewingCiv.cities.any { it.hasJustBeenConquered } }
|
||||
|
||||
displayTutorial(Tutorial.InjuredUnits) { gameInfo.getCurrentPlayerCivilization().getCivUnits().any { it.health < 100 } }
|
||||
displayTutorial(TutorialTrigger.InjuredUnits) { gameInfo.getCurrentPlayerCivilization().getCivUnits().any { it.health < 100 } }
|
||||
|
||||
displayTutorial(Tutorial.Workers) {
|
||||
displayTutorial(TutorialTrigger.Workers) {
|
||||
gameInfo.getCurrentPlayerCivilization().getCivUnits().any {
|
||||
it.hasUniqueToBuildImprovements && it.isCivilian() && !it.isGreatPerson()
|
||||
}
|
||||
@ -552,7 +550,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
if (!civInfo.isDefeated() && !civInfo.isSpectator() && civInfo.getKnownCivs()
|
||||
.filterNot { it == viewingCiv || it.isBarbarian() }
|
||||
.any()) {
|
||||
displayTutorial(Tutorial.OtherCivEncountered)
|
||||
displayTutorial(TutorialTrigger.OtherCivEncountered)
|
||||
val btn = "Diplomacy".toTextButton()
|
||||
btn.onClick { game.setScreen(DiplomacyScreen(viewingCiv)) }
|
||||
btn.label.setFontSize(30)
|
||||
@ -858,27 +856,27 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
|
||||
private fun showTutorialsOnNextTurn() {
|
||||
if (!game.settings.showTutorials) return
|
||||
displayTutorial(Tutorial.SlowStart)
|
||||
displayTutorial(Tutorial.CityExpansion) { viewingCiv.cities.any { it.expansion.tilesClaimed() > 0 } }
|
||||
displayTutorial(Tutorial.BarbarianEncountered) { viewingCiv.viewableTiles.any { it.getUnits().any { unit -> unit.civInfo.isBarbarian() } } }
|
||||
displayTutorial(Tutorial.RoadsAndRailroads) { viewingCiv.cities.size > 2 }
|
||||
displayTutorial(Tutorial.Happiness) { viewingCiv.getHappiness() < 5 }
|
||||
displayTutorial(Tutorial.Unhappiness) { viewingCiv.getHappiness() < 0 }
|
||||
displayTutorial(Tutorial.GoldenAge) { viewingCiv.goldenAges.isGoldenAge() }
|
||||
displayTutorial(Tutorial.IdleUnits) { gameInfo.turns >= 50 && game.settings.checkForDueUnits }
|
||||
displayTutorial(Tutorial.ContactMe) { gameInfo.turns >= 100 }
|
||||
displayTutorial(TutorialTrigger.SlowStart)
|
||||
displayTutorial(TutorialTrigger.CityExpansion) { viewingCiv.cities.any { it.expansion.tilesClaimed() > 0 } }
|
||||
displayTutorial(TutorialTrigger.BarbarianEncountered) { viewingCiv.viewableTiles.any { it.getUnits().any { unit -> unit.civInfo.isBarbarian() } } }
|
||||
displayTutorial(TutorialTrigger.RoadsAndRailroads) { viewingCiv.cities.size > 2 }
|
||||
displayTutorial(TutorialTrigger.Happiness) { viewingCiv.getHappiness() < 5 }
|
||||
displayTutorial(TutorialTrigger.Unhappiness) { viewingCiv.getHappiness() < 0 }
|
||||
displayTutorial(TutorialTrigger.GoldenAge) { viewingCiv.goldenAges.isGoldenAge() }
|
||||
displayTutorial(TutorialTrigger.IdleUnits) { gameInfo.turns >= 50 && game.settings.checkForDueUnits }
|
||||
displayTutorial(TutorialTrigger.ContactMe) { gameInfo.turns >= 100 }
|
||||
val resources = viewingCiv.detailedCivResources.asSequence().filter { it.origin == "All" } // Avoid full list copy
|
||||
displayTutorial(Tutorial.LuxuryResource) { resources.any { it.resource.resourceType == ResourceType.Luxury } }
|
||||
displayTutorial(Tutorial.StrategicResource) { resources.any { it.resource.resourceType == ResourceType.Strategic } }
|
||||
displayTutorial(Tutorial.EnemyCity) {
|
||||
displayTutorial(TutorialTrigger.LuxuryResource) { resources.any { it.resource.resourceType == ResourceType.Luxury } }
|
||||
displayTutorial(TutorialTrigger.StrategicResource) { resources.any { it.resource.resourceType == ResourceType.Strategic } }
|
||||
displayTutorial(TutorialTrigger.EnemyCity) {
|
||||
viewingCiv.getKnownCivs().asSequence().filter { viewingCiv.isAtWarWith(it) }
|
||||
.flatMap { it.cities.asSequence() }.any { viewingCiv.exploredTiles.contains(it.location) }
|
||||
}
|
||||
displayTutorial(Tutorial.ApolloProgram) { viewingCiv.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts) }
|
||||
displayTutorial(Tutorial.SiegeUnits) { viewingCiv.getCivUnits().any { it.baseUnit.isProbablySiegeUnit() } }
|
||||
displayTutorial(Tutorial.Embarking) { viewingCiv.hasUnique(UniqueType.LandUnitEmbarkation) }
|
||||
displayTutorial(Tutorial.NaturalWonders) { viewingCiv.naturalWonders.size > 0 }
|
||||
displayTutorial(Tutorial.WeLoveTheKingDay) { viewingCiv.cities.any { it.demandedResource != "" } }
|
||||
displayTutorial(TutorialTrigger.ApolloProgram) { viewingCiv.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts) }
|
||||
displayTutorial(TutorialTrigger.SiegeUnits) { viewingCiv.getCivUnits().any { it.baseUnit.isProbablySiegeUnit() } }
|
||||
displayTutorial(TutorialTrigger.Embarking) { viewingCiv.hasUnique(UniqueType.LandUnitEmbarkation) }
|
||||
displayTutorial(TutorialTrigger.NaturalWonders) { viewingCiv.naturalWonders.size > 0 }
|
||||
displayTutorial(TutorialTrigger.WeLoveTheKingDay) { viewingCiv.cities.any { it.demandedResource != "" } }
|
||||
}
|
||||
|
||||
private fun backButtonAndESCHandler() {
|
||||
|
Reference in New Issue
Block a user