Files
eagle0/docs/TUTORIAL_SYSTEM.md

23 KiB
Raw Permalink Blame History

Tutorial System Architecture

This document describes how the Unity client's tutorial system is wired together: the classes involved, the trigger catalog, the step lifecycle, and the expected first-session flow. It complements two existing docs:

  • TUTORIAL_CONTENT.md — human-facing copy for tutorial steps and dialogues
  • TUTORIAL_BATTLE_SYSTEM.md — the scripted opening battle at Onmaa

It also overlaps slightly with the in-tree Assets/Tutorial/TUTORIAL_PLAN.md, which is an older implementation plan.

All paths below are relative to src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/.


Current State (important)

There are two parallel subsystems that share the same trigger plumbing:

Subsystem Status Lives in
Step-based tutorial UI (modals, overlays, hints) Dormant Tutorial/TutorialManager.cs, Tutorial/UI/*, Tutorial/Content/*
Narrative dialogue system Live Tutorial/Dialogue/*, Resources/Dialogues/*.json

TutorialContentDefinitions.RegisterAll() returns early with the comment "Old tutorial content suppressed — replaced by narrative dialogue system." As a result:

  • OnboardingSequence is never assigned, so StartOnboarding() no-ops at the "No onboarding sequence assigned" branch.
  • No contextual tutorial sequences are registered with the trigger registry.
  • The trigger registry still fires events normally, but nothing on the step-UI side is listening.
  • DialogueManager consumes those same events and matches them against scripts in Resources/Dialogues/ (tutorial_strategic.json, tutorial_battle.json).
  • Dialogue completion is persisted per game through TutorialDialogueProgressStore, so completed scripts do not replay after a reconnect or scene reload for the same game.

In practice today, the entire active tutorial experience runs through the dialogue system. The step-UI machinery is preserved for future use — content definitions still exist below the early return in TutorialContentDefinitions.cs for reference.


Class Map

Orchestration

  • Tutorial/TutorialManager.cs — Singleton. Owns OnboardingSequence, the active step queue, the trigger registry, and the dialogue manager handle. Initialized by EagleGameController.SetUpGame() and ShardokGameController.SetUpGame().
  • Tutorial/TutorialState.cs — PlayerPrefs persistence. Tracks OnboardingCompleted, OnboardingStepReached, completed sequence IDs (HashSet for O(1) lookup), dismissed hints, and a global "tutorials disabled" preference.
  • Tutorial/TutorialTargetRegistry.cs — Maps string IDs to RectTransforms for highlighting. Static targets are Inspector-assigned (ProvinceInfoPanel, SupportField, CommitButton, etc.); dynamic targets register at runtime (e.g., hero rows from HeroesAndBattalionsPanelController).
  • Tutorial/TutorialDialogueCoordinator.cs — Recomputes durable dialogue triggers from the current strategic model and sends them directly to DialogueManager. This covers reconnects, phase-start tutorial games, and strategic beats that should be recoverable from state.

Triggers

  • Tutorial/Triggers/TutorialTriggerRegistry.cs (~1100 lines) — Routes game events to both the step UI (when sequences are registered) and DialogueManager. Maintains one-shot session flags so the same first-encounter trigger doesn't fire twice.

Content (step UI — dormant)

  • Tutorial/Content/TutorialStep.cs — Per-step record: DisplayMode, CompletionType, target path, copy, panel anchor, highlight options.
  • Tutorial/Content/TutorialSequence.csScriptableObject holding ordered TutorialSteps plus IsOnboarding flag and lifecycle callbacks.
  • Tutorial/Content/TutorialContentDefinitions.cs — Static class that would register all sequences. Currently short-circuits before any registration.

UI (step UI — dormant)

  • Tutorial/UI/TutorialUIManager.cs — Coordinates which presenter renders each step.
  • Tutorial/UI/TutorialCanvasBuilder.cs — Builds the Canvas at runtime (no prefab dependency).
  • Tutorial/UI/TutorialModalPanel.cs — Full-screen blocking modal.
  • Tutorial/UI/TutorialOverlayController.cs + TutorialOverlayBuilder.cs — Dimmer with highlighted cutout, gold border, pulsing animation, and adjacent tooltip text.
  • Tutorial/UI/TutorialHintIndicator.cs — Stub for pulsing-dot mode.

Dialogue (live)

  • Tutorial/Dialogue/DialogueManager.cs — Loads all Resources/Dialogues/*.json scripts at startup, indexes by trigger ID, drives the panel.
  • Tutorial/Dialogue/DialogueScript.cs / DialogueStep.cs — JSON-shaped records for scripts and steps.
  • Tutorial/Dialogue/DialoguePanelController.cs — Renders the speaker headshot, body text, instruction line, optional highlight target, and Continue button.
  • Scripts: Assets/Resources/Dialogues/tutorial_strategic.json, tutorial_battle.json.

Step Lifecycle (Step-UI System)

  1. TriggeredTutorialTriggerRegistry raises an event (e.g., game_started, battle_entered).
  2. Queued or shown — If no active sequence, show immediately. If an active step is hidden (DisplayMode.None), interrupt it. If both new and active are command tutorials, interrupt for responsiveness. Otherwise queue.
  3. RenderedTutorialUIManager.ShowCurrentStep() dispatches to the modal/overlay/hint presenter based on DisplayMode.
  4. Awaiting completion — One of:
    • ButtonClick — user dismisses
    • GameEvent — wait for a named event (e.g., province_selected)
    • Timer — auto-advance after delay
    • UIInteraction — wait for the highlighted target to be interacted with
    • Condition — custom predicate (rare)
  5. AdvanceAdvanceStep() moves to the next step or completes the sequence; completion writes to TutorialState.

Hidden steps (DisplayMode.None) deliberately don't block — they exist so contextual tutorials can fire while the onboarding sequence is waiting on an async event.


Display Modes

Mode Renderer Notes
Modal TutorialModalPanel Full blocking dialog with dim background
Overlay TutorialOverlayController Dimmer with cutout + tooltip near a target
Tooltip TutorialOverlayController Currently same renderer as overlay
Hint TutorialHintIndicator Pulsing dot only (stub)
None (invisible) Waits for a completion event

Panels can be anchored via TutorialPanelAnchor (Center / Left / Right / Top / Bottom).


Trigger Catalog

Most event-style triggers are raised via TutorialTriggerRegistry. Durable strategic dialogue beats are also recomputed by TutorialDialogueCoordinator on each model update and sent directly to DialogueManager.TriggerDialogue(). Both paths match against the dialogue JSON; if step-UI sequences are re-registered, only the registry path will route to them. File:line citations are for the registry unless noted; line numbers may drift.

Bootstrap / first-session

Trigger Fires from When
game_started TutorialDialogueCoordinator.EligibleTriggers() Any tutorial strategic model update; dialogue progress keeps it from replaying
first_battle_available OnModelUpdated() ~L204 A RunningShardokGameModel first appears
battle_entered OnBattleEntered() ~L793 User clicks Fight!
tutorial_battle_ended CheckTutorialBattleEnded() ~L594 Tutorial battle removed from running models
tutorial_rebuild_support CheckTutorialRebuildSupport() ~L616 Captured-heroes phase done
tutorial_taxes_collected CheckTaxesCollected() ~L680 New Year action with positive tax delta
tutorial_in_town CheckInTown() ~L500 Return command becomes available

Strategic-map contextual

Trigger Site When
province_selected EagleGameController.ProvinceWasSelected() Province click
command_issued EagleGameController.PostCommittedCommand() User commits
diplomacy_available CheckStrategicCommandsAvailable() ~L243 Diplomacy command appears
weather_control_available CheckStrategicCommandsAvailable() ~L249 Weather command appears
hero_recruitment_available CheckHeroRecruitmentAvailable() ~L266 Free heroes detected
tutorial_ready_to_join CheckReadyToJoinHero() ~L479 Free hero with WouldJoin
profession_<X>_encountered CheckProfessionTutorial() ~L726 First time seeing each profession

Strategic guidance (state-derived, conditions checked each model update)

Trigger Site Condition
guidance_loyalty_danger CheckLoyaltyDanger() ~L339 November + hero loyalty < 70
guidance_neighbor_danger CheckNeighborDanger() ~L383 Hostile faction adjacent
guidance_recruit_heroes CheckRecruitHeroesGuidance() ~L420 Province with support ≥40 has free heroes
guidance_expand CheckExpandGuidance() ~L449 One stable province, hasn't expanded
guidance_sworn_kinship CheckSwornKinshipGuidance() ~L549 Good candidate or 4+ provinces
tutorial_loyalty_warning CheckTutorialLoyaltyWarning() ~L634 November + hero loyalty < 70
tutorial_support_deadline CheckTutorialSupportDeadline() ~L654 December + province support < 40

Strategic action-result triggers (fired from OnStrategicActionResult())

Trigger Site When
hero_stat_gained ~L281 HeroStatGained action result
hero_profession_gained ~L284 ProfessionGained action result
tutorial_faction_appears ~L287 TutorialFactionAppears action — the Fracture Covenant landing
tutorial_hero_faction_appears ~L290 TutorialHeroFactionAppears action; has a four-step strategic dialogue today
tutorial_hero_departed ~L299 First HeroesDeparted action against another player (King's hero abandons service)
tutorial_hero_departed_again ~L302 Second HeroesDeparted in a different month than the first

Tactical (battle) — abilities and spells

Combat triggers are paced: _combatTutorialPending ensures only one fires per action, with priority archery > duel > melee > charge > fire.

Trigger Site When
shardok_placement_started OnBattleEntered() ~L796 Battle in setup phase
shardok_battle_started OnBattleEntered() ~L793 Battle begins
shardok_battle_running OnTacticalCommandsAvailable() ~L948 Player's first turn
archery_available OnTacticalCommandsAvailable() ~L990 Archery available
melee_available OnTacticalCommandsAvailable() ~L991 Melee available
duel_available OnTacticalCommandsAvailable() ~L1005 Duel available (non-Tarn target)
ability_charge_available OnTacticalCommandsAvailable() ~L1001 Charge for Old Marek
start_fire_available OnTacticalCommandsAvailable() ~L1021 Start Fire on enemy
hide_available OnTacticalCommandsAvailable() ~L985 Hedrick can hide
thunderstorm OnTacticalCommandsAvailable() ~L1056 Weather is thunderstorm
spell_lightning_available OnTacticalCommandsAvailable() ~L960 Lightning available
spell_meteor_available OnTacticalCommandsAvailable() ~L963 Meteor available
spell_holywave_available OnTacticalCommandsAvailable() ~L965 Holy Wave available
spell_raisedead_available OnTacticalCommandsAvailable() ~L969 Raise Dead available
spell_lightning_cast / spell_meteor_cast / spell_holywave_cast / spell_raisedead_cast CheckTacticalActionType() ~L830-838 Spell action observed
ability_charge_used CheckTacticalActionType() ~L842 Charge attack executed
terrain_fire_encountered CheckTacticalActionType() ~L864 Fire damage / spread
terrain_water_encountered CheckTacticalActionType() ~L867 Water crossing
engineer_near_enemy CheckEngineerNearEnemy() ~L1105 John Ranil within 3 hexes of enemy
tutorial_reinforcement_engineer / tutorial_reinforcement_paladin CheckTacticalActionType() ~L857 Reinforcement arrival
friendly_hero_captured CheckHeroRemovals() ~L919 Friendly hero unit removed
enemy_hero_captured CheckHeroRemovals() ~L928 Enemy hero unit removed
battle_action ShardokGameController.OnBattleAction() Any action result
turn_ended ShardokGameController.OnTurnEnded() ~L1137 Battle turn ends
shardok_battle_reset ShardokGameController.cs:627 (fired directly to DialogueManager, bypassing the registry) Battle retry; the registry's battle flags and the battle dialogue scripts' completed-set are reset alongside, so per-battle tutorials re-fire

One-shot semantics

Most contextual triggers are guarded by booleans on the registry (e.g., a _diplomacyShown flag). These are session-local — they don't persist via TutorialState, but the registry has ResetBattleTriggerFlags() for replays. The strategic completion list does persist across sessions.


Highlight Targets

Steps reference UI by string ID through TutorialTargetRegistry:

  1. Static targets are wired in the Inspector on the registry.
  2. Dynamic targets register at runtime (RegisterTarget(id, rectTransform)).
  3. Lookup falls back to GameObject.Find() if not registered.

Step options:

  • TargetGameObjectPath — primary highlight
  • AdditionalHighlightTargets[] — multiple at once
  • HighlightBoundsFromChildren — use children's bounding box
  • HighlightPulsing — strobe animation

Dialogue scripts use the same registry via highlightTarget (and persistHighlight: true to keep the highlight after the dialogue closes).


Sequences vs. Contextual Dispatch

  • Onboarding sequence (IsOnboarding = true) — Single linear sequence, started by StartOnboarding(). Resumes from OnboardingStepReached. Counts only visible steps for progress.
  • Contextual (IsOnboarding = false) — Single- or multi-step, dispatched on demand by TriggerContextualTutorial().
  • Dispatch rules in TriggerContextualTutorial():
    1. No active sequence → show immediately.
    2. Active step is hidden (None) → interrupt.
    3. Both are command tutorials → interrupt.
    4. Otherwise → queue.

Dialogue System (Currently Live)

DialogueManager is a parallel system, not a step-UI subclass. It loads every TextAsset in Resources/Dialogues/ at startup, parses them as DialogueScript objects, and indexes by trigger.

Script shape (see tutorial_strategic.json):

{
  "scripts": [{
    "id": "tutorial_opening",
    "trigger": "game_started",
    "panelPosition": null,
    "steps": [{
      "speakerName": "Old Marek the Learned",
      "speakerImagePath": "fixed/old_marek_the_learned.png",
      "dialogueText": "...",
      "instructionText": "Click <b>Fight!</b> to enter tactical combat.",
      "highlightTarget": "FightButton",
      "persistHighlight": true,
      "highlightProvince": "Onmaa",
      "completionEvent": null
    }]
  }]
}

When a registry trigger fires, OnGameEvent() calls DialogueManager.TriggerDialogue(triggerId). If a script matches and hasn't completed, it queues (or shows immediately) and the panel renders speaker headshot + body + instruction. Steps can pause for a completionEvent (e.g., wait for the player to click the highlighted button), allowing the dialogue to chain across game state changes.

Dialogue panel position defaults sensibly per scene (top during combat) and can be overridden per script.


Persistence

TutorialState (PlayerPrefs JSON):

  • OnboardingCompleted (bool)
  • OnboardingStepReached (int)
  • CompletedTutorials (List → HashSet at load)
  • DismissedHints (List → HashSet at load)
  • AllTutorialsDisabled (bool)

Reset paths:

  • TutorialState.Reset() clears everything.
  • Entering a tutorial game (GameType.Tutorial) resets TutorialState; dialogue replay is controlled separately by the per-game dialogue progress key.
  • Settings → "Reset Tutorials" calls TutorialManager.ResetAllProgress(), which also calls DialogueManager.ResetCompletedScripts().

TutorialDialogueProgressStore separately stores completed dialogue script IDs per game under PlayerPrefs keys named Eagle0_TutorialDialogueProgress_<gameId>.

DialogueManager keeps the active game's completed script IDs in memory and saves them through TutorialDialogueProgressStore. ResetCompletedScripts() clears the current game's dialogue progress; ClearCurrentGameProgress() deletes the current game's persisted dialogue key.


Bootstrap

  1. TutorialManager is in the scene as a singleton; Awake loads state and creates the trigger registry. RegisterAll() is called here but currently no-ops.
  2. ConnectionHandler sets IsTutorialGame and IsMultiplayerGame when the game is created/joined.
  3. EagleGameController.SetUpGame() calls TutorialManager.Instance.Initialize(this, null). If IsTutorialGame && !OnboardingCompleted, it would call StartOnboarding() — currently a no-op because OnboardingSequence is null.
  4. The first model update raises game_started. DialogueManager matches tutorial_opening from tutorial_strategic.json and the dialogue runs.
  5. ShardokGameController.SetUpGame() calls TutorialManager.Instance.Initialize(null, this) when battle starts; raises battle_entered and battle-only triggers from there.

Expected Tutorial Flow (today)

Driven by tutorial_strategic.json + tutorial_battle.json. Old Marek the Learned narrates almost everything.

Narrative arc

You play Sadar Rakon, formerly Ikhaan Tarn's most trusted lieutenant. Tarn's behavior grew erratic, the King wouldn't listen, so Sadar broke off and raised the Reclamation. He's been pushed back to Onmaa for a last stand against Tarn's superior royal army. After surviving, the real twist arrives: a new invasion — The Fracture Covenant, led by a mysterious figure called The Eagle — lands ships on the western coast and starts taking provinces. Heroes from the King's own service mysteriously begin to desert. The Reclamation's framing pivots from "rebellion" to "the only force that can stop the actual invasion."

Strategic-map flow (tutorial_strategic.json)

# Trigger What happens
1 game_started Opening monologue (3 steps): Sadar's backstory, the Reclamation, last stand at Onmaa. Ends highlighting the Fight! button (FightButton, persistHighlight)
2 tutorial_battle_ended Aftermath: Tarn has vanished; you've captured his lieutenants. Recruit them or hold them
3 tutorial_rebuild_support Rebuild Onmaa (3 steps): Marek frames it; John Ranil introduces Improve (engineer bonus); Elena Fyar introduces Give Alms (paladin bonus). Goal: 40 support by January
4 tutorial_loyalty_warning November-ish, low-loyalty hero may leave at year end → use Give Gold / Feast
5 tutorial_support_deadline December nag if support < 40: have Elena give alms now
6 guidance_expand (script tutorial_expansion) Once support is stable: keep developing, taxes coming in January
7 tutorial_taxes_collected January: gold/food collected. Now expand — March your warlord + most heroes to a neighbor, leave 12 behind
8 tutorial_ready_to_join A free hero in {provinceName} is ready to recruit → Visit Town + Recruit
9 tutorial_in_town When you Visit Town: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; Return ends the turn
10 tutorial_faction_appears The twist: Fracture Covenant lands at Ingia and Soria. The Eagle commands them. Tarn may be with them
11 tutorial_hero_departed First King's hero abandons service — "something deeper is at work"
12 tutorial_hero_departed_again Second one in a different month — "something is wrong, I can feel it" (foreshadowing)
13 tutorial_hero_faction_appears John Ranil and Elena Fyar leave Sadar's service and reappear as independent tutorial factions

Tactical battle flow (tutorial_battle.json)

# Trigger What happens
1 shardok_placement_started Placement (3 steps): identify Tarn's units (knights, heavy infantry, dragoons), color legend (red/blue/black), placement instructions, highlight Commit
2 shardok_battle_running Turn 1 strategy (2 steps): victory conditions for both sides, advice to hold castles and End Turn
3 archery_available First archery opportunity → purple outlines, longbows vs. armor
4 melee_available First melee opportunity → right-click adjacent enemy
5 ability_charge_available Charge mechanics (Marek hams it up: "old scholar without a horse")
6 start_fire_available Start Fire on enemy hex, fire spread mechanics
7 thunderstorm Weather: archery disabled, fires extinguished
8 tutorial_reinforcement_paladin Elena Fyar arrives mid-battle (2-step exchange) → Paladin profession intro (Holy Wave)
9 tutorial_reinforcement_engineer John Ranil arrives (2-step exchange) → Engineer profession intro
10 engineer_near_enemy Once Ranil is close to enemy: Fortify + Reduce (siege bombardment)
11 duel_available Champion duel mechanics (Marek warns not to duel Tarn himself)
12 hide_available Hedrick the Hedge-merchant: forest/swamp Hide + ambush
13 enemy_hero_captured / friendly_hero_captured Capture mechanics, with {heroName} substitution
14 shardok_battle_reset If you lose and replay: Marek "had the strangest sensation… as though we'd already fought this battle, and lost" — fourth-wall-adjacent retry framing

Most remaining "Tutorial / first-session onboarding" items in SMALL_EAGLE_TODO.md (Shardok tutorial, narrative hook, first-session goal, early small victory, guided first scenario vs. sandbox) map to adding new dialogue scripts against existing trigger IDs, or — if the step-UI is revived — to populating TutorialContentDefinitions.RegisterAll() and removing the early return.


Notable Behaviors and Gotchas

  • IsTutorialGame gates everything. Outside tutorial games, both subsystems short-circuit early.
  • Combat tutorials are deliberately paced. Don't expect every available-ability trigger to fire on its first eligible turn — the registry intentionally spaces them.
  • One-shot flags are session-local. A trigger that "already fired" will not re-fire even if TutorialState is reset, until the registry is recreated.
  • TutorialTargetRegistry lookups silently fall back to GameObject.Find. Typos in target IDs will render with no highlight rather than throwing.
  • TutorialContentDefinitions.RegisterAll() returning early is load-bearing. If the step UI is revived without auditing trigger overlap, dialogue and step-UI tutorials may double-fire on the same trigger.
  • Tutorial/TUTORIAL_PLAN.md in the Unity project is an older implementation plan and partially overlaps this doc.