## Agility Courses Agility courses are defined in `data/courses/.yaml`. Each course defines a sequence of obstacle rooms, each with a specific verb the player must type to advance. Obstacle rooms themselves are standard room YAML files (typically in `data/rooms/agility/`). **The filename is the ID** (`vent_shaft.yaml` → `vent_shaft`); the loader derives it from the filename. Do not put an `id:` field in the file — it is ignored. > Editing courses is done from the **Map screen** in the admin web GUI: select an obstacle room and use its **Course** side-panel tab to create a course, add the room as a step, edit all of its parameters, or remove it. The legacy standalone "Courses" editor tab has been removed. > > When a room is added as a course step, the admin GUI auto-creates **always-blocked one-way exits** between consecutive obstacle rooms so the in-game map can lay them out correctly. The direction of these exits is chosen by the builder. Always-blocked exits are blocked to players (movement is refused with the standard "blocked" message and they show "(blocked)" in `look`) but visible on the map — they exist solely for map rendering. On the map, course links appear as blue dashed one-way arrows (colored via the `map_course` theme token). ### Course YAML ```yaml name: "Ventilation Shaft Course" required_level: 1 start_room: 200 completion_xp: 40 obstacles: - room_id: 201 verb: scramble xp: 8 fail_damage: [1, 2] fail_chance: 0.25 # optional; omit to derive from agility level phases: - message: "You approach the corroded ventilation wall..." delay: 0 - message: "You find footholds in the rusted panels and begin to climb..." delay: 2 fail_check: true # the failure roll happens on this phase - message: "You scramble up the wall and haul yourself onto the ledge!" delay: 2 - room_id: 202 verb: balance ... ``` ### CourseConfig Fields | Field | Type | Description | | ---------------- | ------------- | ---------------------------------------------------- | | `id` | string | Unique course identifier (filename stem) | | `name` | string | Display name shown to players | | `required_level` | int | Minimum Agility level to attempt the course | | `start_room` | int | Hub room — teleported here on fail or lap completion | | `completion_xp` | int | Bonus XP awarded when a full lap is completed | | `obstacles` | []ObstacleDef | Ordered list of obstacles | ### ObstacleDef Fields | Field | Type | Description | | -------------- | ------------- | ---------------------------------------------------------------------------- | | `room_id` | int | Room ID for this obstacle | | `verb` | string | Command the player types to attempt it (e.g. `scramble`, `jump`, `climb`) | | `exit_dir` | string | Direction for the always-blocked exit created by admin GUI between obstacles (e.g. `south`, `east`) | | `xp` | int | XP awarded for successfully completing this single obstacle | | `fail_damage` | [2]int | `[min, max]` damage on failure (HP clamped to minimum 1 — cannot kill) | | `fail_chance` | float64 | Optional explicit failure probability (0–1). Omit to derive dynamically. | | `phases` | []ObstaclePhase | Preferred form: ordered list of phases (see below) | | `ticks_per_phase` | float64 | **Legacy form only:** shared delay between phases (used when `phases` absent)| | `messages` | []string | **Legacy form only:** exactly 3 messages (used when `phases` absent) | **Two forms are supported.** If `phases` is provided it is used directly. Otherwise the legacy `messages` + `ticks_per_phase` form is auto-migrated: 3 phases with delays `[0, ticks_per_phase, ticks_per_phase]` and the failure check on the middle (index 1) phase. The admin GUI always writes the `phases` form when saving. ### ObstaclePhase Fields | Field | Type | Description | | ------------ | -------- | -------------------------------------------------------------------------- | | `message` | string | Text printed when this phase advances | | `delay` | float64 | Ticks to wait before printing this phase (supports fractional via `engine.ToTicks`) | | `fail_check` | bool | If true, the obstacle's failure roll happens on this phase (set on at most one phase) | ### Phase Advancement An obstacle advances through its phases in order. For each phase, the engine waits `delay` ticks, then prints the message. If the phase carries `fail_check`, the failure roll is made *before* awarding success. On the final phase, XP is awarded and the player is teleported to the next obstacle's room (or to `start_room` if this was the last obstacle — completing a lap). ### Failure - If the obstacle defines `fail_chance`, that value (clamped to 0–1) is the failure probability for every attempt regardless of the player's level. - Otherwise the chance is derived from Agility level vs `required_level`: 30% fail at the required level, −1% per level above, clamped to 5–60%. - On failure, the player takes random damage in `[fail_damage[0], fail_damage[1]]`, is teleported back to `start_room`, and must restart the course. ### Lap Counting Completing all obstacles in a course increments a lap counter stored as a player flag (`agility_laps_`). The `completion_xp` bonus is awarded on the final obstacle only. ### Obstacle Room YAML Each obstacle room should have an `on_enter` message telling the player which verb to use, and a `down` exit back to the course hub: ```yaml name: "Ventilation Shaft - Corroded Wall" description: "A towering wall of corroded ventilation panels..." on_enter: - steps: - messages: ["Type 'scramble' to climb the wall."] exits: down: room: 200 blocked_message: "" ``` The hub room (e.g. room 200) links to the first obstacle of each course: ```yaml name: "Agility Training Grounds" exits: south: 17 north: 201 # Vent Shaft (level 1) east: 210 # Rooftop (level 20) west: 220 # Reactor (level 50) ``` ### Supported Obstacle Verbs | Verb | Gerund (display) | | ---------- | ---------------- | | `scramble` | scrambling | | `jump` | jumping | | `swing` | swinging | | `balance` | balancing | | `climb` | climbing | | `crawl` | crawling | | `vault` | vaulting | | `leap` | leaping | | `slide` | sliding | Obstacle verbs are auto-registered at startup from all course YAML files. No changes to `cmd_registry.go` needed when adding new courses — any verb in a `data/courses/` file will work.