1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
## Agility Courses
Agility courses are defined in `data/courses/<id>.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.
>
> 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_agility` 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 (fractional values allowed) |
| `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_<course_id>`). 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:
room: 17
north:
room: 201 # Vent Shaft (level 1)
east:
room: 210 # Rooftop (level 20)
west:
room: 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 — any verb in a
`data/courses/` file will work without additional configuration.
|