Repository navigation
Interactable NPCs Test Plan
Woutermelon edited this page Oct 8, 2026
·
1 revision
- Unit tests (JUnit 5 + Mockito) cover each class in isolation, with the most coverage on the sequence state machine and availability rules, where bugs are likely and hard to spot in play.
-
Config tests load the real
friendlyNpcs.jsonandrooms.json, so a bad edit to either fails the build instead of the game. -
Integration-style unit tests wire real components together on a test entity (interactor +
PlayerActions) to check locking end to end without rendering. - Manual play testing covers what can't be unit tested: the sprite, indicator, prompt, and the real narrative system.
- Tests use
GameExtension(headless libGDX) where engine classes are needed. Rendering and asset loading are mocked.
Run with ./gradlew test from source/. Reports: source/core/build/reports/tests/test/index.html.
NpcInteractionSequenceTest: ordering and locking (no libGDX)
| Test | Checks |
|---|---|
dialogueOnlyNpcLocksUntilDialogueFinishes |
Lock → dialogue → unlock on finish |
cutsceneOnlyNpcPlaysCutsceneWithoutDialogue |
Cutscene-only NPC works |
cutsceneAfterDialogueKeepsControlsLockedBetweenSteps |
No unlock/relock flicker between steps |
cutsceneBeforeDialogue |
BEFORE_DIALOGUE order |
duringDialogueCueStartsCutsceneAndWaitsForBoth |
Cue starts cutscene; finishes only when both end |
duringDialogueWithoutCueStillPlaysCutsceneAfterwards |
Missing cue doesn't skip the cutscene |
cueIsIgnoredForOtherTimingsAndRepeatedCues |
Cue only affects DURING_DIALOGUE, only once |
onlyLocksForStepsThatAskForIt |
Per-step lock flags |
neverLocksWhenNothingAsksForIt |
Free-movement NPCs never lock |
ignoresFinishEventsForOtherContent |
Other systems' finish events don't advance it |
handlesContentThatFinishesSynchronously |
Missing content that reports "finished" immediately |
cancelUnlocksWithoutFinishing |
Cancel releases the lock, doesn't complete |
startIsIdempotent |
Calling start() twice has no effect |
NpcInteractorComponentTest: player-side component
| Test | Checks |
|---|---|
runsDialogueThenCutsceneAndRestoresControls |
Full flow through events with real PlayerActions
|
refusesASecondInteractionWhileBusy |
One interaction at a time |
cueStartsDuringDialogueCutscene |
Cue event is forwarded |
dyingCancelsAndUnlocksWithoutCompleting |
entityDied cancels cleanly |
disposeReleasesLock |
No stuck lock on dispose |
worksWithoutPlayerActions |
No crash on entities without PlayerActions
|
finishEventsWithNoActiveInteractionAreIgnored |
Stray events are harmless |
markCompletedIsRemembered |
Completion API (for saving later) |
NpcInteractableComponentTest: NPC-side rules
| Test | Checks |
|---|---|
detectsRangeFromCentres |
In range at 1.4, out at 1.6 (range 1.5) |
promptsWithNameAndVerb |
Prompt text |
interactingStartsDialogue |
E starts the dialogue; NPC becomes BUSY
|
onceOnlyNpcCannotBeRepeated |
once + completedMessage
|
repeatableNpcCanBeTalkedToAgain |
Default is repeatable |
waitsForRoomToBeCleared |
requiresRoomCleared + unavailableMessage
|
waitsForPrerequisiteNpc |
requiresCompleted |
playerWithoutInteractorCannotInteract |
Safe when the player lacks the component |
indicatorEventsFireOnlyOnChange |
No repeated show/hide events |
interactingHidesIndicator |
Indicator hides on interact |
FriendlyNpcManagerComponentTest: room-side spawning
| Test | Checks |
|---|---|
roomWithoutNpcsLoadsNothing |
No asset loading for empty rooms |
rejectsUnknownNpcIds |
Typo in rooms.json fails fast |
loadsEachAtlasOnce |
Shared atlases loaded once |
spawnsOnRoomCreatedAndFindsNearestInRange |
Spawning, registration, nearest NPC, prompt |
showsIndicatorOnlyOnNearestAvailableNpc |
Only one indicator at a time |
roomClearedConditionReadsTheEnemyManager |
Room-cleared condition tracks real enemy deaths |
disposeUnloadsAssets |
Assets released on room change |
RoomManagerNpcTest: key routing
| Test | Checks |
|---|---|
npcInRangeConsumesTheKeyPress |
NPC takes priority over exits |
unavailableNpcExplainsWhy |
Status message for unavailable NPC |
InteractableNpcConfigsTest: configuration
| Test | Checks |
|---|---|
shippedDefinitionsLoadAndValidate |
Real friendlyNpcs.json is valid |
shippedDefinitionsCoverDialogueOnlyCutsceneOnlyAndBoth |
Shipped data exercises each kind |
everyRoomSpawnNamesADefinedNpc |
rooms.json and friendlyNpcs.json agree |
rejectsNpcWithNeitherDialogueNorCutscene |
Validation |
rejectsDuplicateIds |
Validation |
rejectsUnknownPrerequisite |
Validation |
rejectsMissingSprite |
Validation |
acceptsCutsceneOnlyNpcWithTexture |
Static texture path accepted |
Additions to existing tests
| Test | Checks |
|---|---|
InteractionPromptTest.shouldPromptForNpcWithVerbAndName |
NPC prompt text and defaults |
InteractionPromptTest.shouldPreferNpcPromptOverItemAndExit |
Prompt priority |
KeyboardPlayerInputComponentTest.shouldIgnoreEWhileNpcInteractionIsRunning |
E does nothing mid-interaction |
KeyboardPlayerInputComponentTest.shouldNotPickUpItemWhenEStartsNpcInteraction |
One key press, one action |
Run from a fresh game. The NPCs are in the starting Dungeon Selection room.
| # | Steps | Expected result |
|---|---|---|
| M1 | Walk towards the Wandering Spirit | Marker appears above it when in range; prompt reads "Press E — Talk to Wandering Spirit"; both disappear when walking away |
| M2 | Press E next to the Spirit | Dialogue opens; WASD, dash and attack do nothing |
| M3 | Finish the dialogue | Cutscene plays; controls still locked; controls return when it ends |
| M4 | Press E on the Spirit again | "The spirit has nothing more to say." No dialogue |
| M5 | Before M2, press E on the Sealed Guardian | Its unavailable message; nothing plays |
| M6 | After M3, press E on the Guardian | Cutscene only, no dialogue |
| M7 | Talk to the Merchant twice | Plays both times (repeatable) |
| M8 | Attack and cast spells at an NPC | No damage, no knockback, not targeted |
| M9 | Stand where an NPC and a bookshelf exit are both in range, press E | NPC interaction starts; room doesn't change |
| M10 | Drop an item next to an NPC, press E | NPC interaction starts; item isn't picked up |
| M11 | Press E repeatedly during dialogue | Interaction isn't restarted |
| M12 | Leave the room and come back | NPCs respawn; Spirit still shows its completed message |
| M13 | Skip a dialogue or cutscene | Controls return; sequence continues or ends normally |
| Risk | Mitigation |
|---|---|
| Narrative system never sends "finished", so controls are stuck | Narrative system always sends "finished", including for skipped or missing content; contract documented above |
Listener arity mismatch causes ClassCastException
|
Arity fixed in the contract; tests listen with the documented signatures |
Typo in rooms.json or friendlyNpcs.json
|
Validated on load; config tests run against the real files |
| Invisible NPC from a wrong animation name | Factory throws with the NPC id, atlas and animation |
| NPC spawned outside the map |
EntityManagerComponent rejects out-of-bounds spawns (logged) |
| Player dies mid-conversation | Interaction cancelled and lock released |