Repository navigation
Training Grounds
Task: #328
PR: #352
Builds on: Interactable NPCs, Cutscenes and Dialogue, Game Room System
A safe practice room the Old Sage can teleport the player to. It has notice boards that explain every control and props that let the player try each one: dummies to hit, targets to shoot, potions to try and cannons to dodge. Nothing in it can hurt the player, and nothing in it is kept when they leave.
The room, its props and the Sage's offer are all data-driven: they are described in rooms.json, friendlyNpcs.json and configs/dialogues, so adding another prop, board or offer needs no new code.
- In the Dungeon Selection hub, press E next to the Old Sage and listen to his tutorial (
SPACE/ENTERadvances,ESCleaves early). - When he finishes, a menu asks whether to go to the training grounds. W / S (or the arrow keys) choose, ENTER, SPACE or E confirm, ESC or Q stay where you are.
- In the training grounds, walk up to a notice board and press E to read it. Each prop below shows a yellow marker and a prompt at the top right when it can be used.
- To leave, press E next to the second Old Sage and choose Return to the village.
The room is split into zones by floor colour. The player arrives in the bottom left.
| Zone | Where | What is there | What it teaches |
|---|---|---|---|
| Start | Bottom left | The Old Sage and the Controls Board | Movement, dash (SPACE), E, inventory (I, TAB, Q), weapon swap (1 2 3), J / K, Hecate's ability (L) |
| Melee | Bottom middle | Melee Board and 4 Training Dummies | Quick attacks (J) and heavy attacks (K) with the sword and knife |
| Supplies | Bottom right | Supplies Board, 7 supply stands and 2 Test Dummies | Potions (TAB, Q), freeze bombs, burn vials and magnet potions |
| Archery | Top left | Archery Board, a firing line, 3 near and 2 far Archery Targets | Bow (3), shooting in the facing direction, the three-arrow spread |
| Dodge range | Top right | Dodge Board and 3 Training Cannons | Dashing and dodging |
| Prop | Behaviour |
|---|---|
| Training dummy / Test dummy | Takes hits like an enemy and shows the damage as a number floating up. It is healed after every hit and never dies, so weapons, spells, freeze bombs and burn vials all work on it. The status line under the room title counts hits. |
| Archery target | Same as a dummy, drawn as a target. Arrows stop on the first thing they hit, so a near target can shield a far one. Near targets are about 8 tiles from the firing line and far ones about 14. |
| Notice board |
E plays a short dialogue with the board as the speaker. Like any dialogue it freezes the world while it is open. |
| Supply stand |
E lends one of its item (health, shield, speed or strength potion, freeze bomb, burn vial or magnet potion). You can only hold one lent copy per item; use it and take another. |
| Training cannon | Fires a harmless orb down its lane every 2.4 s, staggered between cannons. An orb that touches you is a hit, one that passes close without touching is a dodge. The status line keeps the totals. There is never any damage. |
Heavy attacks (K) need a weapon upgrade bought from the merchant, so a new player could never try one. While the player is inside the training grounds:
- Every weapon counts as upgraded. The room says so on entry. Weapons that were already upgraded stay upgraded.
- Lent items are tracked. Whatever is held above what the player walked in with is taken back on the way out, so supplies cannot be carried into a dungeon. Potions they brought in are never touched.
Both are put back exactly when the player leaves the room, however they leave.
Open the Debug Terminal with F1:
| Command | Does |
|---|---|
room goto tutorialRoom |
Moves you straight into the training grounds, with the same enchantment and prop behaviour as arriving through the Sage |
The training grounds is an ordinary room entry with two extra fields. See Dungeon JSON Schema for the shared fields.
{
"id": "tutorialRoom",
"title": "Training Grounds",
"trainingGrounds": true,
"mapWidth": 60,
"mapHeight": 46,
"entryPoints": [{ "id": "fromSage", "x": 6, "y": 8 }],
"enemySpawns": [],
"npcSpawns": [{ "npcId": "trainingGuide", "x": 11, "y": 7 }],
"propSpawns": [
{ "type": "SIGN", "x": 7, "y": 14, "label": "Controls Board", "dialogueId": "tutorial_sign_controls" },
{ "type": "TRAINING_DUMMY", "x": 27, "y": 15, "label": "Training Dummy" },
{ "type": "SUPPLY_STAND", "x": 43, "y": 15, "itemId": "HEALTH_POTION" },
{ "type": "DODGE_CANNON", "x": 45, "y": 41, "interval": 2.4, "delay": 0.0, "range": 16.0 }
],
"obstacles": { "spawns": ["..."] }
}-
trainingGroundsmarks a room as a practice space. Only such rooms get the prop manager and the weapon and item handling, andpropSpawnsis rejected in any other room when the world is validated. - The room has
dungeonIdofnull, so entering it does not start a dungeon timer, and it uses the hub's calm music. - It has no enemies and no exits. The Sage is the only way in or out.
- Row
yof theobstacles.spawnsgrid is tiley, counted from the bottom of the room, so the grid reads upside down.#is a wall,Ba crate, digits are floor tiles (4green,5red,6brown,7dark wood,8white) and a space is void. - The hub gained an entry point
fromTutorial(12, 24) for the way back, next to the Sage's own spot.
Each entry is a PropSpawnConfig. x and y are tile coordinates and must be on open floor, not a wall or crate.
| Field | Used by | Default | Purpose |
|---|---|---|---|
type |
all | required |
TRAINING_DUMMY, ARCHERY_TARGET, SIGN, SUPPLY_STAND or DODGE_CANNON
|
label |
all | per type | Name used in prompts and score messages (e.g. "Melee Board") |
dialogueId |
SIGN |
required | Id of a dialogue in configs/dialogues
|
itemId |
SUPPLY_STAND |
required | A consumable id from ItemIds; it needs an icon (see below) |
interval |
DODGE_CANNON |
2.4 |
Seconds between shots |
delay |
DODGE_CANNON |
0 |
Seconds before the first shot, to stagger neighbouring cannons |
range |
DODGE_CANNON |
20 |
Tiles an orb travels before it fades |
A prop with a bad definition (no type, a sign with no dialogue, a stand for an unknown item) is skipped and logged rather than stopping the room from loading.
Any friendly NPC can offer to teleport the player once their interaction finishes, by adding a teleportOffer:
{
"id": "tutorialGuide",
"name": "Old Sage",
"dialogueId": "tutorial_guide",
"teleportOffer": {
"roomId": "tutorialRoom",
"entryPointId": "fromSage",
"title": "Training Grounds",
"prompt": "The Old Sage can send you to his training grounds...",
"confirmLabel": "Take me to the training grounds",
"declineLabel": "Not right now"
}
}There are two Old Sage definitions: tutorialGuide in the hub (offers tutorialRoom) and trainingGuide in the training grounds (offers selection, entry fromTutorial). roomId and entryPointId are required; the labels default to "Yes" and "Not now". This is validated with the rest of the NPC definitions. See Interactable NPCs.
Each board plays a normal dialogue with one speaker, board, whose portrait is images/tutorial/sign.png: tutorial_sign_controls, tutorial_sign_melee, tutorial_sign_archery, tutorial_sign_supplies and tutorial_sign_dodge. The Sage's own scripts are tutorial_guide and training_guide.
Limitation: keys named in these texts are plain words. If a binding changes in KeyboardPlayerInputComponent, the dialogues need updating to match.
-
RoomFactory.createTrainingRoom, which wraps the ordinarycreateRoomwith the training components -
RoomLoader, which chooses betweencreateRoomandcreateTrainingRoomfrom the room'strainingGroundsflag -
RoomSelector, which moves the player (teleport) and tells the room when E is pressed -
TeleportMenu,TeleportMenuDisplayandKeyboardTeleportMenuInputComponent: the question after the Sage's dialogue - Room-side components:
-
TutorialPropManagerComponent: spawns the props, finds the one in range, shows its prompt and marker, routes E to it -
TrainingLoadoutComponent: the weapon enchantment and the lent items
-
- Prop-side components, built by
TutorialPropFactory:-
TrainingDummyComponentandDamagePopupDisplay: dummies and targets -
PropInteractableComponent: signs and supply stands -
FloatingIconComponent: the item floating over a stand -
DodgeCannonComponent: cannons and their orbs
-
-
PropSpawnConfigandRoomConfig.trainingGrounds(the data)
sequenceDiagram
actor P as Player
participant N as NpcInteractorComponent
participant M as TeleportMenu
participant S as RoomSelector
participant R as Training room
participant L as TrainingLoadoutComponent
P->>N: E next to the Old Sage
N-->>P: dialogue plays, then INTERACTION_FINISHED
P->>M: INTERACTION_FINISHED (npc has a teleportOffer)
M->>M: open, lock controls
P->>M: ENTER on "Take me to the training grounds"
M->>M: close, unlock controls
M-->>S: teleportToRoom(tutorialRoom, fromSage)
S->>S: schedule the room switch
S->>R: dispose old room, create training room
R->>L: RoomCreated(player)
L->>L: upgrade weapons, remember item counts
Note over P,L: Later, leaving the room disposes it and the loadout restores everything
-
The offer.
TeleportMenulistens on the player forINTERACTION_FINISHED. If the NPC that just finished has ateleportOfferit opens, locks the player's controls under its own owner and stops any walk. Opening on finished rather than on start means the Sage says everything first and the question never shares the screen with the dialogue box. -
The move. Accepting triggers
RoomSelector.TELEPORT_REQUEST(teleportToRoom) on the player with the room id and entry point.RoomSelector.teleportlooks the entry point up inRoomLoader, then schedules the switch for the next safe point in the frame, like any other room change. Theroom gotodebug command shares that scheduling. -
Choosing the room's factory.
RoomLoaderbuilds a room withcreateTrainingRoomwhen its config hastrainingGrounds, otherwise with the usualcreateRoom.createTrainingRoomcallscreateRoomand then attaches the prop manager and the loadout, so the shared factory is built exactly as before for every other room. -
Entering. On
RoomCreatedthe manager builds its props and the loadout enchants the weapons. -
Using a prop.
RoomSelectortriggerspropInteracton the room whenever E is pressed. The manager hands it to the nearest prop in range, unless an NPC interaction is running or an NPC is in range, since the Sage always takes priority. -
Leaving. The room is disposed on any switch.
TrainingLoadoutComponent.disposeputs the weapon upgrades and item counts back, and the manager clears any prompt it put up.
| Event | On | Arguments | Meaning |
|---|---|---|---|
teleportToRoom (RoomSelector.TELEPORT_REQUEST) |
Player | (String roomId, String entryPointId) |
Move the player to a named entry point of any room |
propInteract (RoomSelector.PROP_INTERACT) |
Room | (Entity player) |
E was pressed; props decide whether it is for them |
teleportMenuOpened / teleportMenuClosed
|
Menu entity | none | The teleport menu came up or went away |
trainingTargetHit (TrainingDummyComponent.HIT) |
Dummy or target | (String label, Integer damage, Integer totalHits) |
A hit landed; drives the popup and the status line |
dodgeResult (DodgeCannonComponent.RESULT) |
Cannon | (Boolean dodged) |
An orb passed (true) or hit (false) |
EventHandler casts every listener of an event name to one argument count, so keep to the signatures above.
- Dummies are dynamic bodies. Weapon hitboxes are kinematic and Box2D never makes contacts between a kinematic and a static body, so a static dummy could not be hit. A dummy is held still every frame instead.
-
Dummies are enemies as far as targeting goes. They have a hitbox on the NPC layer, a
CombatStatsComponentand aStatusEffectsControllerComponent, so spells, freeze bombs and burn vials treat them like enemies. They are tracked by the prop manager, never byEnemyManagerComponent, so they cannot block a room from being cleared. - Orbs have no physics. The cannon moves them and checks how close they get to the player, so they cannot snag on walls and never deal damage. An orb only counts as dodged if the player was within about two tiles of its path, so wandering elsewhere in the room is not a dodge.
-
A lent item is only ever one copy.
canLendis true while the player holds no more than they walked in with. Anything the room does not lend is refused. -
Item art is pre-shrunk. The shipped item textures are about 1250px square. Drawn at under a metre they alias into a few specks, so each stand uses a 64px icon in
images/tutorial/icons/, named after the item id. A stand for an item without an icon is skipped. -
Prop art is in
images/tutorial/. It was drawn for this feature and is placeholder-quality.
Hecate's ability menu and the teleport menu are both modal, keyboard-driven menus on a cream panel, so they share their code:
| Class | Role |
|---|---|
ModalMenu |
Interface for the state of such a menu: isOpen, moveSelection, confirm, close, getSelectedIndex. Both AbilityMenu and TeleportMenu implement it. |
ModalMenuInputComponent |
Keyboard handling. Swallows every input while open so nothing walks around behind the menu; W/S and arrows move, ENTER/SPACE/E confirm, ESC/Q close. Registered above the UI stage and terminal (10) and below dialogue input (20). |
ModalMenuDisplay |
The panel: title, rows, key hint and highlight. A subclass says what the rows are. |
KeyboardAbilityMenuInputComponent, KeyboardTeleportMenuInputComponent, AbilityMenuDisplay and TeleportMenuDisplay are now thin subclasses. A new menu of this kind needs a state class implementing ModalMenu plus those two small subclasses.
A new notice board: add a dialogue in configs/dialogues with the board speaker, then add a SIGN entry to the room's propSpawns with its dialogueId.
A new dummy, target or cannon: add a TRAINING_DUMMY, ARCHERY_TARGET or DODGE_CANNON entry on open floor. Keep cannons at least a second apart so an orb can be dodged.
A new supply stand: add a 64px icon images/tutorial/icons/<item_id lowercase>.png, list the id in TutorialPropFactory, then add a SUPPLY_STAND entry. The room's TrainingLoadoutComponent picks the item up from the config.
A new prop type: add a value to PropSpawnConfig.PropType, a builder in TutorialPropFactory, and a case in TutorialPropManagerComponent.build. The switch has no default, so the compiler points to every place that needs the new type.
Another teleport: give any NPC a teleportOffer naming a room and one of its entry points.
-
A separate factory function, not a branch in
createRoom. The training grounds are built bycreateTrainingRoom, which callscreateRoomand adds to the result. This was a review suggestion on PR #352: it keeps every other room's construction identical, so it is less likely to break them. -
Props are not enemies. A separate
propSpawnslist and manager, like the friendly NPCs, so props never count towards clearing a room, never drop loot and cannot affect the dungeon flow. - Temporary, not granted. Enchanting weapons and lending items for the length of the visit, and restoring them on leaving, means practice can never be used to skip the merchant or stock up for a dungeon.
- The Sage asks, rather than a door. Reusing the Old Sage and the NPC system keeps the tutorial in one place and needed only a config field, not a new world exit.
- Confirm is ignored for 400 ms. Players mash through dialogue, and a press meant for the Sage's last line must not also accept the offer. The clock is injectable so this is tested without sleeping.
-
Abilities and spells are not playable here. Hecate's abilities (
L) need attuning at Hecate, and spells have no key binding, so the room only describes them. - Art is placeholder. The dummy, target, sign, pedestal, cannon and orb sprites were generated for this task.
- No score or progression. Hits, dodges and clipped orbs are only reported in the status line and are not stored or rewarded.
- Dialogue freezes the world. Reading a board stops the cannons and any floating numbers, since every dialogue does. There is no non-modal sign.
-
Props cannot be placed outside
trainingGroundsrooms. This is enforced on purpose; a second practice room would set the same flag.
See the Training Grounds Test Plan for the automated tests, coverage and the manual QA checklist.
Entities and Components
Input Handling
- Service Locator
- Loading Resources
- Logging
- Unit Testing
- Debug Terminal
- Animations
- Audio
- AI
- Physics
- Terrain
- Concurrency & Threading
- Settings