Story Narration Screen - UQcsse3200/2026-studio-3 GitHub Wiki
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.
| 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 |
-
victoryanddefeatare written but not wired, pending an end-of-run hook. - A malformed
narration.jsonwould 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.
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).
| 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.
- 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]
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 |
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).
| 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
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.
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
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
| 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 |
- 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.showNarrationto validate it before the current screen is disposed, and again in theNarrationScreenconstructor. The file is small, so this was accepted in exchange for never leaving the game on a disposed screen.
- [The Fall of the Pantheon Story](The-Fall-of-the-Pantheon-Story)
- [Sprint 3 Story Outline](Sprint-3-Story-Outline)
- [Card Discovery and Library Lore](Card-Discovery-and-Library-Lore)
- [Game Screens](Game-Screens)