Cutscenes and Dialogue Test Plan - UQcsse3200/2026-studio-4 GitHub Wiki

Overview

This test plan covers the dialogue and video cutscene systems (Cutscenes and Dialogue):

  • The dialogue flow โ€” DialogueRunner (typewriter reveal, line advance, skip) and the DialogueScript data it plays.
  • Cutscene scripts and the mp4 player โ€” CutsceneScript and Mp4VideoPlayer (pure-Java JCodec decoding).
  • The NarrativeManagerComponent, which answers the startDialogue / startCutscene events, freezes the world, locks the player's controls, handles once-only and busy rules, fires timed events and always sends the matching *Finished event.
  • Input capture โ€” NarrativeInputComponent (advance, ESC, swallowed gameplay keys, and handing input to the debug terminal while it is open).
  • The world freeze in EntityService (setFrozen), which is separate from the inventory's toggleUpdate pause.
  • Script loading from configs/dialogues and configs/cutscenes, including that every dialogue and cutscene used by a Interactable NPC has a file.
  • The two debug-terminal entry points, dialogue and cutscene.

A dialogue and a cutscene are deliberately different things. A dialogue is UI on top of the running scene; a cutscene takes over the whole screen. Both freeze the world and lock the player, each under its own owner so one cannot release the other's freeze. They are tested together here because the manager runs both.

Testing is split into automated unit tests (what the code must do) and manual gameplay tests (what the player must see). Every behaviour in the automated plan is enforced by a test in the repository โ€” the test class is named above each table.

Test Environment

  • Unit testing: JUnit 5 and Mockito, run headlessly with the engine's GameExtension. OpenGL is mocked, so the manager is tested against fake DialogueView and CutsceneView implementations and the clock is driven by hand (update(float)) rather than by sleeping. Mp4VideoPlayerTest decodes the real demo video.
  • Game testing: Desktop application.
  • Movement: WASD โ€” Interact: E โ€” Advance dialogue: SPACE / ENTER / left click โ€” Leave dialogue / skip cutscene: ESC โ€” Debug terminal: F1
  • Terminal commands:
dialogue demo           # play configs/dialogues/demo.json
cutscene demovideo      # play configs/cutscenes/demovideo.json (assets/videos/demo.mp4)

File names may be given with or without .json and may contain letters, digits, _, - and .. The terminal stays open if the file is not found.

  • Shipped content: demo dialogue (three speakers: Wandering Spirit, You, Travelling Merchant), merchant_greeting and spirit_greeting dialogues, and demovideo plus the three NPC cutscenes (merchant_opens_shop, spirit_reveals_dungeons, guardian_awakens), which currently all play the demo video as a placeholder.
  • Tuned values as shipped: the typewriter reveals 45 characters per second; the dialogue box is 82% of the screen width (max 1100 px) along the bottom; a video that produces no frame within 5 s is abandoned.

Run the automated tests with:

cd source
./gradlew :core:test

Coverage is written to source/core/build/reports/jacoco/test/html/index.html by:

cd source
./gradlew :core:jacocoTestReport

Coverage

Last verified run: 8 Oct 2026 on branch chaubenn/cutscene-dialogue-ui (525cc0d, PR #231) โ€” 2002 tests, 0 failures project-wide, of which 59 tests across 7 classes cover dialogue and cutscenes.

Quality Gate Coverage

Reference: SonarCloud project overview ยท SonarCloud analysis of PR #231 (0 open issues at the time of writing)

The badges above are project-wide. The figures below are the cutscene and dialogue code only, from a local JaCoCo run of the revision above (this feature was not yet on main, so SonarCloud's per-file figures are not used).

Per-file coverage

File Lines Line coverage Branch coverage Method coverage
components/cutscene/NarrativeManagerComponent.java 156 89.1% 70.7% 90.6%
components/cutscene/DialogueRunner.java 47 93.6% 65.6% 100.0%
components/cutscene/DialogueScript.java 39 100.0% 80.0% 100.0%
components/cutscene/CutsceneScript.java 26 96.2% 75.0% 85.7%
components/cutscene/CutsceneEvents.java 10 40.0% โ€” 40.0%
components/cutscene/ScriptNames.java 8 75.0% 85.7% 75.0%
components/cutscene/NarrativeInputComponent.java 22 95.5% 100.0% 83.3%
components/cutscene/Mp4VideoPlayer.java 91 93.4% 77.5% 100.0%
ui/terminal/commands/DialogueCommand.java 13 100.0% 80.0% 100.0%
ui/terminal/commands/CutsceneCommand.java 13 100.0% 87.5% 100.0%
components/cutscene/DialogueDisplay.java 163 0.0% 0.0% 0.0%
components/cutscene/CutscenePlayer.java 91 0.0% 0.0% 0.0%
components/cutscene/TypewriterText.java 52 0.0% 0.0% 0.0%
components/cutscene/PortraitActor.java 24 0.0% 0.0% 0.0%
entities/factories/NarrativeFactory.java 12 0.0% โ€” 0.0%
EntityService.java (new: setFrozen, isFrozen, shouldUpdate) 11 100.0% 91.7% 100.0%

Aggregate across the 10 logic classes (everything above the four UI classes and the factory; JaCoCo, source/core/build/reports/jacoco/test/html/index.html):

Counter Covered Missed Coverage
Instructions 1650 123 93.1%
Branches 174 55 76.0%
Lines 389 36 91.5%
Methods 91 9 91.0%
Classes 15 0 100.0%

Aggregate across all 15 classes, including the four classes that draw with OpenGL and NarrativeFactory:

Counter Covered Missed Coverage
Instructions 1650 1621 50.4%
Branches 174 201 46.4%
Lines 389 378 50.7%
Methods 91 58 61.1%
Classes 15 7 68.2%

The logic is at about 91% line coverage; the overall figure is lower only because DialogueDisplay, CutscenePlayer, TypewriterText, PortraitActor and NarrativeFactory need a real OpenGL context and are covered by the manual plan below. The uncovered lines in the logic classes are listed under Known coverage gaps.

Tests per class

Test class Tests
NarrativeManagerComponentTest 23
NarrativeInputComponentTest 9
DialogueRunnerTest 7
ScriptLoadingTest 6
EntityServiceFreezeTest 5
NarrativeCommandsTest 5
Mp4VideoPlayerTest 4
Total 59

Automated Unit Test Plan

Dialogue

DialogueRunnerTest

The runner holds no UI or libGDX state, so the whole line flow โ€” typewriter, finish-the-line, next line, finish โ€” is tested directly.

Test Purpose Expected Result
startShowsFirstLineWithNothingRevealed Start a two-line dialogue. Line 0 is announced to the listener with no characters revealed yet, and its speaker resolves to the named speaker.
textIsTypedOutOverTime Step time by exactly two characters' worth, then a full second. Two characters show first, then the whole line; the line then reads as complete.
advanceFirstCompletesTheLineThenMovesOn Press advance mid-line, then again. The first press reveals the rest of the line without moving on; the second moves to line 1.
advancingPastTheLastLineFinishesOnce Advance through every line and past the end. onFinished fires exactly once, and further advances do nothing.
skipAllFinishesImmediately Skip from the first line. Only the first line was ever shown, and the dialogue finishes at once โ€” this is what ESC uses.
emptyScriptFinishesOnStart Start a script with no lines. It finishes immediately without showing anything.
unknownSpeakerAndMissingTextAreTolerated Show a line whose speaker is not defined and whose text is null. No speaker, empty text, and the line reads as already complete โ€” no exception.

Cutscenes and the Narrative Manager

NarrativeManagerComponentTest

The manager owns the freeze, the control lock, the busy and once-only rules and the events. These tests run it against fake views and a real EntityService and PlayerActions, and hold the "always release, always report finished" guarantee in place.

Test Purpose Expected Result
dialogueFreezesWorldLocksControlsAndReleasesWhenFinished Play a dialogue, then finish it. While open the world is frozen, controls are locked and dialogueStarted has fired; on finish both are released, the view is closed and dialogueFinished fires.
lineShownEventsAndCuesAreForwarded Show two lines, the second flagged "cue": true. dialogueLineShown fires per line with its index, and dialogueCutsceneCue fires only for the flagged line.
startDialogueEventPlaysTheDialogueAndMissingFilesStillFinish Send startDialogue for an id with no file. Nothing is shown, but dialogueFinished is still sent so a waiting NPC is released; the world is never frozen.
scriptEventPlaysAScriptBuiltInCode Send startDialogueScript with a script built in code. The view is shown that exact script.
secondDialogueIsIgnoredWhileOneIsOpen Request a second dialogue while one is open. Ignored: the first stays on screen and no events are sent.
emptyDialogueFinishesWithoutFreezing Play a script with no lines. dialogueFinished is sent straight away and the game never freezes.
onceOnlyDialoguePlaysOncePerSession Play a once dialogue twice, then reset. The second play sends dialogueFinished without showing it or freezing; after resetPlayed() it plays again.
aFollowUpCanStartFromInsideTheFinishedEvent Start a cutscene from a dialogueFinished listener. The cutscene starts and the world is frozen again โ€” state is cleared before the event, which is how an NPC chains dialogue into a cutscene.
leavingADialogueEndsItEarlyAndReleasesTheGame Leave (ESC) an open dialogue. dialogueSkipped then dialogueFinished, the freeze and lock are released, and the dialogue is no longer active.
aSkippedDialogueCanBePlayedAgainFromTheStart Skip a dialogue, then play it again. It starts again from the beginning.
leaveClosesTheDialogueBeforeTheCutscene Leave while a dialogue and a cutscene are both open. The first leave closes only the dialogue; the second skips the cutscene.
leaveWithNothingPlayingDoesNothing Leave with nothing open. Returns false and sends no events.
advanceGoesToTheDialogue Advance while a dialogue is open. The dialogue view is asked to advance.
cutsceneFreezesWorldLocksControlsAndReleasesWhenFinished Play a cutscene, then finish it. Frozen and locked while playing; released on finish with cutsceneStarted then cutsceneFinished.
cutsceneThatFinishesImmediatelyDoesNotLeaveTheGameFrozen Use a view that reports finished from inside play() (e.g. a video that cannot be decoded). The freeze and lock are released and no cutscene is left active.
secondCutsceneIsIgnoredWhileOneIsPlaying Request a second cutscene mid-play. Ignored; the first keeps playing.
onceOnlyCutscenePlaysOncePerSession Play a once cutscene twice. The second sends cutsceneFinished immediately and plays nothing.
skippableCutsceneCanBeSkipped Advance during a skippable cutscene. cutsceneSkipped then cutsceneFinished, and the game is released.
nonSkippableCutsceneSwallowsTheKeyButKeepsPlaying Advance during a cutscene with skippable: false. The key is consumed but the cutscene keeps playing.
timedEventsFireInOrderAtTheirTimes Schedule events at 4 s and 1 s, then step time. They fire in time order at their times, with their argument (or none), and never twice.
timedEventsStopFiringOnceTheCutsceneEnds Finish a cutscene before an event's time. The event never fires.
dialogueAndCutsceneReleaseTheirOwnFreezeIndependently Open both, finish the cutscene, then the dialogue. The game stays frozen and locked until the second one ends.
disposeNeverLeavesTheGameFrozen Dispose the manager with both open. The freeze and the control lock are released.

Input

NarrativeInputComponentTest

The handler sits above the player and terminal input so nothing can be played behind a dialogue, but it must step aside for the terminal and must never unbalance the player's key bookkeeping.

Test Purpose Expected Result
ignoresEverythingWhileNothingIsPlaying Send keys and a click with no dialogue or cutscene. Nothing is consumed or advanced.
spaceEnterAndLeftClickAdvance Press SPACE, ENTER and left click during a dialogue. All three are consumed and advance it.
rightClickIsSwallowedWithoutAdvancing Right-click during a dialogue. Consumed, but does not advance.
escapeLeavesTheDialogueOrSkipsTheCutscene Press ESC while something is playing. Consumed, and the manager is asked to leave.
gameplayKeysAreSwallowedAndF1StillOpensTheTerminal Press W, J and F1 during a dialogue. W and J are consumed and do nothing; F1 passes through so the terminal still opens.
typingInTheTerminalDoesNotAdvanceTheDialogue With the terminal open, send SPACE, ENTER, ESC, a letter and a click. None are consumed, so the terminal gets them and the dialogue neither advances nor closes. This is the regression behind PR #231's reported bug.
inputIsHandledAgainOnceTheTerminalCloses Close the terminal and press SPACE. The dialogue advances again.
keyUpIsOnlySwallowedForKeysPressedDuringTheNarrative Hold D before the dialogue, press W during it, release both. Releasing D reaches the player; releasing W is swallowed โ€” so the player's walk direction stays balanced.
keyUpOfAKeySwallowedBeforeTheTerminalOpenedIsStillSwallowed Press W during a dialogue, open the terminal, release W. The release is still swallowed, because the player never saw the press.

Script Files

ScriptLoadingTest

These load the real shipped JSON, so a broken or missing script fails the build rather than a player.

Test Purpose Expected Result
demoDialogueLoadsWithSpeakersAndLines Load demo. It has lines, every line's speaker is defined, every line has text, and every speaker's portrait atlas exists.
jsonExtensionIsAccepted Load demo.json and demovideo.json. Both load.
demoCutsceneLoadsAndPointsAtARealVideo Load demovideo. It is skippable and its video path exists in the assets.
missingOrUnsafeNamesReturnNull Load nope, null, blank, ../configs/rooms and sub/dir. All return null โ€” a name can never escape the scripts folder.
everyDialogueAndCutsceneUsedByAFriendlyNpcHasAScript Read friendlyNpcs.json and load each NPC's dialogue and cutscene id. Every one exists, so an NPC can never hang waiting on a missing script.
builderMethodsAccumulate Chain speaker/line and ofVideo(...).at(...). Each call appends rather than replaces.

Video

Mp4VideoPlayerTest

Test Purpose Expected Result
rgbBytesAreConvertedToOpaqueRgba Convert a 2ร—1 picture of known values. JCodec's signed RGB bytes (value โˆ’ 128) become the expected opaque RGBA bytes.
onlyTheRequestedWindowIsCopied Convert the top row and the bottom row of a 2ร—2 picture. Only the requested window is copied โ€” this is how the decoder's macroblock padding (e.g. 368 decoded rows for a 360-row video) is cut off.
demoVideoDecodesFramesAndFinishes Decode the real videos/demo.mp4, stepping time until it ends. No failure, the whole video plays out, at least one frame is shown, and the texture is the true 640ร—360.
notAnMp4IsRejected Open a JSON file as a video. The constructor throws rather than starting a decoder.

World Freeze

EntityServiceFreezeTest

Test Purpose Expected Result
everythingUpdatesWhenNothingIsFrozen Update with no freeze. Every entity updates.
freezeStopsEveryoneExceptOptedInEntities Freeze, then update. Only entities that called setUpdatesWhilePaused(true) (the HUD and the narrative entity) update.
freezeIsReleasedOnlyWhenEveryOwnerHasReleased Two owners freeze; release one twice, then the other. Still frozen until both have released.
freezeIsIndependentOfTheInventoryPause Open the inventory pause (toggleUpdate), then freeze and unfreeze. The inventory pause is neither cleared nor changed by the freeze.
uiComponentsDoNotCountAsOptedIn Freeze with an enemy that owns a UIComponent (e.g. a health bar). The enemy is still frozen โ€” unlike the inventory pause, a freeze is not bypassed by UI components.

Terminal Commands

NarrativeCommandsTest

Test Purpose Expected Result
dialogueCommandPlaysTheNamedFile Run dialogue demo. The demo script is sent to the player.
dialogueCommandAcceptsAJsonExtension Run dialogue demo.json. Same script plays.
dialogueCommandRejectsMissingFilesAndBadArguments Run with an unknown file, no arguments, an extra argument and a null player. All return false (so the terminal stays open) and nothing plays.
cutsceneCommandPlaysTheNamedFile Run cutscene demovideo. The demovideo script is sent, pointing at videos/demo.mp4.
cutsceneCommandRejectsMissingFilesAndBadArguments Run with an unknown file, no arguments and a null player. All return false and nothing plays.

Manual Gameplay Test Plan

Setup for every test below: run the game (cd source && ./gradlew :desktop:run), start a run and open the debug terminal (F1). Tests that need enemies must be done in a dungeon room (walk to a bookshelf in the hub and press E); the hub has none. dialogue demo and cutscene demovideo can be run from anywhere.

Dialogue box

Test Steps Expected Result
Layout Run dialogue demo. A parchment box sits along the bottom with the text on the left and, on the right, a framed portrait with a name plate beneath it.
Above the HUD Look at the run timer at the bottom left while the box is open. The box is drawn over the HUD, never behind it.
Typewriter Watch the first line appear. Text types out at a steady pace; words do not jump to the next line half way through typing.
Continue hint Wait for the line to finish typing. A small bouncing arrow appears at the bottom right of the text box.
Finish then advance Press SPACE mid-line, then again. The first press shows the rest of the line; the second moves to the next line.
Speakers change Advance through the whole demo dialogue. The portrait and name change per line (Wandering Spirit, You, Travelling Merchant), and each portrait is animated.
Wrapping Read the longest line. It wraps inside the text panel without overflowing the box.
Ends cleanly Advance past the last line. The box disappears and the player can move again.
Resize Resize or maximise the window while the box is open. The box stays anchored to the bottom and rescales.

Input

Test Steps Expected Result
Other advance keys Advance lines using ENTER and a left click. Both advance; a right click does nothing.
Gameplay is inert During a dialogue press W A S D, J, K, I, TAB, Q, 1โ€“3. Nothing happens: the player does not move or attack and the inventory does not open.
Held key Hold D, run dialogue demo, release D during it, then finish. The player does not drift after the dialogue ends.
ESC leaves Press ESC mid-dialogue. The dialogue closes at once and controls return.
Restart after ESC At the Travelling Merchant press E, ESC mid-dialogue, then press E again. The dialogue starts again from the first line.
Terminal does not advance During a dialogue press F1, type spell (including the space). The text appears in the terminal and the dialogue stays on its current line.
Terminal closes Press F1 again, then SPACE. The terminal closes and SPACE advances the dialogue again.

World freeze

Test Steps Expected Result
Enemies stop In a dungeon room with moving enemies run dialogue demo. Every enemy stops where it is, projectiles and traps freeze and sprite animations stop.
Resume in place Finish the dialogue. Enemies carry on from exactly where they were.
No damage Run the dialogue with enemies next to the player. The player takes no damage while it is open.
Player locked Try to move, dash and attack during a dialogue. The player does nothing.
Run timer Note the run timer, open a dialogue for ~10 s, close it. The timer did not advance while the dialogue was open.
Inventory pause unchanged Close all dialogues, then press I. The inventory opens and pauses the game as before.
Prompt hidden Walk up to an NPC (so "Press E" shows), start a dialogue. The prompt disappears while the dialogue is open.

Video cutscene

Test Steps Expected Result
Plays full screen Run cutscene demovideo. The mp4 plays full screen over the game, correct colours and aspect ratio with black bars where the shape differs, for about 4 s.
Returns to the game Let it finish. The game is visible again, unfrozen, with controls back.
Skip Run it and press SPACE, then again with ESC, then with a click. Each skips at once and returns to the game.
World frozen Run it in a dungeon room. Enemies and the run timer are frozen for the whole video.
No audio Listen while it plays. Silent โ€” video only.
Resize Resize the window during playback. The picture keeps its aspect ratio.
Busy Run cutscene demovideo again while it is playing. Ignored; the first keeps playing.
Undecodable video Copy any text file to source/core/assets/videos/bad.mp4, create configs/cutscenes/bad.json containing { "video": "videos/bad.mp4" }, then run cutscene bad. The cutscene ends immediately, the game is not left frozen, and an error is logged.

Terminal commands

Test Steps Expected Result
Valid names Run dialogue demo and dialogue demo.json. Both play and the terminal closes.
Missing file Run dialogue nope and cutscene nope. Nothing plays and the terminal stays open.
Bad arguments Run dialogue with no argument, and dialogue demo extra. Rejected; the terminal stays open.
Allowed characters Type _, - and . into the terminal. They appear in the entered text.

Interactable NPCs

Test Steps Expected Result
Merchant In the hub press E next to the Travelling Merchant. The two-line merchant_greeting dialogue plays, then the cutscene, then controls return.
Repeatable Press E at the merchant again. The interaction plays again.
Spirit is once-only Complete the Wandering Spirit's interaction, then press E again. "The spirit has nothing more to say." and nothing plays.
Guardian is gated Approach the Sealed Guardian before the Spirit. "The guardian will not stir. Perhaps the spirit knows why."
Guardian unlocks Talk to the Spirit, run room clear, return to the guardian. The prompt becomes "Wake"; the cutscene plays with no dialogue.
E during a dialogue Press E repeatedly during an NPC dialogue. It does not restart or stack the interaction.
ESC mid NPC dialogue At the merchant press ESC during the dialogue. The dialogue closes; the NPC's cutscene then plays (and can itself be skipped with ESC); afterwards the NPC can be talked to again.

Combined

Test Steps Expected Result
Video over a dialogue While a dialogue is open, run cutscene demovideo from the terminal. The video plays and the dialogue box stays on top of it. The first ESC closes the dialogue, the second skips the video, and the game stays frozen until both have ended.
Back to back Run dialogue demo, finish it, then run cutscene demovideo. Each freezes and releases cleanly; the player can move afterwards.

Known Coverage Gaps

Everything uncovered in the logic classes is listed here. Nothing below is a known defect; each is either untestable without OpenGL or a branch that no shipped content reaches.

Location Uncovered Why
DialogueDisplay, CutscenePlayer, TypewriterText, PortraitActor The whole classes (0%) They create scene2d actors, textures and the stage, which need a real OpenGL context; GameExtension mocks GL. Covered by the manual plan above. The behaviour the manager needs from them is defined by the DialogueView / CutsceneView interfaces and tested through fakes.
NarrativeFactory The whole class Pure wiring of the narrative entity; it creates the UI components above. Exercised every time the game starts.
NarrativeManagerComponent.playCutscene(String) The load-by-id path, including the "missing file still sends cutsceneFinished" branch The dialogue version of this rule is tested (startDialogueEventPlaysTheDialogueAndMissingFilesStillFinish) but the cutscene twin is not.
NarrativeManagerComponent.playDialogue(String) The "file found" branch Tests reach a real file only through DialogueCommand, which sends the already-loaded script.
NarrativeManagerComponent.update() (no argument) The frame hook Tests drive the package-private update(float) directly.
NarrativeManagerComponent.hold(...) Clearing the "Press E" prompt The test player has no InteractionPromptDisplay; checked manually under "Prompt hidden".
CutsceneEvents.playDialogue(Entity, String) / playCutscene(Entity, String) The two one-line helpers and the private constructor The script-object helpers and the underlying events are covered.
NarrativeInputComponent.touchUp Releasing a mouse button that was swallowed touchDown is covered.
Mp4VideoPlayer decoder thread The catch blocks for a decode failure or interruption Only a valid video and a non-mp4 file are tested; a file that fails part-way through is not.
CutsceneScript.TimedEvent() The no-argument constructor Only used by the JSON reader. A throwaway test confirmed an events list in a cutscene file parses correctly, but no shipped file has one, so it is not in the suite.
ScriptNames The private constructor and the "no Gdx.files" branch Defensive guards.
Early returns in DialogueRunner and NarrativeManagerComponent A few if (...) return; guards (starting or finishing a dialogue twice, a blank timed-event name, a null script, skipping with nothing open) Defensive; no shipped content reaches them.

Areas Not Covered by Automated Tests

  • Visual correctness. Nothing asserts a pixel. Whether the box looks right, whether the typewriter wraps cleanly, whether the portrait scales sensibly and whether the nine-patch borders line up is judged manually.
  • MainGameScreen wiring of the freeze. Skipping physics, room logic and the run timer, and zeroing the time scale, while frozen has no automated test; it is only checked by the world-freeze manual tests.
  • Wall-clock timers. The freeze zeroes the shared game time scale, but timers that read the system clock or build their own GameTime (for example some cooldown and effect deadlines) keep running while a dialogue is open, so they can already have expired when the game resumes.
  • Video variety. Only the baseline H.264 demo clip is decoded in tests. Other profiles, large or high-resolution videos and long videos depend on JCodec and the player's machine; performance is judged manually. There is no audio.
  • The real terminal input chain. The terminal-open guard is unit tested with a stand-in for the terminal; the end-to-end behaviour (F1, typing spell , SPACE) was verified in the running game and is in the manual plan.
  • NPC event contract. The sequencing of dialogue and cutscene around an NPC (NpcInteractionSequence, NpcInteractorComponent) belongs to the Interactable NPCs tests and is not repeated here; this plan only checks that every NPC's scripts exist.
  • Content quality. Placeholder cutscenes, writing and tone are not tested. The NPC cutscenes currently reuse the demo video.

Related pages