From 94823b52168b44894fb5e9c960358ed02ebb122b Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sun, 5 Jul 2026 22:35:21 -0400 Subject: feat: courses gui overhaul, add course tab to map, add hidden exits to give courses structure --- building_guide/courses.md | 75 ++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 34 deletions(-) (limited to 'building_guide') diff --git a/building_guide/courses.md b/building_guide/courses.md index e11f909..e5deb32 100644 --- a/building_guide/courses.md +++ b/building_guide/courses.md @@ -4,6 +4,10 @@ Agility courses are defined in `data/courses/.yaml`. Each course defines a s **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 **hidden one-way exits** between consecutive obstacle rooms so the in-game map can lay them out correctly. The direction of these hidden exits is chosen by the builder. Hidden exits are invisible in `look` output and cannot be traversed — 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 @@ -14,29 +18,27 @@ completion_xp: 40 obstacles: - room_id: 201 verb: scramble - ticks_per_phase: 2 xp: 8 fail_damage: [1, 2] - messages: - - "You approach the corroded ventilation wall..." - - "You find footholds in the rusted panels and begin to climb..." - - "You scramble up the wall and haul yourself onto the ledge!" + 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 - ticks_per_phase: 2 - xp: 8 - fail_damage: [1, 2] - messages: - - "You step onto the narrow coolant pipe..." - - "Arms outstretched, you carefully place one foot in front of the other..." - - "You reach the other side of the pipe and step onto solid ground!" + ... ``` ### CourseConfig Fields | Field | Type | Description | | ---------------- | ------------- | ---------------------------------------------------- | -| `id` | string | Unique course identifier | +| `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 | @@ -45,31 +47,36 @@ 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`) | -| `ticks_per_phase` | float64 | Ticks between each phase message (supports fractional via `engine.ToTicks`) | -| `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) | -| `messages` | []string | Exactly 3 strings for the 3-phase advancement system | +| 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`) | +| `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) | -### 3-Phase Message System +**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. -Each obstacle advances through 3 phases, printing one message per phase: +### ObstaclePhase Fields -``` -Phase 0 (start): messages[0] printed immediately -Phase 1 (middle): messages[1] printed — FAILURE CHECK happens here -Phase 2 (end): messages[2] printed — XP awarded, teleport to next room -``` +| 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 only occurs at phase 1. Success chance is based on Agility level: -- At required level: 70% success (30% fail) -- Each level above reduces fail chance by 1% (down to minimum 5%) -- Fail chance capped at 60% +### Failure -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. +- 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 @@ -115,4 +122,4 @@ exits: | `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. +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. \ No newline at end of file -- cgit v1.2.3