Skip to content

Training Grounds

Benjamin Chau edited this page Oct 22, 2026 · 1 revision

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.

Player guide

  1. In the Dungeon Selection hub, press E next to the Old Sage and listen to his tutorial (SPACE / ENTER advances, ESC leaves early).
  2. 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.
  3. 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.
  4. To leave, press E next to the second Old Sage and choose Return to the village.

The zones

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

Props

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.

What changes while you are in the room

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.

Trying it out

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

Configuration

The room, in assets/configs/rooms.json

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": ["..."] }
}
  • trainingGrounds marks a room as a practice space. Only such rooms get the prop manager and the weapon and item handling, and propSpawns is rejected in any other room when the world is validated.
  • The room has dungeonId of null, 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 y of the obstacles.spawns grid is tile y, counted from the bottom of the room, so the grid reads upside down. # is a wall, B a crate, digits are floor tiles (4 green, 5 red, 6 brown, 7 dark wood, 8 white) 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.

Props: propSpawns

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.

The Sage's offer, in assets/configs/friendlyNpcs.json

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.

Notice board dialogues

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.

Participants

  • RoomFactory.createTrainingRoom, which wraps the ordinary createRoom with the training components
  • RoomLoader, which chooses between createRoom and createTrainingRoom from the room's trainingGrounds flag
  • RoomSelector, which moves the player (teleport) and tells the room when E is pressed
  • TeleportMenu, TeleportMenuDisplay and KeyboardTeleportMenuInputComponent: 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:
    • TrainingDummyComponent and DamagePopupDisplay: dummies and targets
    • PropInteractableComponent: signs and supply stands
    • FloatingIconComponent: the item floating over a stand
    • DodgeCannonComponent: cannons and their orbs
  • PropSpawnConfig and RoomConfig.trainingGrounds (the data)

How it fits together

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
Loading
  1. The offer. TeleportMenu listens on the player for INTERACTION_FINISHED. If the NPC that just finished has a teleportOffer it 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.
  2. The move. Accepting triggers RoomSelector.TELEPORT_REQUEST (teleportToRoom) on the player with the room id and entry point. RoomSelector.teleport looks the entry point up in RoomLoader, then schedules the switch for the next safe point in the frame, like any other room change. The room goto debug command shares that scheduling.
  3. Choosing the room's factory. RoomLoader builds a room with createTrainingRoom when its config has trainingGrounds, otherwise with the usual createRoom. createTrainingRoom calls createRoom and then attaches the prop manager and the loadout, so the shared factory is built exactly as before for every other room.
  4. Entering. On RoomCreated the manager builds its props and the loadout enchants the weapons.
  5. Using a prop. RoomSelector triggers propInteract on 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.
  6. Leaving. The room is disposed on any switch. TrainingLoadoutComponent.dispose puts the weapon upgrades and item counts back, and the manager clears any prompt it put up.

Events

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.

Rules worth knowing

  • 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 CombatStatsComponent and a StatusEffectsControllerComponent, so spells, freeze bombs and burn vials treat them like enemies. They are tracked by the prop manager, never by EnemyManagerComponent, 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. canLend is 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.

Modal menus

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.

Adding things

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.

Design decisions

  • A separate factory function, not a branch in createRoom. The training grounds are built by createTrainingRoom, which calls createRoom and 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 propSpawns list 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.

Known limitations and future work

  • 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 trainingGrounds rooms. This is enforced on purpose; a second practice room would set the same flag.

Testing

See the Training Grounds Test Plan for the automated tests, coverage and the manual QA checklist.

Related pages

Table of Contents

Home

Getting Started

Game Engine

Entities and Components
Input Handling

Features

Components
Achievements
Player
Items and Inventory
Enemies
Bosses
Interactable NPCs
Cutscenes and Dialogue
Training Grounds
Music
Weapons
Abilities & Spells
Rooms and Dungeons
Game Screens and Areas
UI

Test Plans

Team 1
Team 2

Troubleshooting

Clone this wiki locally