aboutsummaryrefslogtreecommitdiff
path: root/building_guide/courses.md
blob: d7d2877f662a51c34ac3f7571105b648186df8e3 (plain)
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
## 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. 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_<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: 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.