Story Narration Screen - UQcsse3200/2026-studio-3 GitHub Wiki

Story Narration Screen

Team 6, Sprint 3 | Story crawls before key moments of a run | Issue #342 | PRs #349, #353 | Branch: feature/sprint3-cards-and-story

Short, skippable story crawls give the player a reason to be where they are: one when a new run starts and one before the boss battle. The text follows the approved story in The Fall of the Pantheon Story.

Where Crawls Play

Sequence When it plays Then continues to
opening Main menu → New Game, after the previous run is reset Map
pre_boss Selecting the FINAL node on the map Boss battle
victory After winning the final boss encounter; the run is ended Main menu
defeat After losing a battle in an active run Main menu

Known Limitations

  • victory and defeat are written but not wired, pending an end-of-run hook.
  • A malformed narration.json would throw when New Game is pressed. The production file is guarded by the loader test above.
  • The crawl currently uses a plain background; story backgrounds are a separate art task.

Related Pages

# Story Narration Screen

Team 6, Sprint 3 | Story crawls before key moments of a run | Issue [#342](https://github.com/UQcsse3200/2026-studio-3/issues/342) | PRs [#349](https://github.com/UQcsse3200/2026-studio-3/pull/349), [#353](https://github.com/UQcsse3200/2026-studio-3/pull/353) | Branch: feature/sprint3-cards-and-story

Short, skippable story crawls give the player a reason to be where they are: one when a new run starts and one before the boss battle. The text follows the approved story in [The Fall of the Pantheon Story](The-Fall-of-the-Pantheon-Story).

Where Crawls Play

Sequence When it plays Then continues to
opening Main menu → New Game, after the previous run is reset Map
pre_boss Selecting the FINAL node on the map Boss battle
victory Not wired yet: defeating the boss returns to the map, so there is no end-of-run moment to hook into —
defeat Not wired yet —

Crawls never play when loading a save. Ordinary COMBAT and ELITE nodes go straight to battle.

pre_boss plays every time the boss node is entered, including after abandoning the battle through the pause menu or restoring a save made mid-battle. It can always be skipped.

Player Behaviour

  • Each passage fades in (0.6 s), holds (2.5 s) and fades out (0.6 s), then the next passage starts.
  • Any key or mouse click skips the whole sequence: the current passage fades out and the game continues.
  • When the last passage finishes, or after skipping, the game moves to the destination screen exactly once.

[Screenshot: opening crawl]

[Screenshot: pre_boss crawl]

Content: configs/narration.json

Each top-level key is a sequence ID. A sequence is a list of passages, and a passage is a list of one to three lines shown together.

{
  "opening": [
    ["Heaven sent a lesser angel", "to walk among the fallen gods."],
    ["The pantheon was appointed to keep the world in order.", "A villain corrupted them, and still they do not know."],
    ["They keep their duties. Their servants keep their posts.", "You were sent not to destroy them, but to set them free."]
  ],
  "pre_boss": [ ... ],
  "victory": [ ... ],
  "defeat": [ ... ]
}

Rules enforced by NarrationConfigLoader:

Rule Result if broken
File exists and is valid JSON with an object at the root NarrationLoadingException
Sequence IDs are non-blank and unique; each sequence is an array NarrationLoadingException
Every passage has one to three lines NarrationLoadingException
Every line is a non-blank string NarrationLoadingException
Requested sequence ID does not exist Not an error: empty list, and the screen completes on its first frame

Editing the text

Edit narration.json directly; no code or test changes are needed. The screen tests compare what is displayed against the loaded config, not hardcoded strings. NarrationConfigLoaderTest.productionConfigDefinesAllStorySequences checks that all four sequences exist and are non-empty, so a broken production file fails the build rather than the game.

Narration text: Ziqin (Member 4). Screen and wiring: Zhenyu He (Member 1).

Implementation

Class Role
NarrationConfigLoader Loads and validates narration.json; returns immutable passages for a sequence ID.
NarrationScreen Plays one sequence with its own stage, renderer and input; calls a completion callback once. Uses the shared UI skin and loads no extra textures.
GdxGame.showNarration(sequenceId, next) Disposes the current screen and re-registers process-level services (the same transition as setScreen(ScreenType)), then shows NarrationScreen; on completion calls setScreen(next).
MainMenuActions New Game: endRun() then showNarration("opening", MAP).
MapScreen.enterEncounter FINAL: showNarration("pre_boss", BATTLE_SCREEN); other battle rooms unchanged.
flowchart LR
    Menu[Main menu: New Game] --> Reset[RunState.endRun]
    Reset --> Opening[NarrationScreen opening]
    Opening --> Map[Map]
    Map -->|FINAL node| PreBoss[NarrationScreen pre_boss]
    PreBoss --> Boss[Boss battle]
    Map -->|COMBAT / ELITE| Battle[Battle]
    Load[Main menu: Load] --> Map
Loading

NarrationScreen is not a ScreenType because it needs a sequence ID and a destination. Do not construct it directly from another screen's input handler: its constructor replaces global services and the input processor, so it must go through showNarration, which disposes the old screen first. The completion callback runs outside stage and input dispatch, so it is safe for it to dispose the narration screen.

Design Diagrams

Where each narration sequence plays in the game flow. Loading a save never plays narration. If narration.json is invalid, the error is logged and the game goes straight to the next screen.

flowchart TD
    Menu[Main menu]
    NewGame[New Game]
    LoadGame[Load Game]
    Opening[Opening narration]
    SaveLoad[Save and Load screen]
    Restore[Restore save with no narration]
    Map[Map]
    Final[FINAL node]
    OtherBattle[COMBAT or ELITE node]
    PreBoss[Pre boss narration]
    Battle[Battle]
    Win[Battle win]
    Loss[Battle loss]
    EndBattle[End battle screen]
    RewardGate[Reward claimed]
    FinalCheck{Final encounter completed}
    ActiveWin{Run still active}
    EliteCheck{Elite temple reward pending}
    ElitePortal[Elite portal]
    ActiveLoss{Run was active}
    Victory[Victory narration]
    Defeat[Defeat narration]

    Menu --> NewGame
    NewGame --> Opening
    Opening --> Map

    Menu --> LoadGame
    LoadGame --> SaveLoad
    SaveLoad --> Restore
    Restore -->|"Back with active run"| Map
    Restore -->|"Back without active run"| Menu

    Map --> Final
    Final --> PreBoss
    PreBoss --> Battle
    Map --> OtherBattle
    OtherBattle --> Battle

    Battle --> Win
    Battle --> Loss
    Win --> EndBattle
    Loss --> EndBattle

    EndBattle -->|"win"| RewardGate
    RewardGate --> FinalCheck
    FinalCheck -->|yes| Victory
    Victory --> Menu
    FinalCheck -->|no| ActiveWin
    ActiveWin -->|yes| EliteCheck
    EliteCheck -->|yes| ElitePortal
    EliteCheck -->|no| Map
    ActiveWin -->|no| Menu

    EndBattle -->|"loss"| ActiveLoss
    ActiveLoss -->|yes| Defeat
    Defeat --> Menu
    ActiveLoss -->|no| Menu
Loading

The main classes involved in loading, showing and routing narration.

classDiagram
    class NarrationConfigLoader {
        +String DEFAULT_FILE
        +loadSequence(String sequenceId) List~List~String~~
        +loadSequence(String filename, String sequenceId) List~List~String~~
    }
    class NarrationLoadingException
    class NarrationScreen {
        ~float FADE_SECONDS
        ~float HOLD_SECONDS
        -Runnable onComplete
        -boolean completed
        +NarrationScreen(String sequenceId, Runnable onComplete)
        -skip() void
        +render(float delta) void
        +dispose() void
    }
    class GdxGame {
        -RunState runState
        -CardDiscoveryService cardDiscoveryService
        +setScreen(ScreenType screenType) void
        +showNarration(String sequenceId, ScreenType next) void
        -prepareScreenTransition(ScreenType screenType) void
    }
    class RunState {
        +isRunActive() boolean
        +isFinalEncounterCompleted() boolean
        +hasPendingEliteTempleReward() boolean
        +endRun() void
    }
    class EndBattleScreen {
        -boolean won
        -boolean returning
        -boolean rewardClaimed
        -returnToMenu() void
        -onRewardClaimed() void
    }
    NarrationConfigLoader ..> NarrationLoadingException : throws
    GdxGame --> NarrationConfigLoader : validates before dispose
    GdxGame --> NarrationScreen : creates
    GdxGame *-- RunState : owns
    NarrationScreen --> NarrationConfigLoader : loads with
    EndBattleScreen --> GdxGame : showNarration
    EndBattleScreen --> RunState : reads
Loading

Test Coverage

Test Covers
NarrationConfigLoaderTest Production file defines all four sequences; passage order and immutability; unknown ID returns empty; missing, malformed and invalid files throw.
NarrationScreenTest Fade/hold/advance timing; completes exactly once; any key or click skips; repeated input does not restart the fade; disposal cancels completion; unknown ID completes on first render; the callback can safely dispose the screen.
MainMenuActionsTest New Game calls endRun() then showNarration("opening", MAP) and never setScreen(MAP) directly; Load plays no narration.

MapScreen has no unit-test harness (it needs a renderer, UI and textures), so its routing was verified in game:

Check Result
New Game shows opening, then the map Pass
A combat node goes straight to battle with no crawl Pass
The boss node shows pre_boss, then the boss battle Pass
Loading a save returns to the map with no crawl Pass

Known Limitations

  • Victory and defeat narration are wired into EndBattleScreen through PR #367. Final-boss victory ends the run before playing the victory sequence. Defeat narration plays after a loss in an active run.
  • GdxGame.showNarration validates the narration configuration before disposing the current screen. If loading fails, the error is logged and the game continues to the destination screen. The production file is guarded by the loader test above.
  • The crawl currently uses a plain background; story backgrounds are a separate art task.
  • The narration file is read twice per sequence: once in GdxGame.showNarration to validate it before the current screen is disposed, and again in the NarrationScreen constructor. The file is small, so this was accepted in exchange for never leaving the game on a disposed screen.

Related Pages

⚠️ **GitHub.com Fallback** ⚠️