Moddable prettier Tutorials - Step 1 (#7064)

This commit is contained in:
SomeTroglodyte
2022-06-06 08:32:23 +02:00
committed by GitHub
parent 6c990b67e2
commit e91c0ff212
12 changed files with 338 additions and 184 deletions

View File

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

View File

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

View 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"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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