aboutsummaryrefslogtreecommitdiff
path: root/building_guide/objects.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-09 16:09:35 -0400
committerhistoria <[not public]>2026-07-09 16:09:35 -0400
commitb8c90886ef1f3afb8d908aac89028bd177836cae (patch)
tree7e566a7e308e7531bbb7d6a3a39239e789f69a3c /building_guide/objects.md
parentc705ae942573984784ef501bf8198f61f5206ddd (diff)
downloadthehouseoficarus-b8c90886ef1f3afb8d908aac89028bd177836cae.tar.gz
feat(admin): multi-select with shift+drag for common bulk operations
Diffstat (limited to 'building_guide/objects.md')
-rw-r--r--building_guide/objects.md531
1 files changed, 235 insertions, 296 deletions
diff --git a/building_guide/objects.md b/building_guide/objects.md
index db72cf3..cb38e0c 100644
--- a/building_guide/objects.md
+++ b/building_guide/objects.md
@@ -1,90 +1,156 @@
## Objects
-Objects interact with the world through **inline behavior configs** under
-`gather:`, `talk:`, or `use:` keys. There is no separate behavior
-directory — everything goes directly in the object's YAML.
+Objects interact with the world through **inline behavior configs** under `gather:`, `talk:`,
+`use:`, or `safespot:` keys. There is no separate behavior directory — everything goes
+directly in the object's YAML.
### Naming & file organization
-**The filename is the ID.** An object is looked up by its filename stem
-(`copper_rock.yaml` → `copper_rock`), and the loader derives `ObjectDef.ID` from that
-filename. Rooms reference the object by this bare ID regardless of which folder the file
-lives in. **Do not put an `id:` field in the file** — it is ignored. (A nested `- id:` under
-a room's `objects:` list is a different thing: a *reference* to an object by its filename,
-and is still required there.)
-
-- **Generic, shared objects** (rocks, trees, altars, stations) use a semantic name and live
- flat in `data/objects/` — e.g. `data/objects/copper_rock.yaml`.
-- **Room-specific one-offs** (signs, set-dressing, scenery for a single room): if they are
- description-only (the passive subset), prefer defining them **locally in the room** (see
- [Local objects](#local-objects-defined-in-the-room) below). If a one-off needs
- interactable behavior (`gather`/`talk`/`use`/etc.) it must be a file — prefix it with the
- room number and place it in `data/objects/unique/`, e.g. `data/objects/unique/1002_sign.yaml`
- (ID `1002_sign`, referenced from room `1002`).
-
-Loading walks `data/objects/` recursively, so subdirectories are purely organizational and
-need no changes to room references when a file is moved. They do **not** namespace IDs: every
-filename stem must be globally unique across all of `data/objects/`. Use `hidden: true` for
-signage/scenery so it doesn't appear in room listings.
+**The filename is the ID.** An object is looked up by its filename stem (`copper_rock.yaml`
+→ `copper_rock`). Rooms reference the object by this bare ID. Do not put an `id:` field in
+the file — it is ignored. (A nested `- id:` under a room's `objects:` list is a *reference*
+to an object by filename, and is still required.)
+
+- **Generic, shared objects** (rocks, trees, altars, stations) live flat in `data/objects/`.
+- **Room-specific one-offs** (signs, set-dressing) that are description-only should be
+ defined as **local objects** in the room (see below). If a one-off needs interactable
+ behavior (`gather`/`talk`/`use`/etc.) it must be a file — place it in
+ `data/objects/unique/`, e.g. `data/objects/unique/1002_sign.yaml`.
+
+Loading walks `data/objects/` recursively, so subdirectories are purely organizational. All
+filename stems must be globally unique across all of `data/objects/`.
### Local objects (defined in the room)
-Room-specific **description / scenery** objects can be defined directly inside the room's
-`objects:` list instead of as separate files in `data/objects/unique/`. An entry in the list
-is treated as **local** when it carries a `name:` (or any other content field —
-`description`, `aliases`, `inroom_description`, `color`, `hidden`, `on_look`). An entry with
-only `- id: <file>` is a plain **reference** to a file object, as before. `id:` is reserved
-for references; **local objects do not take an `id:`** — their identity comes from the name.
+Room-specific description/scenery objects can be defined directly inside the room's
+`objects:` list instead of as separate files. An entry is **local** when it carries a
+`name:` (or any other content field — `description`, `aliases`, `inroom_description`,
+`color`, `hidden`, `on_look`). An entry with only `- id: <file>` is a plain **reference**:
```yaml
# in data/rooms/intro/1001.yaml
objects:
- id: workbench # reference: resolves to data/objects/workbench.yaml
- - name: window # local: identity derived from the name, no file
+ - name: window # local: identity from name, no file
hidden: true
description: "A thick trim with rounded bolt heads frames the tiny window."
- name: sign
aliases: [safety card, frame]
- inroom_description: A large framed sign is mounted near the cockpit door.
- description: |-
- Conditional or multi-line descriptions work exactly like file objects.
+ inroom_description: "A large framed sign is mounted near the cockpit door."
+ description: "Safety instructions are printed in bold lettering."
on_look:
- set_player_flags:
1001_look_sign: true
```
- **Identity comes from `name`.** Internally the object's id is the name, normalized
- (lowercased, surrounding/redundant spaces trimmed, spaces kept — `"Instrument Panel"` →
- `instrument panel`). You never write an `id:`; doing so on a local entry is ignored and
- warned about at startup.
-- **Identity is room-scoped.** Two different rooms may each have an object named `sign`
- without conflict — no room-number prefixing needed (unlike the old `data/objects/unique/`
- files).
-- **Each object in a room must have a unique name.** Two *distinct* objects in the same room
- with the same exact name — whether both local, both file references, or one of each — is a
- startup **error** (the player could not disambiguate them). Multiple instances of the *same*
- object are still fine via repeated references (e.g. two `- id: copper_rock`).
-- **Partial-name siblings are fine.** `rusty sign` and `shiny sign` can coexist; `look sign`
- matches both and prompts *"which one?"*, while `look rusty sign` resolves directly.
+ (lowercased, spaces kept — `"Instrument Panel"` → `instrument panel`).
+- **Identity is room-scoped.** Two rooms may each have an object named `sign` without
+ conflict.
+- **Each object in a room must have a unique name.** Two distinct objects in the same room
+ with the same name is a startup error.
+- **Partial-name siblings are fine.** `rusty sign` and `shiny sign` can coexist; `look
+ sign` matches both with "which one?", while `look rusty sign` resolves directly.
- Local objects support only the **passive subset**: `name`, `aliases`, `color`, `hidden`,
`inroom_description`, `description` (including conditional variants), and `on_look`.
-- Interactable / stateful behavior (`gather`, `talk`, `use`, `safespot`, `steal`, `guard_mob`,
- `removal_item`, `on_use`, craft stations) **must** be a standalone object file;
- startup validation errors if those appear locally.
+- Interactable behavior (`gather`, `talk`, `use`, `safespot`, `steal`, `guard_mob`,
+ `removal_item`, `on_use`, craft stations) **must** be standalone files; startup validation
+ errors if those appear locally.
- Local objects are re-read from the room file on every access, so edits take effect
immediately (file objects are cached after first load).
Prefer local for one-room flavor; keep generic/reusable/interactable objects as files.
+---
+
+### Hidden objects
+
+Objects with `hidden: true` don't appear in the room's object listing. Players discover them
+by reading room descriptions or trying commands. The object is still fully interactable —
+`push gate`, `look gate`, `hide outcrop` all work.
+
+```yaml
+name: stone lever
+hidden: true
+description: "A cleverly concealed lever behind a loose stone."
+on_use:
+ - condition:
+ global_flag: secret_passage_open
+ not: true
+ message: "You pull the lever. Grinding echoes from the east."
+ action:
+ set_global_flags:
+ secret_passage_open: true
+```
+
+Room description hints at it:
+```yaml
+description: "A dusty corridor. One of the wall stones looks slightly out of place."
+```
+
+#### Aliases
+
+By default an object matches its `name` (word-prefix matching). Add `aliases` for extra
+names players can type:
+
+```yaml
+name: landing craft # matches "landing", "craft", "landing craft"
+aliases: [shuttle, ship] # also matches "shuttle" and "ship"
+hidden: true
+```
+
+If a player's input matches more than one distinct object, `look` lists the candidates
+("That's ambiguous, which one?") — keep aliases specific enough to avoid overlap.
+
+#### Conditional object descriptions
+
+An object's `description` can be a plain string or a list of conditional variants (first
+matching condition wins). If **none** of the variants match, the object is treated as absent
+for that player — `look <it>` reports nothing is there. Combined with `hidden: true`, the
+same object can appear only while a player's flags warrant it (e.g. during an intro) and
+vanish on return visits:
+
+```yaml
+name: crowd of people
+aliases: [people, crowd, passengers]
+hidden: true
+description:
+ - condition: # after the pilot arrives
+ all_of:
+ - player_flag: boarded
+ not: true
+ - player_flag: lined_up
+ text: "The passengers have formed a single-file line."
+ - condition: # before the pilot arrives
+ all_of:
+ - player_flag: boarded
+ not: true
+ - player_flag: lined_up
+ not: true
+ text: "A couple dozen anxious passengers mill about the pad."
+ # once `boarded` is set, no variant matches → the crowd is gone
+```
+
+A bare string in `description:` maps to a single unconditional entry, so a plain object is
+always present.
+
+---
+
+### Gather behavior
+
+Gather behaviors (`gather:`) define mining, fishing, and woodcutting object interactions.
+See the [Gather section of behaviors](behaviors.md#gather-mining-fishing-woodcutting) for
+the complete reference — SuccessFormula, DropEntry, shared depletion, bird's nests, and all
+GatherConfig fields.
+
+Quick example (copper rock):
-Gathering object (mining):
```yaml
name: copper rock
-color: "B2" # xterm-256 color index (00-FF)
+color: "B2"
gather:
skill: mining
- tools:
- - pickaxe
+ tools: [pickaxe]
success:
base: 0.40
per_level: 0.01
@@ -93,51 +159,42 @@ gather:
fail_message: "You chip away but get nothing useful."
drops:
- item_id: copper_ore
- level: 1 # required skill level for this drop
- xp: 17 # XP awarded when this drop lands
+ level: 1
+ xp: 17
weight: 90
depletes: true
- message: "You manage to mine some {B2}copper ore{/}."
+ success_message: "You manage to mine some {B2}copper ore{/}."
- table: gem_table
weight: 10
depletes: false
- message: "You spot a glint of something valuable!"
+ success_message: "You spot a glint of something valuable!"
respawn_timer: 50
respawn_broadcast: "A glint of copper catches your eye from some {name}."
```
-Tree object (woodcutting with shared depletion):
-```yaml
-name: oak tree
-color: "71"
-gather:
- skill: woodcutting
- tools:
- - axe
- success:
- base: 0.40
- per_level: 0.01
- cap: 0.90
- gather_message: "You swing your axe at the oak tree..."
- fail_message: "You swing but get no logs."
- drops:
- - item_id: oak_logs
- level: 15
- xp: 37
- weight: 100
- depletes: false
- message: "You get some {71}oak logs{/}."
- respawn_timer: 14
- deplete_timer: 45
- nest_chance: 256
- respawn_broadcast: "An {name} grows back."
-```
+---
-Color accepts xterm-256 indices with optional modifiers (`bold`, `dim`, `underline`)
-and gradients (`g:C4,52`). In ANSI mode, extended colors downgrade to the nearest
-ANSI color.
+### Talk behavior (conversations)
+
+Objects can host dialog trees via the `talk:` key. This is the same format as talk configs
+on mobs. See the [Talk section of behaviors](behaviors.md#talk-dialog-trees) for the full
+reference — multi-message nodes, randomized messages, conditional nodes, option-level
+actions, and linear auto-advance.
+
+---
+
+### Interactions — on_use / on_look
+
+Objects can define `on_use` and/or `on_look` to react to player actions. Both take a list of
+interactions; entries are checked top-to-bottom, the first whose `condition` passes wins.
+The full `Interaction` shape (condition, message, action with the entire StepAction
+vocabulary) is documented in the [Interaction Reference](behaviors.md#interaction-reference).
+
+#### on_use — using items on objects
+
+`on_use` runs when the player types `use <obj>` (bare) or `use <item> on <obj>`
+(item-specific). Entries with no `item_id` are bare:
-Object interaction (gate, lever — uses `on_use:` key):
```yaml
name: iron gate
hidden: true
@@ -145,7 +202,6 @@ description: "A heavy iron gate set into the north wall."
on_use:
- condition:
global_flag: gate_open
- value: true
not: true
message: "You push the heavy iron gate open."
action:
@@ -153,67 +209,20 @@ on_use:
gate_open: true
```
-Decorative object (no behavior keys — just a name/description):
-```yaml
-name: town fountain
-description: "Clear water sparkles in the sunlight."
-```
-
-Talk object (NPC conversations — uses `talk:` key):
-```yaml
-name: Tool Shed
-description: "A small shed with an open window."
-talk:
- nodes:
- start:
- message: "\"Welcome to the tool shed!\""
- options:
- - text: "\"What do you have?\""
- goto: shop
- - text: "\"Goodbye.\""
-```
-
-On-look interaction (runs when a player examines an object with `look <name>`):
-```yaml
-name: sign
-aliases: [notice, board]
-description: "A wooden signpost with faded writing."
-on_look:
- - set_player_flags:
- read_sign: true
-```
-`on_look` is a list of interactions (same shape as `on_use`). The first entry
-whose `item_id` and `condition` pass wins, and fires AFTER the object's
-description is shown. An entry with an `item_id` only fires if the player
-currently carries that item in their inventory (empty `item_id` = always
-fires on look). It uses the same `interaction` shape as `on_use` — supports
-`condition`, `message`, and a full `action` (`set_global_flags`,
-`set_player_flags`, `give_item`, `take_item`, `teleport`, `heal`, `credits`,
-`aps_node`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`,
-`delay`), per the [interaction reference](behaviors.md#interaction-reference).
-
-See the [Talk section of behaviors](behaviors.md#talk-dialog-trees) for the full
-dialog tree format. See [Stealable Objects](#stealable-objects) for theft mechanics.
-
----
-
-## Interactions (On Use / On Look)
-
-Objects can define `on_use` and/or `on_look` to react to player actions. Both take a
-list of interactions (entries are checked top-to-bottom; the first whose
-`condition` passes wins — one fires per use/look).
+Item-specific:
-Simple message (no action):
```yaml
-name: anvil
+name: bookshelf
on_use:
- - item_id: silver_bar
- message: "You should use this with a mold at a furnace."
- - item_id: gold_bar
- message: "You should use this with a mold at a furnace."
+ - item_id: dusty_tome
+ message: "The bookshelf slides aside, revealing a secret passage!"
+ action:
+ set_global_flags:
+ secret_passage_open: true
```
-Puzzle interaction (take item, set flag):
+Puzzle interaction:
+
```yaml
name: crystal slot
on_use:
@@ -222,7 +231,7 @@ on_use:
global_flag: crystal_inserted
message: "The crystal key is already in the slot."
- item_id: crystal_key
- message: "You insert the crystal key into the slot. It clicks into place."
+ message: "You insert the crystal key. It clicks into place."
action:
take_item: crystal_key
set_global_flags:
@@ -230,6 +239,7 @@ on_use:
```
Quest item exchange:
+
```yaml
on_use:
- item_id: ancient_scroll
@@ -244,55 +254,24 @@ on_use:
temple_door_open: true
```
-### Interaction Fields
-
-| Field | Type | Description |
-| ----------- | ---------- | --------------------------------------------------------------- |
-| `item_id` | string | `on_use`: required item id (empty = bare `use <object>`). `on_look`: only fires if you carry this item. (Mobs' `on_kill`: only fires if you wield this weapon in main_hand or off_hand.) |
-| `condition` | Condition | Optional condition (same as exits/talk/on_enter/triggers) |
-| `message` | string | Message shown to the player when this entry fires |
-| `action` | StepAction | Optional actions (see [interaction reference](behaviors.md#interaction-reference)) |
-
-### Available Actions (same superset as on_enter steps/triggers/talk)
-
-| Field | Type | Description |
-| ------------------ | -------------- | ----------------------------------------------------- |
-| `set_global_flags` | map[string]any | Set global flags (shared by all players) |
-| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML) |
-| `give_item` | string | Give an item to inventory (1 unit) |
-| `take_item` | string | Remove an item from inventory (1 unit) |
-| `teleport` | int | Move player to a room ID |
-| `heal` | int | Restore hitpoints (clamped to MaxHP) |
-| `credits` | int | Add (positive) or deduct (negative) credits |
-| `aps_node` | bool | Mark current room as a discovered APS node |
-| `broadcast` | string | Announce to all in the room (uses `%p`, `%v`) |
-| `broadcast_global` | string | Announce to all online (uses `%p`, `%v`) |
-| `spawn_mob` | string/map | Spawn a transient mob (`id` or full [config](behaviors.md#spawn-mob-config)) |
-| `despawn_mob` | string | Despawn all transient mobs of this id |
-| `message` | string | Direct message to the player (also valid here) |
-| `delay` | int | Ticks to wait before this step (only meaningful in on_enter/triggers) |
-
-### Available Conditions (same as exits/talk/on_enter/triggers)
-
-| Field | Description |
-| ------------- | ------------------------------------------------------- |
-| `global_flag` | Global flag check |
-| `player_flag` | Per-character flag check |
-| `has_item` | Inventory item check |
-| `min_credits` | Minimum credits |
-| `value` | Expected value for flag checks |
-| `not` | Invert the condition |
-| `all_of` | All sub-conditions must pass |
-| `any_of` | Any sub-condition must pass |
+#### on_look — actions when examining an object
+
+`on_look` fires AFTER the object's description is shown. An entry with `item_id` only fires
+if the player carries that item. Supports the full Interaction shape:
+
+```yaml
+name: sign
+on_look:
+ - set_player_flags:
+ read_sign: true
+```
---
-## Stealable Objects
+### Stealable objects
-Objects can be stealable via the `steal` command. These use drop entries — either
-item IDs or drop table references — resolved by weighted pick. The `steal` command
-supports auto-selection when only one target is available, or `steal <name>` for
-explicit targeting, including `steal <n>.<name>` to disambiguate identical defs.
+Objects can be stealable via the `steal` command. These use drop entries — item IDs or drop
+table references — resolved by weighted pick:
```yaml
name: Market Stall
@@ -305,33 +284,28 @@ steal:
xp: 12
speed: 5
guard_mob: guard
+ guard_range: 2
```
-| Field | Description |
-| ---------------- | ------------------------------------------------ |
-| `steal.drops` | Array of drop entries (item_id + weight, or table + weight) |
-| `steal.level` | Required thieving level |
-| `steal.xp` | XP awarded per successful steal |
-| `steal.speed` | Ticks per steal attempt (base wait) |
-| `steal.guard_mob`| Mob def ID that guards this object (watches it) |
-
-Each drop entry:
-| Field | Description |
-| ------------ | ------------------------------------------------ |
-| `item_id` | Item ID (mutually exclusive with `table`) |
-| `table` | Drop table ID to sub-resolve (mutually exclusive with `item_id`) |
-| `weight` | Weight in the weighted-pick pool |
-| `quantity` | Override quantity (0 = use sub-item's own qty for tables, or 1 for items) |
+| Field | Description |
+|-------|-------------|
+| `steal.drops` | Array of drop entries (item_id + weight, or table + weight) |
+| `steal.level` | Required thieving level |
+| `steal.xp` | XP awarded per successful steal |
+| `steal.speed` | Ticks per steal attempt (base wait) |
+| `steal.guard_mob` | Mob def ID that guards this object |
+| `steal.guard_range` | Room range within which the guard watches (default 0 = same room only) |
-When `guard_mob` is set, a mob with that def ID in the same room watches the object
-on a tick-based cycle (8 ticks watching, 4 ticks looking away). While the guard is
-watching, steal success chance is halved and failures trigger a confrontation dialog.
+When `guard_mob` is set, a mob with that def ID in the same room watches the object on a
+tick-based cycle (8 ticks watching, 4 ticks looking away). While the guard is watching,
+steal success chance is halved and failures trigger a confrontation dialog. Use the `sneak`
+command to see guard watch state changes in real time.
-Use the `sneak` command to see guard watch state changes in real time.
+Mobs can also be stealable (see `mobs.md`).
---
-## Safespots
+### Safespots
Safespot objects provide cover that blocks melee attacks. Players use `hide <object>` to
crouch behind them and attack with ranged or science weapons. Melee attacks force the player
@@ -344,7 +318,7 @@ hidden: true
inroom_description: "A jagged rock outcrop juts from the floor."
description: "A large, jagged rock formation providing natural cover."
safespot:
- tier: 2
+ tier: 1
max_block_size: large
max_occupants: 3
unsafe_chance: 0.008
@@ -356,65 +330,67 @@ safespot:
- message: "The rock formation stands solid."
degrade_message: "The rock structure begins crumbling!"
- message: "The rock outcrop is cracked and worn."
- degrade_message: "A few jagged bits are all that's left of the rock outcrop!"
+ degrade_message: "A few jagged bits are all that's left!"
- message: "Only a few jagged rocks remain."
degrade_message: "The rock outcrop crumbles to nothing!"
```
Safespot objects should be `hidden: true`. They never appear in room descriptions — players
-discover them through quest guidance, `inroom_description` text in the room's YAML, or by
-examining objects directly with `look <name>`.
+discover them through room text hints, quest guidance, or by trying `look <name>`. Safespots
+only become visible in `look` when the player's effective safespot tier meets the object's
+`safespot.tier` requirement. Gaining tier through quest completion and achievements
+gradually reveals new coverage:
-### SafespotConfig Fields
-
-| Field | Type | Default | Description |
-|---|---|---|---|
-| `tier` | int | 0 | Required safespot tier to use this object |
-| `max_block_size` | string | `""` (all) | Largest mob size blocked: small/medium/large/massive |
-| `max_occupants` | int | 0 | Max players that can hide behind this object (0 = unlimited) |
-| `unsafe_chance` | float64 | 0 | Per-tick chance (0.0-1.0) to be forced out of cover |
-| `decay_ticks` | float64 | 0 | Guaranteed ticks before object degrades one level |
-| `decay_chance` | float64 | 0 | Per-tick random chance (0.0-1.0) to degrade one level |
-| `respawn_on_hide` | bool | false | If true, hiding behind an empty safespot resets it to max level. Only triggers when no occupants are present. |
-| `respawn_ticks` | float64 | 0 | Ticks until a destroyed safespot respawns (0 = never respawns — object is deleted from the room) |
-| `levels` | []SafespotLevel | required | At least one level entry |
+| Flag | Source | Tier bonus |
+|------|--------|------------|
+| `quest_animal_magnetism` | Quest | +1 |
+| `quest_dragon_slayer` | Quest | +1 |
+| `achieve_medium_combat` | Achievement | +1 |
+| `achieve_hard_combat` | Achievement | +2 |
-### SafespotLevel Fields
+#### SafespotConfig fields
-| Field | Type | Description |
-|---|---|---|
-| `message` | string | Shown to the player when the safespot is at this level |
-| `degrade_message` | string | Broadcast to the room when the safespot degrades FROM this level |
+| Field | Default | Description |
+|-------|---------|-------------|
+| `tier` | 0 | Required safespot tier to use this object |
+| `max_block_size` | `""` (all) | Largest mob size blocked: small/medium/large/massive |
+| `max_occupants` | 0 (unlimited) | Max players behind this object |
+| `unsafe_chance` | 0 | Per-tick chance (0.0–1.0) to be forced out of cover |
+| `decay_ticks` | 0 | Guaranteed ticks before degrading one level |
+| `decay_chance` | 0 | Per-tick random chance (0.0–1.0) to degrade |
+| `respawn_on_hide` | false | First occupant resets safespot to max level |
+| `respawn_ticks` | 0 | Ticks until destroyed safespot respawns (0 = deleted) |
+| `levels` | required | At least one SafespotLevel entry |
-Levels are ordered from highest to lowest. The first entry is the safespot's best state;
-the last entry is its weakest state before destruction. When the last level degrades,
-the safespot is destroyed.
+#### SafespotLevel fields
-### Three Safespot Patterns
+| Field | Description |
+|-------|-------------|
+| `message` | Shown to the player when safespot is at this level |
+| `degrade_message` | Broadcast to room when safespot degrades FROM this level |
-The combination of `respawn_on_hide` and `respawn_ticks` creates three distinct use cases:
+#### Three safespot patterns
-**1. Standard Safespot (always available)**
+**Standard safespot** — always available, permanent:
```yaml
safespot:
+ tier: 1
+ max_block_size: small
respawn_on_hide: true
respawn_ticks: 0
levels:
- - message: "You crouch behind the rock formation, using it as cover."
+ - message: "You crouch behind the fence post, using it as cover."
```
-The safespot starts at full health each time a player hides behind it (first occupant only —
-subsequent players don't reset the level). Ideal for skilling areas and general exploration.
-Since `respawn_ticks: 0`, a destroyed safespot would be deleted, but with a single level and
-no decay, it never degrades. Simple and permanent.
-
-**2. Regenerating Safespot (timer-based respawn)**
+**Regenerating safespot** — degrades across hides, respawns after destruction:
```yaml
safespot:
respawn_on_hide: false
respawn_ticks: 300
+ decay_ticks: 500
+ decay_chance: 0.01
levels:
- message: "The rock formation stands solid."
degrade_message: "The rock structure begins crumbling!"
@@ -424,12 +400,7 @@ safespot:
degrade_message: "The rock outcrop crumbles to nothing!"
```
-Degradation persists between hides — a player who leaves and re-hides finds the
-safespot at its previous level. After total destruction, a timer (`respawn_ticks`)
-counts down, then the safespot reforms at full health. Suits areas with heavy use
-where a temporary respite is valuable.
-
-**3. Disposable Safespot (boss fight cover)**
+**Disposable safespot** — permanent degradation, deleted on destruction:
```yaml
safespot:
@@ -442,57 +413,25 @@ safespot:
degrade_message: "The barricade shatters completely!"
```
-Degradation persists between hides. When the last level degrades, the safespot is
-**deleted from the room entirely** — it does not respawn. Perfect for boss encounters
-where a room script places temporary cover that the party consumes during the fight.
-Once destroyed, it's gone for good (until the boss room re-instantiates it).
+#### Mob Size
-### Mob Size
+Mobs must have a `size` field for safespot blocking to work (small < medium < large <
+massive, default medium). A safespot with `max_block_size: large` blocks mobs of size small,
+medium, and large, but not massive.
-Mobs must have a `size` field for safespot blocking to work:
+#### Combat integration
-```yaml
-# data/mobs/cow.yaml
-size: small
-
-# data/mobs/moss_giant.yaml
-size: large
-```
-
-Size ordering: `small` < `medium` < `large` < `massive`. Defaults to `medium` when absent.
-A safespot with `max_block_size: large` blocks mobs of size small, medium, and large,
-but not massive.
-
-### Progression
-
-Players gain safespot tier by completing quests and combat achievements, which set
-player flags via talk node actions:
-
-| Flag | Source | Tier bonus |
-|---|---|---|
-| `quest_animal_magnetism` | Quest completion | +1 |
-| `quest_dragon_slayer` | Quest completion | +1 |
-| `achieve_medium_combat` | Combat achievement | +1 |
-| `achieve_hard_combat` | Combat achievement | +2 |
-
-### Combat Integration
-
-- A safespot blocks a mob's **melee** attacks if the mob's size is within the
- safespot's `max_block_size`.
-- When a mob's melee is blocked, if it also has a `ranged`/`science` attack type it
- switches to its strongest ranged/science attack (by max hit) and keeps attacking.
- A mob with only melee is fully blocked (cannot aggro or land hits).
-- Mobs whose size exceeds `max_block_size` are never blocked and always use melee.
-- If a safespotted player attacks with melee, they automatically leave cover.
-- Hiding in combat takes 4 ticks; if the mob hits during this time, the hide fails.
+- A safespot blocks melee attacks from mobs within `max_block_size`.
+- When a mob's melee is blocked but it has a ranged/science attack type, it switches to its
+ strongest ranged/science attack.
+- Mobs exceeding `max_block_size` are never blocked and always use melee.
+- Melee attacks from a safespotted player force them out of cover.
+- Hiding in combat takes 4 ticks; taking a hit during this cancels the hide.
- Hiding out of combat takes 1 tick.
+- Safespots block all hazard damage (see `hazards.md`).
-### Option: safespot_alert
+#### Option: safespot_alert
-Players can customize the message shown when forced out of a safespot:
+Players can customize the alert message when forced out of cover:
option safespot_alert "{C4 bold}** DANGER **{/} Cover blown!"
-Default: `"{C4 bold}** Your safespot has been compromised! **{/}"`
-
----
-