From b8c90886ef1f3afb8d908aac89028bd177836cae Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 9 Jul 2026 16:09:35 -0400 Subject: feat(admin): multi-select with shift+drag for common bulk operations --- AGENTS.md | 200 ------- building_guide/behaviors.md | 605 +++++++++------------ building_guide/conditions.md | 83 --- building_guide/construction.md | 307 +++++------ building_guide/courses.md | 1 + building_guide/doors.md | 93 ---- building_guide/hidden_objects.md | 92 ---- building_guide/items.md | 60 +- building_guide/mobs.md | 7 + building_guide/objects.md | 531 ++++++++---------- building_guide/quests.md | 15 +- building_guide/recipes.md | 215 ++++---- building_guide/rooms.md | 462 ++++++++++++---- building_guide/search.md | 15 - building_guide/state.md | 13 - building_guide/tips.md | 10 +- building_guide/triggers.md | 23 +- building_guide/wandering_mobs.md | 20 - building_guide/wandering_objects.md | 37 -- data/.admin_history.json | 2 +- data/courses/reactor.yaml | 96 ---- data/courses/rooftop.yaml | 83 --- data/courses/testcourse.yaml | 38 ++ data/courses/vent_shaft.yaml | 70 --- data/rooms/example_unused/agility2/99999200.yaml | 13 - data/rooms/example_unused/agility2/99999201.yaml | 28 - data/rooms/example_unused/agility2/99999202.yaml | 26 - data/rooms/example_unused/agility2/99999203.yaml | 26 - data/rooms/example_unused/agility2/99999204.yaml | 26 - data/rooms/example_unused/agility2/99999205.yaml | 26 - data/rooms/example_unused/agility2/99999206.yaml | 12 - data/rooms/example_unused/agility2/99999210.yaml | 26 - data/rooms/example_unused/agility2/99999211.yaml | 26 - data/rooms/example_unused/agility2/99999212.yaml | 26 - data/rooms/example_unused/agility2/99999213.yaml | 26 - data/rooms/example_unused/agility2/99999214.yaml | 26 - data/rooms/example_unused/agility2/99999215.yaml | 26 - data/rooms/example_unused/agility2/99999220.yaml | 26 - data/rooms/example_unused/agility2/99999221.yaml | 26 - data/rooms/example_unused/agility2/99999222.yaml | 26 - data/rooms/example_unused/agility2/99999223.yaml | 26 - data/rooms/example_unused/agility2/99999224.yaml | 26 - data/rooms/example_unused/agility2/99999225.yaml | 26 - data/rooms/example_unused/agility2/99999226.yaml | 26 - data/rooms/example_unused/town/9999915.yaml | 2 + data/rooms/example_unused/town/9999916.yaml | 6 +- data/rooms/example_unused/town/9999917.yaml | 4 +- data/rooms/example_unused/town/9999918.yaml | 21 +- data/rooms/example_unused/town/9999919.yaml | 8 - data/rooms/example_unused/town/9999920.yaml | 19 +- data/rooms/example_unused/town/9999921.yaml | 13 - data/rooms/example_unused/town/9999930.yaml | 13 - .../rooms/example_unused/underground2/9999950.yaml | 14 - .../rooms/example_unused/underground2/9999951.yaml | 16 - .../rooms/example_unused/underground2/9999952.yaml | 15 - .../rooms/example_unused/underground2/9999953.yaml | 17 - .../rooms/example_unused/underground2/9999954.yaml | 11 - .../rooms/example_unused/underground2/9999955.yaml | 18 - .../rooms/example_unused/underground2/9999956.yaml | 10 - .../rooms/example_unused/underground2/9999957.yaml | 9 - .../rooms/example_unused/underground2/9999958.yaml | 10 - .../rooms/example_unused/underground2/9999959.yaml | 10 - .../rooms/example_unused/underground2/9999960.yaml | 10 - .../rooms/example_unused/underground2/9999961.yaml | 8 - .../rooms/example_unused/underground2/9999962.yaml | 10 - .../rooms/example_unused/underground2/9999963.yaml | 10 - .../rooms/example_unused/underground2/9999964.yaml | 10 - .../rooms/example_unused/underground2/9999965.yaml | 14 - .../rooms/example_unused/underground2/9999966.yaml | 14 - .../rooms/example_unused/underground2/9999967.yaml | 14 - .../rooms/example_unused/underground2/9999968.yaml | 14 - data/rooms/intro/2001.yaml | 20 +- data/rooms/intro/2002.yaml | 14 +- data/rooms/intro/2003.yaml | 12 +- data/rooms/intro/2004.yaml | 12 +- data/rooms/intro/2005.yaml | 32 +- data/rooms/intro/2006.yaml | 5 +- data/rooms/test222/1.yaml | 13 - internal/admin/api_map.go | 26 +- internal/admin/api_rooms_bulk.go | 214 ++++++++ internal/admin/server.go | 1 + internal/admin/static/admin.css | 4 + internal/admin/static/map.js | 347 +++++++++++- internal/admin/undo.go | 97 +++- 84 files changed, 1922 insertions(+), 2768 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 building_guide/conditions.md delete mode 100644 building_guide/doors.md delete mode 100644 building_guide/hidden_objects.md delete mode 100644 building_guide/search.md delete mode 100644 building_guide/state.md delete mode 100644 building_guide/wandering_mobs.md delete mode 100644 building_guide/wandering_objects.md delete mode 100644 data/courses/reactor.yaml delete mode 100644 data/courses/rooftop.yaml create mode 100644 data/courses/testcourse.yaml delete mode 100644 data/courses/vent_shaft.yaml delete mode 100644 data/rooms/example_unused/agility2/99999200.yaml delete mode 100644 data/rooms/example_unused/agility2/99999201.yaml delete mode 100644 data/rooms/example_unused/agility2/99999202.yaml delete mode 100644 data/rooms/example_unused/agility2/99999203.yaml delete mode 100644 data/rooms/example_unused/agility2/99999204.yaml delete mode 100644 data/rooms/example_unused/agility2/99999205.yaml delete mode 100644 data/rooms/example_unused/agility2/99999206.yaml delete mode 100644 data/rooms/example_unused/agility2/99999210.yaml delete mode 100644 data/rooms/example_unused/agility2/99999211.yaml delete mode 100644 data/rooms/example_unused/agility2/99999212.yaml delete mode 100644 data/rooms/example_unused/agility2/99999213.yaml delete mode 100644 data/rooms/example_unused/agility2/99999214.yaml delete mode 100644 data/rooms/example_unused/agility2/99999215.yaml delete mode 100644 data/rooms/example_unused/agility2/99999220.yaml delete mode 100644 data/rooms/example_unused/agility2/99999221.yaml delete mode 100644 data/rooms/example_unused/agility2/99999222.yaml delete mode 100644 data/rooms/example_unused/agility2/99999223.yaml delete mode 100644 data/rooms/example_unused/agility2/99999224.yaml delete mode 100644 data/rooms/example_unused/agility2/99999225.yaml delete mode 100644 data/rooms/example_unused/agility2/99999226.yaml delete mode 100644 data/rooms/example_unused/town/9999919.yaml delete mode 100644 data/rooms/example_unused/town/9999921.yaml delete mode 100644 data/rooms/example_unused/town/9999930.yaml delete mode 100644 data/rooms/example_unused/underground2/9999950.yaml delete mode 100644 data/rooms/example_unused/underground2/9999951.yaml delete mode 100644 data/rooms/example_unused/underground2/9999952.yaml delete mode 100644 data/rooms/example_unused/underground2/9999953.yaml delete mode 100644 data/rooms/example_unused/underground2/9999954.yaml delete mode 100644 data/rooms/example_unused/underground2/9999955.yaml delete mode 100644 data/rooms/example_unused/underground2/9999956.yaml delete mode 100644 data/rooms/example_unused/underground2/9999957.yaml delete mode 100644 data/rooms/example_unused/underground2/9999958.yaml delete mode 100644 data/rooms/example_unused/underground2/9999959.yaml delete mode 100644 data/rooms/example_unused/underground2/9999960.yaml delete mode 100644 data/rooms/example_unused/underground2/9999961.yaml delete mode 100644 data/rooms/example_unused/underground2/9999962.yaml delete mode 100644 data/rooms/example_unused/underground2/9999963.yaml delete mode 100644 data/rooms/example_unused/underground2/9999964.yaml delete mode 100644 data/rooms/example_unused/underground2/9999965.yaml delete mode 100644 data/rooms/example_unused/underground2/9999966.yaml delete mode 100644 data/rooms/example_unused/underground2/9999967.yaml delete mode 100644 data/rooms/example_unused/underground2/9999968.yaml delete mode 100644 data/rooms/test222/1.yaml create mode 100644 internal/admin/api_rooms_bulk.go diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 3da4260..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,200 +0,0 @@ -# AGENTS.md - -The House of Icarus — a sci-fi MUD in Go. YAML-driven, no database. "Technology" replaces Prayer, "Science" replaces Magic, Scavenging replaces Runecrafting. - -## Build & Run - -``` -make build # go build -o thoi ./cmd/thoi -make run # build + run with DATA_DIR=./data -make test # DATA_DIR=$(PWD)/data go test ./... -timeout 60s -make vet # go vet ./... -``` - -Binary accepts `--config ` (defaults to `config.yaml`). Dependencies: `gopkg.in/yaml.v3`, `github.com/gorilla/websocket`. - -## Prefixes - -File prefixes in `internal/game/`: `game.go/cmd_registry.go/tick.go` (core), `core_*` (infra), `act_*` (actions), `cmd_*` (commands), `sys_*` (subsystems), `render_*` (output), `production_*` (crafting engine), `combat_*` (hits), `look_*` (display). - -## Key Conventions - -**YAML-driven.** Rooms, items, objects, mobs, drops, modules, courses under `data/`. Read from disk on every access — edit and it takes effect immediately. **The filename is the ID** for every data type: lookup by filename stem. **Do not put a top-level `id:` field** (ignored). A nested `- id:` under a room's `objects:`/`mobs:` is a *reference* and is still required. Subdirectories are organizational only. **Local objects:** a room `objects:` entry that carries content fields (`name`/`description`/`aliases`/`inroom_description`/`color`/`hidden`/`on_look`) is a fully local, room-scoped object def instead of a reference — passive subset only (no gather/talk/use/safespot/steal/guard_mob/on_use). Local identity derives from the normalized `name` (lowercased, whitespace-collapsed, spaces kept); `id:` is reserved for references and ignored+warned on local objects. Each object in a room must have a unique name (startup error on collision, incl. local-name vs referenced file id); names may repeat across different rooms. - -**Live state.** Player data persisted to YAML on every change. Ground items, mob instances, object states are in-memory only. - -**No database.** Everything flat YAML. Passwords use sha256 + 16-byte random salt. - -**Transport-agnostic.** `Conn` interface (`internal/net/conn.go`) — telnet and websocket. Game code works with `*Session`. - -**Tick-driven.** 600ms default (min 50ms). All game advancement per tick. YAML timer fields accept `float64`; `engine.ToTicks(base)` probabilistically rounds. - -**Concurrency.** Every state package uses its own `sync.Mutex`. Pattern: lock → snapshot/unmarshal → unlock → process. Engine creates subscriber snapshot each tick. - -## Two State Systems - -- **Global flags** (`set_global_flags` / `global_flag`): Shared by all players. -- **Player flags** (`set_player_flags` / `player_flag`): Per-character, saved to YAML. - -## Room Scripting - -- **`on_enter` steps** (`[]behavior.StepAction`): message + optional `condition` + `delay` + any of the shared `StepAction` effects (`set_global_flags`/`set_player_flags`, `give_item`/`take_item`, `teleport`, `heal`, `credits`, `aps_node`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`). Timed steps (any non-zero field triggers the timed path) run as scheduled enter sequences; otherwise messages print synchronously. Conditions snapshotted once at entry. Resumes on reconnect if interrupted. -- **Conditional descriptions** (`description: [{text, condition}]`): first passing variant wins. For objects, if no variant matches the object is absent for that player. -- **Exits** carry an `on_traverse:` list of `behavior.Interaction` (same shape as on_use) — first passing entry fires AFTER `p.RoomID` is updated to the destination, so `aps_node`/`teleport` resolve against the new room. `ExitDef.Condition` still gates whether the exit is passable at all; each `on_traverse` entry has its own additional `Condition`. - -## Data Files - -``` -data/rooms/.yaml data/items/.yaml data/mobs/.yaml -data/objects/.yaml data/objects/unique/ data/drops/.yaml -data/hazards/.yaml data/help/.yaml data/modules/.yaml -data/techs/.yaml data/courses/.yaml data/players/accounts|characters/ -``` - -Wander config per-instance in room YAML, not on MobDef. See `building_guide/` for full YAML format reference. - -## Map & Colors - -**Color specs are hex** (`00`–`FF`, case-insensitive) — the notation for the xterm-256 palette index (internally still `0–255`; only the string form is hex). Applies everywhere `color.Parse` runs: `color:` fields (rooms/items/objects), the `color`/`symbol` commands, `bg:`/`g:` tokens, and inline `{spec}text{/}` tags. Raw `ColorSpec{Fg:int}` literals stay decimal indices. `color` command targets live in `colorCategoryOrder` (`cmd_color.go`) + `config.DefaultColors()`; `map_at` (the `@`) and `map_blocked` (the blocked `X`) are themeable. - -**Map rendering** (`render_map.go`): BFS lays rooms on a 2D grid (NSEW move, up/down don't). Rooms carry an optional `color:` field (player `symbol` color overrides it). Links between two grid-adjacent rooms render per-direction: two-way → bar (`-`/`│`), one-way → arrow (`← ↑ → ↓` / `< ^ > v` pointing along travel), no traversable direction → blocked `X` (`map_blocked`). Bars/arrows blend the two endpoints' colors (`color.Average`); only `X` uses the blocked color. A player's custom symbol also shows in dim brackets on the look title. - -## Startup Validation - -Comprehensive integrity checks on all data files (errors logged, server still starts). Duplicate ID checks across all directories. Referential integrity: exits→rooms, mobs/objects/spawns→defs, drops/steal→items/tables, craft blocks→items/stations/tools, etc. Configurable orphan-room reachability check (`startup_validation.root_rooms`). Grid-overlap check: starting from `root_rooms`, lays each horizontal plane (NSEW only; up/down seed new planes) on a 2D grid and errors when two rooms collide on one cell (overlap) or a room maps to two cells (twist). - -## Command Dispatch - -`handleGameCommand()` in `game.go`: expands account aliases → split input → `classifyCommand()` → `ClassInstant` (executes now + prompt), `ClassFree` (next tick queue), `ClassActive` (sorted timestamp queue), `ClassUnknown` (verbAliases/obstacleVerbs, else error). - -## Tick-Based Action Queue - -`ProcessQueuedCommands()`: clear stale one-tick actions → execute free commands → execute active commands (sorted, first-queued wins conflicts) → advance walks → flush shared depletions. `ActionState` tracks player activity for `look`. Queue feedback shown by `show_queued_cmds`. - -## Action System - -Actions route through `startAction()` in `act.go`: normalize verb → resolve target (objects then mobs) → load behavior → route to handler. - -**Behavior types (YAML-driven):** `gather` (mine/chop/cut/fish), `use` (crafting stations), `talk` (NPC dialog). **Hardcoded actions:** burn/stoke, search, production (cook/smelt/smith/craft/mix/fletch/clean), steal, agility, farm, identify, finishing blow. Gather behaviors use SuccessChance = Base + (level-required)*PerLevel, clamped `[0, Cap]`. Action types and all YAML config fields are self-documenting in the source (`act_state.go`, `behavior.go`, `item/item.go`). - -### Interactions (unified conditional-action model) - -`behavior.Interaction` (struct in `behavior/behavior.go`) is the shared per-entry shape for every "when X happens, do Y with optional condition" feature in the game: - -- **On Use** (`on_use:` on objects) — fires on `use ` (bare, no `item_id`) or `use on ` (item-keyed). Triggered in `cmd_use.go`; queued as a one-tick action; tick executor in `act_use_interaction.go`. -- **On Look** (`on_look:` on objects) — fires after the object's description is shown on `look `. Triggered in `look_target.go`; runs the first passing entry inline (no queue). An entry with an `item_id` only fires if the player currently carries that item in their inventory. -- **On Kill** (`on_kill:` on mobs) — fires (additively over standard loot) when a combat kill of the mob completes OR when a `kind: task` mob's work drains its HP to zero (the "completion" of the task). Triggered from `endCombat` in `combat_mob.go`. An entry with an `item_id` only fires if the player is wielding that item in a weapon-hand slot (main_hand or off_hand) at the moment of the kill. -- **Exit On Traverse** (`on_traverse:` on exits) — fires AFTER `p.RoomID` is set to the destination, so `aps_node`/`teleport` resolve against the new room. Stashed in `Player.MovePendingInteraction` at move-classify time, applied in `completeMove` (`cmd_move.go`). -- **On Enter steps** (`on_enter:` on rooms) — list of `behavior.StepAction`; each step has its own `Condition` + `Message`/`Delay` + full effect set. Reconnect-resume. Driven by `enterSeq`/`EnterSeqTick` in `act_room.go`. -- **Trigger steps** (`steps:` on room/global `TriggerDef`) — same `StepAction` per step, newly supports per-step `Condition`. Trigger watcher + async playback in `sys_triggers.go` / `trigger_store.go`. The event-listener / cascade / re-entry-guard machinery is separate from interactions' inline dispatch. -- **Talk** — `TalkNode.Action` and `TalkOption.Action` are `*NodeAction` (inline subset). Wrap via `handleNodeAction` → `applyNodeAction`. - -Each list of Interactions is walked top-to-bottom; the **first** entry whose item filter and `Condition` pass wins and fires (one entry per triggering event). `applyInteraction` (`act_effects.go`) is the convenience wrapper for that walk + fire; individual callers can loop directly. The item filter is interaction-kind-specific: on_use matches the held item, on_look requires `p.HasItem`, on_kill requires `p.IsWielding`; nil disables the filter (exit traversal, on_traverse). - -**Effect superset — `behavior.StepAction`.** The universal effect container embedded by all interaction `action` fields. Holds the inline-only `NodeAction` fields (`set_global_flags`, `set_player_flags`, `give_item`, `take_item`, `teleport`, `heal`, `credits`, `aps_node`) PLUS the sequence-only fields (`message`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, `delay`) and a per-step `Condition`. Inline callers (use/look/kill/talk) ignore the sequence-only fields; on_enter / trigger steps use them. - -**One executor — `applyStepAction(sess, p, step, roomID, scope, flagValue)` in `act_effects.go`.** Scopes: `scopePlayer` (inline: flags + items + heal + credits + teleport + aps_node + broadcast), `scopePlayerSeq` (on_enter / trigger player seq: above + broadcast_global + spawn_mob + despawn_mob), `scopeGlobal` (trigger global seq: broadcasts + global flags + world-owned spawn/despawn only). `applyNodeAction` (`act_talk.go`) is a thin wrapper passing a NodeAction-wrapped StepAction at scopePlayer; `fireEnterStep` / `executeTriggerStep` / `executeGlobalTriggerStep` are thin wrappers at scopePlayerSeq / scopeGlobal. The old duplicate executors (`applyNodeAction` body, `fireEnterStep` body, `executeTriggerStep`, `executeGlobalTriggerStep`) collapsed into one in `act_effects.go`. - -`aps_node` retains its dedicated bool field — it stamps `aps_node_: true` on the player, which the `aps` command (`cmd_aps.go`) reads to build the datapad. (The room-id is dynamic, so it can't be expressed as a static `set_player_flags` entry without template substitution.) `reputation_cost` was removed (assassin reputation shop is not yet wired up). - -### Condition System - -Used by exits (exit gate + `on_traverse`), talk options/nodes, on-enter steps, on_use / on_look (objects), on_kill (mobs), trigger steps: `global_flag` (global), `player_flag` (per-char), `value` (exact match), `has_item`, `min_credits`, `all_of`, `any_of`, `not`. Bare global_flag/player_flag passes on truthy. - -## Two Depletion Mechanics - -- **Per-drop** (`depletes: true`): depletes on first successful gather of that drop. -- **Shared** (`deplete_timer: `): timer counts down while anyone gathers; at 0, next gather depletes for all. Regenerates when idle. - -## Ground Item System - -`DropDespawnTicks = 1000`. `ReserveTicks = 100` (mob loot reserved for killer). Same-ID items NOT merged. Spawn items respawn with merge. Despawn/reserve visible via options. - -## Drop Resolution (`behavior/store.go`) - -Drop tables are skill-independent weighted lists of items. `ResolveDrop` does one weighted pick; if the picked entry is a `table:` ref, its `quantity` yields that many copies of a single sub-roll (used by gather/search/steal). `ResolveDropList` (mob loot only, `combat_mob.go`) instead treats a picked table entry's `quantity` as the number of **independent rolls** against the sub-table (each may differ), returning one entry per roll; a plain item entry yields one result. Mob loot is still a single weighted pick per kill — a table entry just expands into N sub-rolls. - -## Shop System (`cmd_shop.go`) - -Root-level `shop:` on a MobDef (`behavior.ShopConfig`) — NOT part of the talk tree. Traded via plain `StateGame` commands: `list`, `buy `, `sell `, with `list ` / `buy from ` / `sell to ` to disambiguate when a room has multiple shop mobs (else "Which shop?"). No bespoke shop CLI/session state. **Prices derive from item `value:`** — shops sell at 100% of value; buy back via OSRS formula `value×max(10, buy_percentage−stock×change_percentage)/100` (defaults `buy_percentage: 40`, `change_percentage: 3`, floor 10%). Per-item `stock` (target+starting); buying depletes (out of stock at 0), selling raises it (lowering next sell price). Restock moves current stock toward target one step per item `restock_ticks` (default `behavior.ShopDefaultRestock`, set from `game_constants.shop_default_restock` in config.yaml = 1000) in **both** directions; dynamic (unlisted) items decay to 0. `buys_anything` (default true) lets specialty shops refuse unlisted items. **Live stock is per-MobInstance, in-memory only** (`ShopStock`/`ShopRestock` maps), mutated only under `MobStore.mu` via `ShopTake`/`ShopAdd`/`ShopStockOf`; restock runs in `MobStore.Tick()`. - -## Combat System - -`attack_type` (stab/slash/crush/ranged/science) selects attack/defense bonuses. **Weapons** carry a single `attack_type` string. **Mobs** carry an `attack_type` list (`[]string`, validated): exactly one melee type (stab/slash/crush) plus optionally `ranged`/`science`. A mob uses its melee type in normal combat; when a player is safespotted from melee (`safespotBlocksMelee`), the mob switches to its strongest ranged/science type (`mobStrongestRangedScience`, by max hit) via `effectiveMobAttackType`. A melee-only mob whose melee is blocked is fully blocked (`safespotBlocksMob`). Tech protection (`applyTechProtection`) uses the *resolved* attack type of the landed hit. Ranged consumes 1 ammo/attack. Aggressive mobs auto-attack if `playerLevel <= mobLevel * 2`. Movement during combat = flee attempt (mob hits abort). Death: drop credits + keep 3 most valuable items, respawn at room 1, deactivate techs. Equipment `requirements` checked before equipping. Formulas and XP splits in `combat/tracker.go`. - -## Labor System (`sys_labor.go`, `sys_hazard.go`) - -Non-violent reuse of combat engine for worksites. Task mobs (`kind: task`): HP drains to 0 to complete work (progress bar fills). Never attacks back. Commands `work`/`attack`/`kill` route to `doAttack`. Per-type defenses = method efficiency. - -**Hazards:** Room references shared hazard by ID. `HazardTick()` rolls against everyone in room every `speed` ticks. `required_item` negates. Safespot blocks all hazard damage. Lethal hit calls `killPlayer`. Does NOT interrupt movement. Safe→dangerous prompts `[Y/n]` unless `danger_warning` off. - -## Technology System (`tech.go`) - -Togglable buffs that drain battery. 4 categories: Attack/Strength/etc. tiers (5/10/15% boost), Protection techs (40% reduction), Utility (regen/drain/retribution), Combo. Reserved IDs wired to code: `protect_melee`, `protect_ranged`, `protect_science`, `retribution`. Battery = Technology level. Each tech has drain rate. Equipment TechnologyBonus reduces drain (cap 50%). ActiveTechs resets on reconnect. - -## Science System (`science.go`) - -Mods from `data/modules/`: combat/utility/transport/enchant/processing. `trigger ` fires once. `autotrigger ` auto-fires each round when deck equipped. Deck combat uses weapon speed; mod fires instead if autotrigger set. No deck = manual queuing. View with `mods`/`modules`/`module`. - -## Hacking System - -Minigame interface: 3 games at terminals (levels 1/20/40). Interface in `game/hacking/`. - -## Skills (23 total, 1-99, classic XP table) - -| Category | Skills | -| ---------- | --------------------------------------------------------------------------- | -| Combat | Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology, Assassin | -| Gathering | Fishing, Woodcutting, Mining, Hacking, Scavenging, Thieving | -| Production | Cooking, Smithing, Crafting, Fletching, Pharmacy, Construction, Farming | -| Utility | Agility, Firemaking | - -## Session States - -States in `net/server.go` covering login flow, game, NPC talk, menus (bank/recipe/confirm), and hacking. (Shops are command-driven in `StateGame` — no dedicated shop state.) - -## ItemDef Fields - -Full field reference in `item/item.go`. Key fields: equip_slot, weapon_type, attack_type, stats, speed, requirements, tool_type, craft block, farming, potion, search_table. - -## CraftIndex - -`CraftIndex` built once at startup: O(1) lookups via `ByType`, `ByStation`, `ByInput`, `FindByTwoInputs`. No cache invalidation — static after startup. - -## Places to Be Careful - -- `startAction` searches objects first, then mobs. Mobs need `behavior:` on YAML def to be interactable. -- Exits use a strict `{room: }` map format. -- `checkCondition` is the single condition evaluator — shared across exits, talk, on-enter, on_use/on_look (objects), on_kill (mobs), trigger steps. Every interaction type and every timed step shares one condition language. -- Object `hidden: true` = not in room listings but still interactable. -- Alias expansion happens before command dispatch and can shadow built-ins. -- `say` preserves case from raw input. -- `get`, `drop`, `quit` cancel active action. -- `findGroundMatches`/`findInventoryMatches` support bidirectional prefix matching. -- Mob wander config is per-instance in room YAML, not on MobDef. -- Walk distance capped by agility level. -- `wear all` equips highest-value per slot; `remove all` unequips everything. -- Cape of Agility: instant movement (1 tick). Graceful pieces reduce by 0.25 each; full set bonus. Cape overrides Graceful. -- During combat, movement = flee attempt. Mob hits abort flee. -- Task mobs (`kind: task`) reuse combat engine 1:1; HP drains to 0 to complete. Never attack back — only room hazard provides danger. -- Hazard damage omits flee-abort — never interrupts walking. Mob hits still do. -- Safespot blocks ALL hazard damage, but melee work/attack forces you out of cover. -- `killPlayer` is the shared death path — used by combat and lethal hazards. `mob` may be nil. -- Science (deck) attacks use `combat.EffectiveRoll` for both rolls. `techLevelBonus(p, "science")` included in attack roll (Neural Focus). - -## Adding a New Command - -1. Add `cmd_.go` in `internal/game/` with `func (g *Game) executeXxx(sess *net.Session, args []string, rawInput string)` -2. Register in `commandRegistry` in `cmd_registry.go` -3. Add help YAML in `data/help/` - -## Known Gaps - -- **Tick subscriber ordering**: `AdvanceActions`, combat subscriptions, mob attack subscriptions, - and hazard ticks are independent subscribers without priority guarantees. A trigger completing - and a mob landing a hit on the same tick have non-deterministic ordering. -- **3D map BFS cost**: `buildGraph()` does a full 3D BFS across the entire reachable world on every - map render (`map`/`look`/move). For large worlds (10k+ rooms) this could become expensive. Options: - (a) cache the graph keyed by player room and invalidate on world edits (`dig`/`room insert`/`undig`), - (b) prune by visited rooms (revert to horizontal-only expansion from unvisited), or (c) compute z-level - assignments once at startup and use precomputed plane membership. diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md index 3b7d769..f358a33 100644 --- a/building_guide/behaviors.md +++ b/building_guide/behaviors.md @@ -1,45 +1,45 @@ ## Behaviors -Behaviors are **inline configs** placed directly inside object YAML (`gather:`, `talk:`, -`use:`, `safespot:`) or mob YAML (`talk:`). Each section below shows the keys you can use under each behavior type. - -The examples below are object/mob files; remember the filename is the ID for these. +Behaviors are **inline configs** placed directly inside object YAML (`gather:`, `talk:`, `use:`, +`safespot:`) or mob YAML (`talk:`, `shop:`, `steal:`). Each section below shows the keys for +each behavior type. ### Gather (mining, fishing, woodcutting) -Mining — per-drop depletion on a rock object: +Gather configs live under the `gather:` key on an object. The object's filename is its ID. + +Mining — per-drop depletion on a rock: + ```yaml name: copper rock color: "B2" gather: skill: mining - tools: # requires item with matching tool_type + tools: - pickaxe success: - base: 0.40 # 40% base chance - per_level: 0.01 # +1% per skill level - cap: 0.95 # 95% max + base: 0.40 + per_level: 0.01 + cap: 0.95 gather_message: "You swing your pickaxe at the rock..." 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 - weight: 90 # 90% chance when roll succeeds - depletes: true # rock becomes depleted after this drop - message: "You manage to mine some {B2}copper ore{/}." - - table: gem_table # reference a shared drop table + level: 1 + xp: 17 + weight: 90 + depletes: true + success_message: "You manage to mine some {B2}copper ore{/}." + - table: gem_table weight: 10 - depletes: false # gem drops don't deplete the rock - message: "You spot a glint of something valuable!" - respawn_timer: 50 # ticks until rock respawns + depletes: false + success_message: "You spot a glint of something valuable!" + respawn_timer: 50 respawn_broadcast: "A glint of copper catches your eye from some {name}." ``` -Drop messages support inline color tags: `{<00-FF>}text{/}`. Use the item's color -index to match its display color. Gradients also work: `{g:C4,52}text{/}`. +Fishing — non-depleting: -Non-depleting gather (fishing on a fishing spot object): ```yaml name: fishing spot gather: @@ -55,14 +55,15 @@ gather: fail_message: "Nothing seems to bite." drops: - item_id: raw_trout - level: 5 # required skill level for this drop - xp: 20 # XP awarded when this drop lands + level: 5 + xp: 20 weight: 100 - depletes: false # never depletes - message: "You catch a {45}raw trout{/}!" + depletes: false + success_message: "You catch a {45}raw trout{/}!" ``` -Woodcutting with shared depletion and bird's nests: +Woodcutting — shared depletion with bird's nests: + ```yaml name: oak tree color: "71" @@ -78,18 +79,19 @@ gather: fail_message: "You swing but get no logs." drops: - item_id: oak_logs - level: 15 # required skill level for this drop - xp: 37 # XP awarded when this drop lands + level: 15 + xp: 37 weight: 100 - depletes: false # depletion is timer-based (deplete_timer) - message: "You get some {71}oak logs{/}." - respawn_timer: 14 # ticks until tree respawns after being cut down - deplete_timer: 45 # max ticks before next gather depletes (counts down while chopping) - nest_chance: 256 # 1/256 chance for a bird's nest on each successful gather + depletes: false + success_message: "You get some {71}oak logs{/}." + respawn_timer: 14 + deplete_timer: 45 + nest_chance: 256 respawn_broadcast: "An {name} grows back." ``` -Regular tree — always depletes on first gather, no shared timer, no nests: +Regular tree — always depletes on first gather: + ```yaml name: tree gather: @@ -104,11 +106,11 @@ gather: fail_message: "You swing but get no logs." drops: - item_id: logs - level: 1 # required skill level for this drop - xp: 25 # XP awarded when this drop lands + level: 1 + xp: 25 weight: 100 - depletes: true # regular tree depletes on first successful gather - message: "You get some {6B}logs{/}." + depletes: true + success_message: "You get some {6B}logs{/}." respawn_timer: 80 respawn_broadcast: "A {name} grows back." ``` @@ -116,46 +118,52 @@ gather: #### GatherConfig Fields | Field | Type | Description | -|---|---|---| +|-------|------|-------------| | `skill` | string | Skill name for level check | -| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`) | +| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`, `[axe]`) | +| `no_tool_speed` | float64 | Base wait in ticks when no tool is held | | `bait` | string | Required bait item (for fishing) | | `success` | SuccessFormula | Base + per_level, capped | -| `gather_message` | string | Custom "You gather..." message | -| `depleted_message` | string | Custom "The rock is depleted" message | -| `exhausted_message` | string | Custom "The tree falls" message | -| `fail_message` | string | Custom failure message | +| `gather_message` | string | Message when gathering starts | +| `depleted_message` | string | Message when resource is depleted | +| `exhausted_message` | string | Message when tree falls | +| `fail_message` | string | Message on failed gather | +| `broadcast_message` | string | Broadcast to room on successful gather | | `drops` | []DropEntry | Weighted drop entries | | `respawn_timer` | float64 | Ticks until depleted object respawns | -| `respawn_broadcast` | string | Broadcast message when object respawns | +| `respawn_broadcast` | string | Broadcast when object respawns | | `deplete_timer` | float64 | Ticks for shared depletion (trees) | | `nest_chance` | int | 1/N chance for bird's nest alongside normal drop | #### SuccessFormula | Field | Type | Description | -|---|---|---| -| `base` | float64 | Base success chance (0.0-1.0) | +|-------|------|-------------| +| `base` | float64 | Base success chance (0.0–1.0) | | `per_level` | float64 | Chance increase per skill level | | `cap` | float64 | Maximum success chance | #### DropEntry | Field | Type | Description | -|---|---|---| +|-------|------|-------------| | `item_id` | string | Item ID to drop | | `table` | string | Reference to a shared drop table in `data/drops/` | | `weight` | int | Relative drop weight | | `level` | int | Skill level required for this drop | | `xp` | int | XP awarded when this drop lands | | `depletes` | bool | Resource depletes on this drop | -| `quantity` | int | Amount to drop (1-3 for random) | -| `message` | string | Player message on drop | +| `quantity` | int | Amount to drop (1–3 for random) | +| `success_message` | string | Player message on drop (supports color tags) | +| `tool` | string | Only drops when this tool_type is active | -#### Shared depletion explained +Drop messages support inline color tags: `{<00–FF>}text{/}` and gradients `{g:C4,52}text{/}`. +Use the item's color index to match its display color. + +#### Shared depletion When `deplete_timer > 0`, the tree has a shared despawn timer: -- The timer starts at `deplete_timer` max when the first player begins chopping. +- Timer starts at `deplete_timer` max when the first player begins chopping. - Each tick, if anyone is chopping, the timer counts down. - When the timer reaches 0, the NEXT successful gather depletes the tree. - If no one is chopping and the tree isn't depleted, the timer ticks back UP. @@ -163,22 +171,27 @@ When `deplete_timer > 0`, the tree has a shared despawn timer: Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks. -#### Bird's nests +#### Bird's nests and the `search` command -When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop -a bird's nest. The nest goes to inventory (or to the ground if inventory is full). -Use the `search` command to open nests — they roll on the `birds_nest_drop` table. +When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop a +bird's nest. The nest goes to inventory (or to the ground if inventory is full). -#### XP drops +Use the `search` command to open searchable items in your inventory: -Every successful drop awards the XP defined on that drop entry. If the player's `xpdrops` -toggle is on, the output includes the XP gain: `(+37xp wct)`. +``` +search e.g. search nest + e.g. search birds nest +``` + +Searches your inventory for the named item. If found, removes it and rolls on a drop table +(currently `birds_nest_drop`). Only bird's nests are searchable at present. + +--- ### Talk (dialog trees) Talk configs go under the `talk:` key on objects or mobs. A conversation is a set of named -**nodes**, each with a message, optional action, optional auto-advance target, and choices -for the player. +**nodes**, each with messages, optional action, and choices for the player. ```yaml name: Guard @@ -198,13 +211,11 @@ talk: goto: has_pass condition: player_flag: got_pass - value: true - text: "\"Goodbye.\"" - # no goto = end conversation about_gate: messages: - - "\"Supplies. Weapons.\" He shifts his weight. \"Bring me some copper ore and I'll stamp you a pass.\"" + - "\"Supplies. Weapons. Bring me copper ore and I'll stamp you a pass.\"" action: set_player_flags: talked_to_guard: true @@ -239,120 +250,41 @@ talk: #### TalkNode fields | Field | Type | Description | -|---|---|---| -| `messages` | []string | NPC dialogue, shown one at a time. The player presses enter to advance through each message. Options appear after the last message. A single-element list works like a simple one-shot line. | -| `condition` | Condition | Optional. If the condition fails, the node is skipped entirely — its messages, action, and options are not shown. Use with `goto` to auto-advance to a different node. | -| `action` | NodeAction | Fires when the node is entered (give items, set flags, etc). | -| `options` | []TalkOption | Player choices. If empty and `goto` is set, auto-advances. | -| `goto` | string | Node ID to auto-advance to when there are no visible options OR when the node condition fails. | +|-------|------|-------------| +| `messages` | []string | NPC dialogue, shown one at a time. Player presses enter to advance. Options appear after the last message. | +| `message` | string/list | Single message or randomized list (picks one at random). | +| `condition` | Condition | If condition fails, the node is skipped via `goto`. | +| `action` | NodeAction | Fires when the node is entered. | +| `options` | []TalkOption | Player choices. Empty + `goto` set = auto-advance. | +| `goto` | string | Node ID to auto-advance to when no visible options or condition fails. | #### TalkOption fields | Field | Type | Description | -|---|---|---| -| `text` | string | Player-facing choice text. | -| `goto` | string | Node ID to navigate to if this choice is selected. Omit to end the conversation. | -| `condition` | Condition | Only show this option if the condition passes. | -| `action` | NodeAction | Fires when this option is selected, BEFORE navigating to `goto`. | - -#### Node action reference - -| Field | Effect | -|---|---| -| `set_global_flags` | Sets global flags (global, shared by all players). | -| `set_player_flags` | Sets player-local flags (per-character, quest progress). | -| `give_item` | Gives an item to the player's inventory. | -| `take_item` | Removes an item from the player's inventory. | -| `teleport` | Moves the player to a room ID. | -| `heal` | Restores that many hitpoints. | -| `credits` | Credits charged (negative) or awarded (positive). | -| `aps_node` | Marks this room's APS node as unlocked. | - -The full effect vocabulary (used by all interaction types and on_enter/trigger -steps) additionally includes `message`, `broadcast`, `broadcast_global`, -`spawn_mob`, `despawn_mob`, and `delay` — see [interaction -reference](#interaction-reference) below. - -> **Planned (not yet implemented):** `assign_task`, `skip_task`, `extend_task`, and `sawmill` are under design and will be added in a future update. - -All fields in a single action are processed together — give an item, take an item, set flags, -and heal in one node. - -Example — quest completion with multiple actions: -```yaml -action: - take_item: dragon_head - give_item: dragon_slayer_medal - set_player_flags: - dragon_quest: complete - dragon_slain: true - heal: 99 - teleport: 1 -``` - -#### Option-level actions - -Actions can be placed directly on options. The action fires when the player selects that -choice, BEFORE navigating to `goto`. This eliminates needing a separate "action node" for -simple transactions: - -```yaml -nodes: - start: - messages: - - "Welcome! Houses are 10 credits. Interested?" - options: - - text: "Yes, I'll buy one." - action: - credits: -10 - set_player_flags: - owns_house: true - goto: purchased - condition: - player_flag: owns_house - not: true - - text: "No thanks." - purchased: - messages: - - "Excellent! The plot is yours." - options: - - text: "Thanks!" - goto: start -``` - -If an option has both an `action` and a `goto`, the action runs first, then the player -navigates to the target node (which may also have its own action, fired on entry). +|-------|------|-------------| +| `text` | string | Player-facing choice text | +| `goto` | string | Node ID to navigate to. Omit to end conversation. | +| `condition` | Condition | Only show if condition passes | +| `action` | NodeAction | Fires when selected, BEFORE navigating to `goto` | #### Multi-message nodes -`messages` is a list of NPC lines shown one at a time. The player presses -enter to advance through each message. After the last message, the node's options -(if any) are displayed. This is the preferred way to deliver NPC monologues — no -named nodes or `goto` chains needed: +`messages` is a list of NPC lines shown one at a time with `[enter to continue]` prompts: ```yaml nodes: sign_reminder: messages: - "\"Yeah, just us two! Not a lot of people heading into the belt these days.\"" - - "\"But we'll be landing shortly! Please look at the information sign.\"" + - "\"But we'll be landing shortly! Look at the information sign.\"" options: - text: "\"Okay, thanks.\"" - text: "\"Goodbye.\"" ``` -Each `[enter to continue]` prompt is automatic. The node's action fires once on -entry, before the first message. Node conditions still work normally — -if the condition fails, the entire sequence is skipped via `goto`. - -If a multi-message node has no options after the last message and `goto` is set, it -auto-advances to the target node. Non-empty input during messages cancels the -conversation (just like invalid input does during option selection). - #### Randomized messages -`message` accepts either a plain string or a list. A list picks one entry at random each -time the node is visited: +Use a list under `message` to pick one at random each time the node is visited: ```yaml nodes: @@ -368,38 +300,30 @@ nodes: #### Linear auto-advance -When a node has no options (or all its options are hidden by conditions) and `goto` is set, -the conversation automatically advances to the target node. This creates dialogue chains -without prompting the player: +When a node has no visible options and `goto` is set, it auto-advances: ```yaml nodes: monologue_1: - message: "The elder clears his throat and begins..." + message: "The elder clears his throat..." goto: monologue_2 monologue_2: message: "\"Long ago, before the asteroid was settled...\"" goto: monologue_3 monologue_3: - message: "\"...a great darkness fell upon these halls.\" He pauses." + message: "\"...a great darkness fell upon these halls.\"" goto: choice_point choice_point: - message: "\"Do you understand the weight of what I'm telling you?\"" + message: "\"Do you understand?\"" options: - text: "\"I think so.\"" - text: "\"Not really.\"" ``` -If a node has both `goto` and options, the options take priority — `goto` is only used -when no options are visible. - #### Conditional nodes -A node can have its own `condition` that gates the entire node — message, action, -and options. When the condition fails, the node is skipped and the conversation -automatically advances to `goto` (if set). This lets you build branching first -messages or entire mutually-exclusive conversation paths based on player flags, -items, or any other condition: +A node with a `condition` gates the entire node. When the condition fails, the node is skipped +and goes to `goto`. This lets you branch the entire conversation based on player state: ```yaml nodes: @@ -407,107 +331,134 @@ nodes: condition: player_flag: finished_tutorial not: true - message: "\"Welcome, newcomer! Need any help getting started?\"" + messages: + - "\"Welcome, newcomer! Need help getting started?\"" goto: returning_player options: - text: "\"Yes, where do I go?\"" goto: directions - - text: "\"I'll figure it out.\"" returning_player: - message: "\"Ah, you're back! I heard you handled that raid beautifully.\"" + messages: + - "\"Ah, you're back! I heard you handled that raid.\"" options: - text: "\"The loot was worth it.\"" - - text: "\"Barely made it out alive.\"" - directions: - message: "\"Head north through the gate and follow the road.\"" +``` + +When `finished_tutorial` is set, `start` fails → jumps to `returning_player`. When unset, +the player sees the newcomer message and options. Node conditions compose with option +conditions: if the node condition passes, each option's condition is checked independently. + +#### Option-level actions + +Actions on options fire when selected, BEFORE navigating to `goto`: + +```yaml +nodes: + start: + messages: + - "Houses are 10 credits. Interested?" options: - - text: "\"Thanks!\"" + - text: "Yes, I'll buy one." + action: + credits: -10 + set_player_flags: + owns_house: true + goto: purchased + condition: + player_flag: owns_house + not: true + - text: "No thanks." + purchased: + messages: + - "The plot is yours. Use the Estate Directory to enter." + options: + - text: "Thanks!" + goto: start ``` -When `finished_tutorial` is set, the `start` node's condition fails, so the player -never sees "Welcome, newcomer" — it jumps straight to `returning_player`. When the -flag is unset, the condition passes and the player sees the newcomer message and its -options. +If an option has both `action` and `goto`, action runs first, then navigation. + +#### Node/option action vocabulary + +| Field | Effect | +|-------|--------| +| `set_global_flags` | Sets global flags (shared by all players) | +| `set_player_flags` | Sets player-local flags (per-character, saved to YAML) | +| `give_item` | Gives an item to the player's inventory | +| `take_item` | Removes an item from the player's inventory | +| `teleport` | Moves the player to a room ID | +| `heal` | Restores that many hitpoints (clamped to MaxHP) | +| `credits` | Positive = award, negative = charge | +| `aps_node` | Marks this room's APS node as unlocked | + +All fields in a single action are processed together: + +```yaml +action: + take_item: dragon_head + give_item: dragon_slayer_medal + set_player_flags: + dragon_quest: complete + dragon_slain: true + heal: 99 + teleport: 1 +``` -Node conditions compose cleanly with option conditions. When the node condition -passes, each option's own condition is then checked independently to determine -visibility — exactly as option conditions always work. When the node condition -fails, the node (and all its options) are skipped entirely. +--- ### Shops -A shop is a **root-level `shop:` property on a mob def** (not part of the talk tree). -Any mob in a room that has a `shop:` block can be traded with directly from the game -prompt — there is no separate shop mode. Players use the ordinary commands: +A shop is a **root-level `shop:` property on a mob** (not part of the talk tree). Any mob +with a `shop:` block can be traded with from the game prompt — no separate shop mode: ``` -list Show the shop inventory of the mob in the room -buy Buy an item (partial names work, e.g. "buy bait") +list Show shop inventory +buy Buy an item (partial names work) buy Buy a quantity -buy all Buy as many as you can afford and carry -sell Sell an item from your inventory +buy all Buy as many as you can afford/carry +sell Sell an item sell Sell a quantity sell all Sell your entire held stack ``` -When a room has more than one shop mob, disambiguate with the mob name: - -``` -list -buy from -sell to -``` - -If several shops are present and no mob is named, the game replies "Which shop?" -and lists the options. +With multiple shops in a room, disambiguate: `list `, `buy from `, `sell + to `. #### Pricing Shops never define per-item prices — prices derive from each item's `value:` field: - -- **Buy price** (player buying) is always **100% of the item's value**. -- **Sell price** (player selling) uses the OSRS store formula: - - ``` - sell = value × max(10, buy_percentage − stock × change_percentage) / 100 - ``` - - where `stock` is the shop's current holding of that item. With the defaults - (`buy_percentage: 40`, `change_percentage: 3`), an item the shop has none of - sells for 40% of value, dropping 3% per unit already in stock down to a floor - of 10% of value (reached at 10 in stock). Selling multiple units in one command - prices each unit at the rising stock level. +- **Buy price** is always **100% of the item's value**. +- **Sell price** uses the OSRS store formula: `value × max(10, buy_percent − stock × + change_percent) / 100`. With defaults (buy_percent: 40, change_percent: 3), an item sells + for 40% of value at zero stock, dropping 3% per unit in stock, floor 10%. #### Stock and restocking -Each shop item carries a `stock` (its target/maximum, and its starting amount). -Buying depletes stock — an item at 0 stock is out of stock. Selling raises stock, -lowering subsequent sell prices. Every `restock_ticks` the current stock moves one -step toward the configured target (refilling when low, decaying overstock when -high). An item that omits `restock_ticks` uses the global default from -`game_constants.shop_default_restock` in `config.yaml` (1000 ticks). Items a player -sold that the shop didn't originally list are held as dynamic stock that decays -back to 0. +Each shop item carries a `stock` (target/max and starting amount). Buying depletes stock; +selling raises stock, lowering sell prices. Every `restock_ticks` the current stock moves +one step toward the target. Items with no `restock_ticks` use the global default (1000 +ticks). Items a player sold that the shop didn't list are held as dynamic stock that decays +to 0. #### Shop fields | Field | Description | -|---|---| -| `shop.message` | Optional greeting shown at the top of `list`. | -| `shop.buy_percentage` | Sell price at zero stock, as % of value (default 40). | -| `shop.change_percentage` | Sell price drop per unit of stock, as % of value (default 3). | -| `shop.buys_anything` | Whether the shop buys items not in its list (default true). | -| `shop.items[].item_id` | Item definition ID. | -| `shop.items[].stock` | Target/maximum and starting stock. | -| `shop.items[].restock_ticks` | Per-item restock interval (default `game_constants.shop_default_restock`, 1000). | - -Full example — General Store: +|-------|-------------| +| `shop.message` | Optional greeting shown at top of `list` | +| `shop.buy_percentage` | Sell price at zero stock, % of value (default 40) | +| `shop.change_percentage` | Sell price drop per unit of stock, % of value (default 3) | +| `shop.buys_anything` | Accept items not in its list (default true) | +| `shop.items[].item_id` | Item definition ID | +| `shop.items[].stock` | Target/maximum and starting stock | +| `shop.items[].restock_ticks` | Per-item restock interval (default 1000) | + +Example — General Store: + ```yaml name: Shopkeeper protected: true -hp: 50 shop: - message: "\"Welcome to the General Store! Have a look around.\"" + message: "\"Welcome to the General Store!\"" items: - item_id: fishing_rod stock: 10 @@ -518,114 +469,78 @@ shop: talk: nodes: start: - message: "\"Welcome! Type 'list' to see my wares.\"" + message: "\"Type 'list' to see my wares.\"" options: - text: "\"Goodbye.\"" ``` -A specialty shop that only buys back its own stock sets `buys_anything: false`. +--- -### Object Interactions (levers, switches, gates) — `on_use` and `on_look` +### Interaction Reference -Interactions are the unified, conditional action system shared by objects -(`on_use`, `on_look`), mobs (`on_kill`), exit traversal -(`on_traverse`), on_enter steps, room/global triggers, talk nodes, and talk -options. Each interaction has an optional condition, an optional message, -and an optional `action` (the full effect vocabulary). When a list of -interactions is evaluated, the first whose condition passes wins and fires. +The `Interaction` struct is the shared shape used by `on_use`, `on_look`, `on_kill`, +`on_traverse`, on_enter steps, and trigger steps. When a list of interactions is evaluated, +the first whose condition passes wins and fires. -On objects, `on_use` runs when the player types `use ` (bare) or -`use on ` (item-specific). Entries with no `item_id` are bare (fire when `use ` is typed) — -triggered by `use `: +| Field | Type | Description | +|-------|------|-------------| +| `item_id` | string | `on_use`: required item (empty = bare `use `). `on_look`: only fires if carrying this item. `on_kill`: only fires if wielding this weapon. | +| `condition` | Condition | Optional gate | +| `message` | string | Message shown to the player when this entry fires | +| `action` | StepAction | Optional effects (see below) | -```yaml -name: iron gate -hidden: true -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: - set_global_flags: - gate_open: true -``` +#### StepAction / NodeAction vocabulary -Item-specific interaction: -```yaml -name: bookshelf -on_use: - - item_id: dusty_tome - message: "The bookshelf slides aside, revealing a secret passage!" - action: - set_global_flags: - secret_passage_open: true -``` +The universal effect vocabulary used by all interaction types, talk node actions, on_enter +steps, and trigger steps: -`on_look` runs after the description is shown when the player types -`look `. It is a list of interactions (same shape), with `item_id` -acting as a *has_item* gate: an entry with an `item_id` only fires if the -player currently carries that item in their inventory (empty = always fires -on look). -typically empty: +| Field | Type | Description | +|-------|------|-------------| +| `set_global_flags` | map[string]any | Set global flags (cascades other triggers) | +| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML) | +| `give_item` | string | Give one unit of an item to inventory | +| `take_item` | string | Remove one unit of an item from inventory | +| `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 discovered APS node | +| `message` | string | Direct message to the player (`%p`/`%v` supported) | +| `broadcast` | string | Announce to all in the room | +| `broadcast_global` | string | Announce to all online | +| `spawn_mob` | string/map | Spawn a transient mob | +| `despawn_mob` | string | Despawn all transient mobs of this id | +| `delay` | int | Ticks to wait (on_enter/triggers only) | +| `condition` | Condition | Per-step gate (on_enter/trigger steps only) | + +#### SpawnMobConfig + +For transient mob spawning, pass either a bare string (the mob ID) or a map: ```yaml -name: sign -on_look: - - set_player_flags: - read_sign: true +spawn_mob: rat + +# Equivalent to: +spawn_mob: + id: rat ``` +Full config: + +| Field | Description | +|-------|-------------| +| `id` | Mob ID to spawn (required) | +| `owner_only` | Private to the triggering player | +| `despawn_on_leave` | Fades when owner leaves allowed rooms | +| `despawn_rooms` | Rooms where the spawn survives | +| `despawn_ticks` | Despawn countdown in ticks | + --- -### Interaction Reference +### On Kill — Mob Interactions -The `Interaction` struct is the shared per-entry shape: - -| Field | Type | Description | -| ----------- | ---------- | --------------------------------------------------------------- | -| `item_id` | string | `on_use`: required item id (empty = bare `use `). `on_look`: only fires if you carry this item. `on_kill`: only fires if you wield this weapon (main_hand or off_hand) when the mob is defeated. | -| `condition` | Condition | Optional gate (see [conditions](conditions.md)) | -| `message` | string | Message shown to the player when this entry fires | -| `action` | StepAction | Optional effects — the full superset below | - -The `StepAction` superset is the universal effect container used by every -interaction type, talk node actions, on_enter steps, and trigger steps. The -inline-only `NodeAction` fields are the subset historically used by talk -nodes; the additional sequence-only fields (`message`, `broadcast`, -`broadcast_global`, `spawn_mob`, `despawn_mob`, `delay`) are available on -all of them but primarily meaningful for on_enter/triggers (inline callers -may use `broadcast` to announce to the room — handy for `on_kill` -announcements). - -| Field | Type | Description | -| ------------------ | -------------- | -------------------------------------------------------------------- | -| `set_global_flags` | map[string]any | Set global flags (shared by all players; cascades other triggers) | -| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML; cascades) | -| `give_item` | string | Give one unit of an item to inventory | -| `take_item` | string | Remove one unit of an item from inventory | -| `teleport` | int | Move player to a room ID; runs `look` + `on_enter` of the destination | -| `heal` | int | Restore hitpoints (clamped to MaxHP) | -| `credits` | int | Add (positive) or deduct (negative) credits (gated by affordability) | -| `aps_node` | bool | Mark the current room as a discovered APS node on the datapad | -| `message` | string | Direct message to the player (template vars `%p`/`%v` supported) | -| `broadcast` | string | Announce to all in the room (`%p`/`%v` substituted per recipient) | -| `broadcast_global` | string | Announce to all online (`%p`/`%v` substituted per recipient) | -| `spawn_mob` | string/map | Spawn a transient mob — bare string (id) or full [config](#spawn-mob-config) | -| `despawn_mob` | string | Despawn all transient mobs of this id (optionally owner-filtered) | -| `delay` | int | Ticks to wait before this step fires (on_enter/triggers only) | -| `condition` | Condition | Per-step gate (on_enter steps and trigger steps; evaluated before firing) | - -### Mob Interactions (On Kill) - -Mobs can define `on_kill` (fires when this mob is defeated — either by a -combat kill or by a `kind: task` mob's HP draining to zero, the -"completion" of the work). It is additive — standard loot `drops` still -hit the ground first, then the first matching interaction fires. An -entry with an `item_id` only fires if the player is wielding that item -in a weapon-hand slot (main_hand or off_hand) at the moment of the kill. +Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the +ground first, then the first matching interaction fires. An entry with `item_id` only fires +if the player wields that weapon: ```yaml name: boss @@ -633,7 +548,7 @@ combat: ... on_kill: - condition: global_flag: boss_quest_active - message: "The boss crumbles to dust. The Guardian Stone reverberates!" + message: "The boss crumbles to dust!" action: set_global_flags: boss_slain: true @@ -646,42 +561,16 @@ on_kill: dragon_slain: true ``` +Task mob `on_kill` works the same way — fires when the work is completed: + ```yaml name: reactor panel task: ... on_kill: - - message: "With a final burst of effort, the panel snaps into place." + - message: "The panel snaps into place." action: set_global_flags: reactor_repaired: true set_player_flags: repaired_reactor: true ``` - -### Spawn Mob Config - -For transient mob spawning (in `spawn_mob:` fields), pass either a bare -string (the mob ID) or a map with these fields: - -```yaml -spawn_mob: rat -``` - -```yaml -spawn_mob: - id: boss_add - owner_only: true - despawn_on_leave: true - despawn_rooms: [100, 101] - despawn_ticks: 30.0 -``` - -| Field | Description | -| ----------------- | ------------------------------------------------------------------------ | -| `id` | Mob ID to spawn (required when a map) | -| `owner_only` | If true, the spawn is private to the triggering player | -| `despawn_on_leave`| Trigger-spawned mob fades after leaving the owner's room (grace window) | -| `despawn_rooms` | List of rooms where the spawn survives without ticking the despawn timer | -| `despawn_ticks` | Despawn countdown in ticks (float OK; rounded probabilistically via `engine.ToTicks`) | - ---- diff --git a/building_guide/conditions.md b/building_guide/conditions.md deleted file mode 100644 index 2fb3d7a..0000000 --- a/building_guide/conditions.md +++ /dev/null @@ -1,83 +0,0 @@ -## Conditions Reference - -Conditions are used in talk option guards, talk node conditions, exit gates, -on-enter scripts, on_use / on_look / on_kill interactions, -exit on_traverse interactions, room descriptions, object descriptions, and -trigger value matching. - -### Simple conditions - -A bare `global_flag:` / `player_flag:` check passes when the flag is **set to a truthy -value** (`true`, a non-zero number, a non-empty string). Add `value:` only when -you need to match a specific value (e.g. a numeric quest stage). `not: true` -inverts any check. - -```yaml -# Check a global flag is set -condition: - flag: gate_open - -# Check a global flag is NOT set -condition: - flag: gate_open - not: true - -# Check a player flag is set -condition: - player_flag: finished_tutorial - -# Check a player flag is NOT set (e.g. only on the first visit) -condition: - player_flag: finished_tutorial - not: true - -# Match a specific (non-boolean) value -condition: - player_flag: quest_stage - value: 3 - -# Check if player has an item -condition: - has_item: bronze_key - -# Check if player does NOT have an item -condition: - has_item: bronze_key - not: true - -# Check if player has enough credits -condition: - min_credits: 50 -``` - -### Compound conditions - -All must pass: -```yaml -condition: - all_of: - - flag: gate_open - - has_item: pass_stub -``` - -Any one must pass: -```yaml -condition: - any_of: - - has_item: bronze_key - - has_item: iron_key - - player_flag: master_of_unlocking -``` - -Nested compounds: -```yaml -condition: - all_of: - - player_flag: quest_started - - any_of: - - has_item: wolf_pelt - - has_item: bear_pelt -``` - ---- - diff --git a/building_guide/construction.md b/building_guide/construction.md index dd63fd7..fef4a7b 100644 --- a/building_guide/construction.md +++ b/building_guide/construction.md @@ -1,216 +1,209 @@ -# Construction Skill +## Construction -The Construction skill allows players to process logs into planks and assemble planks into furniture at a workbench. Higher-tier logs produce more valuable planks and furniture. +The Construction skill lets players saw logs into planks and assemble planks into furniture at a +workbench. Recipes live on the output item's YAML via the `craft:` block — just like every other +crafting skill (see `recipes.md`). -> **Filenames are IDs.** The item/object YAML shown below have no `id:` field — each file's name is its ID (e.g. `construct_oak_planks.yaml` → `construct_oak_planks`). See `items.md` / `objects.md`. +### Required Objects -## Skill - -- Skill name: `construction` -- Abbreviation: `con` -- Level range: 1-99 -- XP table: classic standard - -## Commands - -| Command | Description | -|---------|-------------| -| `construct` / `make` | Open construction menu at a workbench | -| `construct ` | Build a specific item | -| `construct ` | Build multiple of an item | - -## Objects - -### workbench - -The primary construction station. Placed in rooms to enable construction. +**Workbench** (`data/objects/workbench.yaml`) — the station where construction happens: ```yaml name: workbench color: "AC" -description: "A sturdy wooden workbench..." -inroom_description: "A workbench stands against the wall..." +description: + - text: "A sturdy wooden workbench covered in sawdust, nails, and blueprints." +inroom_description: "A workbench stands against the wall, cluttered with tools and wood shavings." ``` -### estate_directory - -A terminal that shows registered homeowners and lets them enter their house. Handled entirely in Go code (`internal/game/cmd_estate_directory.go`), not via YAML behaviors. +**Estate Directory** (`data/objects/estate_directory.yaml`) — terminal for homeowners: ```yaml name: Estate Directory color: "27" -description: "A holographic terminal displaying property records..." -inroom_description: "An estate directory terminal..." +description: + - text: "A holographic terminal displaying property records." +inroom_description: "An estate directory terminal stands in the center of the neighborhood." ``` -When a player looks at the directory, it scans `data/players/characters/*.yaml` for the `owns_house_local_neighborhood` player flag and lists all homeowners. When a player uses or talks to the directory and owns a house, they can teleport to their house entrance room. - -## Tools +The directory is handled by Go code: when a player looks at or uses it, it scans character +YAML files for the `owns_house_local_neighborhood` player flag and lets homeowners teleport +into their house. -### saw +### Tools -Required tool for cutting logs into planks at the workbench. +**Saw** (`data/items/tools/saw.yaml`) — required for log-to-plank recipes: ```yaml name: saw -tool_type: saw +color: "98" +description: "A sharp-toothed saw for cutting planks from logs at a workbench." +tool: + type: saw value: 30 ``` -## Items - ### Planks (4 tiers) -| Item ID | Name | Level | Value | Made From | -|---------|------|-------|-------|-----------| -| `planks` | planks | 1 | 20 | logs | -| `oak_planks` | oak planks | 15 | 40 | oak_logs | -| `teak_planks` | teak planks | 35 | 80 | teak_logs | -| `mahogany_planks` | mahogany planks | 50 | 120 | mahogany_logs | +Recipes on each plank item convert logs into planks at the workbench with a saw: + +```yaml +# data/items/materials/planks.yaml +name: planks +color: "89" +value: 20 +craft: + - type: construction + level: 1 + xp: 5 + ticks_per_cycle: 20 + station: [workbench] + tool: saw + ingredients: + - items: [logs] + quantity: 1 + success_message: "You saw the logs into %n." +``` + +```yaml +# data/items/materials/oak_planks.yaml +name: oak planks +color: "66" +value: 40 +craft: + - type: construction + level: 15 + xp: 10 + ticks_per_cycle: 20 + station: [workbench] + tool: saw + ingredients: + - items: [oak_logs] + quantity: 1 + success_message: "You saw the oak logs into %n." +``` -### Furniture (sellables) +| Item | Level | Logs → | Value | +|------|-------|--------|-------| +| `planks` | 1 | `logs` | 20 | +| `oak_planks` | 15 | `oak_logs` | 40 | +| `teak_planks` | 35 | `teak_logs` | 80 | +| `mahogany_planks` | 50 | `mahogany_logs` | 120 | -| Item ID | Level | Planks Required | -|---------|-------|-----------------| -| `wooden_shelf` | 4 | 2 planks | -| `wooden_table` | 6 | 3 planks | -| `wooden_chair` | 8 | 2 planks | -| `oak_shelf` | 18 | 2 oak planks | -| `oak_table` | 20 | 3 oak planks | -| `teak_table` | 38 | 3 teak planks | -| `mahogany_table` | 52 | 3 mahogany planks | +### Furniture -## Recipes +Furniture recipes consume planks at the workbench (no saw needed): -All recipes use `type: construction` and `station: [workbench]`. Log-to-plank recipes additionally require `tool: saw`. +```yaml +# data/items/misc/wooden_chair.yaml +name: wooden chair +color: "82" +value: 35 +craft: + - type: construction + level: 8 + xp: 30 + ticks_per_cycle: 6 + station: [workbench] + ingredients: + - items: [planks] + quantity: 2 +``` -Example plank recipe: ```yaml -type: construction -level: 15 -xp: 10 -wait: 20 -station: [workbench] -tool: saw -consume: - - items: [oak_logs] - quantity: 1 -output: oak_planks -message: "You saw the oak logs into sturdy oak planks." +# data/items/misc/wooden_table.yaml +name: wooden table +color: "82" +value: 50 +craft: + - type: construction + level: 6 + xp: 25 + ticks_per_cycle: 6 + station: [workbench] + ingredients: + - items: [planks] + quantity: 3 ``` -Example furniture recipe: ```yaml -type: construction -level: 6 -xp: 25 -wait: 6 -station: [workbench] -consume: - - items: [planks] - quantity: 3 -output: wooden_table -message: "You build a sturdy wooden table." +# data/items/misc/oak_table.yaml +name: oak table +color: "5E" +value: 100 +craft: + - type: construction + level: 20 + xp: 50 + ticks_per_cycle: 6 + station: [workbench] + ingredients: + - items: [oak_planks] + quantity: 3 ``` -## NPCs +| Item | Level | Planks | XP | +|------|-------|--------|----| +| `wooden_shelf` | 4 | 2 planks | 15 | +| `wooden_table` | 6 | 3 planks | 25 | +| `wooden_chair` | 8 | 2 planks | 30 | +| `oak_shelf` | 18 | 2 oak planks | 35 | +| `oak_table` | 20 | 3 oak planks | 50 | +| `teak_table` | 38 | 3 teak planks | 75 | +| `mahogany_table` | 52 | 3 mahogany planks | 100 | -### estate_broker +### NPCs -Located in room 16 (Construction Site). Talk behavior sells a house plot for 10 credits via `cost` + `set_player_flags`. Sets `owns_house_local_neighborhood: "local_neighborhood"` on purchase. +**Estate Broker** — sells a house plot for 10 credits. Talk node action uses `credits` (negative = charge) and `set_player_flags`: ```yaml name: estate broker +protected: true +unique: true talk: nodes: start: - message: "\"Interested in a house?\"" + messages: + - "Welcome! I'm selling housing plots for 10 credits. Interested?" options: - - text: "\"How much?\"" - goto: offer - - text: "\"No thanks.\"" - offer: - message: "\"10 credits for a plot.\"" - action: - cost: 10 - set_player_flags: - owns_house_local_neighborhood: "local_neighborhood" + - text: "Yes, I'd like to buy a house." + condition: + player_flag: owns_house_local_neighborhood + not: true + action: + credits: -10 + set_player_flags: + owns_house_local_neighborhood: local_neighborhood + goto: purchased + - text: "No thanks." + goto: goodbye + purchased: + messages: + - "The plot is yours. Use the Estate Directory to enter." options: - - text: "\"Deal!\"" -unique: true -``` - -### sawmill_operator - -Located in room 171 (Lumberyard). Talk behavior processes all logs in inventory into planks for a credit fee. Uses the `sawmill: true` node action which triggers `processSawmill()` in `internal/game/action_talk.go`. - -Fees: regular (5cr), oak (10cr), teak (20cr), mahogany (40cr). - -## Science Mod: Plank Make - -Level 86 Science utility mod that converts logs to planks at 70% of sawmill cost. Uses naturejunk, solarjunk, and scrap. - -```go -{ID: "plank_make", Name: "Plank Make", Level: 86, BaseXP: 90.0, - JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap": 1}, - Category: ModUtility, TargetType: "inventory"}, + - text: "Thanks!" + goto: start + goodbye: + messages: + - "Come back anytime!" ``` -Usage: `trigger plank make` (all logs) or `trigger plank make ` (specific). - -## Player Houses +**Sawmill Operator** — converts logs to planks for credits via dialog. Fees per log: regular (5cr), oak (10cr), teak (20cr), mahogany (40cr). Use option conditions with `any_of` + `has_item` to gate the "process my logs" choice. -Player houses are real YAML rooms stored in `data/rooms/player_housing/`. Each house area (e.g. Local Neighborhood) has entrance + workshop rooms with unique room IDs in the 200+ range. +### Player Houses +Player houses are real YAML rooms in `data/rooms/player_housing/`. Each area has entrance + workshop rooms. House room files follow the same format as world rooms: + ```yaml name: "Player House: Entrance Hall" -description: "..." +description: "A modest but welcoming entrance hall." exits: south: 170 north: 201 objects: - - id: charging_station -``` - -The room loader (`World.LoadRoom`) checks `data/rooms/.yaml` first, then falls back to `data/rooms/player_housing/.yaml`. - -Ownership is tracked via player flags (e.g. `owns_house_local_neighborhood: "local_neighborhood"`). The Estate Directory scans character YAML files for these flags. - -## Implementation - -### Production Integration - -Construction is added to `productionTypes` in `action_production.go`: -```go -"construction": {"construct", "constructing"}, + - id: workbench ``` -This enables automatic support through the unified production system (`advanceProduction`, `startProductionFromRecipe`, etc.). - -### Command - -`cmd_construct.go` follows the same pattern as `cmd_craft.go`, using per-recipe `Station` and `Tool` checks. Station: `workbench`, Tool: `saw` (for plank recipes only). - -### Option - -`construct_all` (OptBool, default false): Auto-start construction when only one product is available. - -## Data File Summary - -| Path | Purpose | -|------|---------| -| `data/objects/workbench.yaml` | Workbench station object | -| `data/objects/estate_directory.yaml` | Directory terminal object | -| `data/items/saw.yaml` | Saw tool | -| `data/items/planks.yaml` - `mahogany_planks.yaml` | Plank items (4 tiers) | -| `data/items/wooden_shelf.yaml` - `mahogany_table.yaml` | Furniture items (7 items) | -| Item YAMLs with `craft:` blocks (planks, furniture) | Construction recipes — inline on output item YAML | -| `data/mobs/estate_broker.yaml` | Estate broker mob (talk inline) | -| `data/mobs/sawmill_operator.yaml` | Sawmill operator mob (talk inline) | -| `data/rooms/16.yaml` | Construction Site (updated) | -| `data/rooms/170.yaml` | Local Neighborhood | -| `data/rooms/171.yaml` | Lumberyard | -| `data/rooms/player_housing/200.yaml` | Player house entrance | -| `data/rooms/player_housing/201.yaml` | Player house workshop | -| `data/help/construct.yaml` | Help topic | +Ownership is tracked via player flags. The Estate Directory scans character files for flags +like `owns_house_local_neighborhood` to show registered homeowners. diff --git a/building_guide/courses.md b/building_guide/courses.md index 71dd255..e799002 100644 --- a/building_guide/courses.md +++ b/building_guide/courses.md @@ -51,6 +51,7 @@ obstacles: | -------------- | ------------- | ---------------------------------------------------------------------------- | | `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. | diff --git a/building_guide/doors.md b/building_guide/doors.md deleted file mode 100644 index c987d0f..0000000 --- a/building_guide/doors.md +++ /dev/null @@ -1,93 +0,0 @@ -## Global vs Player State: Door Examples - -> **Filenames are IDs.** Object and room YAML below carry no `id:` field — the filename is the ID (e.g. `door_button.yaml` → `door_button`). A nested `- id:` under a room's `objects:` list is a *reference* and stays. - -### Example A: Door with a button in another room (GLOBAL) - -A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is open for everyone. - -**Button object** (`data/objects/door_button.yaml`): -```yaml -name: stone button -hidden: true -on_use: - - condition: - global_flag: secret_door_open - not: true - message: "You press the stone button. You hear grinding stone in the distance." - action: - set_global_flags: - secret_door_open: true -``` - -**Room 3** — contains the button: -```yaml -name: "Button Chamber" -description: "A small stone chamber. A button protrudes from the east wall." -exits: - south: 1 -objects: - - id: door_button -``` - -**Room 7** — contains the door: -```yaml -name: "Hidden Passage" -description: "A dusty corridor. A heavy stone door blocks the way north." -exits: - south: 2 - north: - room: 8 - condition: - flag: secret_door_open # checks WORLD flag - value: true - blocked_message: "A heavy stone door blocks the way." -objects: - - id: stone_door - hidden: true -``` - -### Example B: Key-locked door (PLAYER-LOCAL) - -A locked door that only opens for a player carrying the key. Each player must find their own key. - -**Key item** (`data/items/rusty_key.yaml`): -```yaml -name: rusty key -description: "An old iron key, still functional." -value: 0 -stackable: false -``` - -**Room 5** — locked door: -```yaml -name: "Locked Storage" -description: "A small storage room. The way east is blocked by a locked iron door." -exits: - west: 2 - east: - room: 6 - condition: - has_item: rusty_key # checks PLAYER inventory - blocked_message: "The iron door is locked. You need a key." -objects: - - id: iron_door - hidden: true -``` - -**Room 6** — the other side (no key needed to exit): -```yaml -name: "Storage Closet" -description: "Shelves of dusty crates." -exits: - west: 5 # exit back — no condition -spawns: - - item_id: uncut_ruby - quantity: 1 - respawn_ticks: 500 -``` - -Key difference: the button door uses `global_flag` (shared state — one player presses, everyone benefits), the key door uses `has_item` (per-player inventory check — each player needs their own key). - ---- - diff --git a/building_guide/hidden_objects.md b/building_guide/hidden_objects.md deleted file mode 100644 index 1866d67..0000000 --- a/building_guide/hidden_objects.md +++ /dev/null @@ -1,92 +0,0 @@ -## 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`, etc. - -**The filename is the ID** (`secret_lever.yaml` → `secret_lever`); don't add an `id:` field (see `objects.md`). - -```yaml -# data/objects/secret_lever.yaml -name: stone lever -hidden: true -description: "A cleverly concealed lever behind a loose stone." -on_use: - - condition: - global_flag: secret_passage_open - value: true - not: true - message: "You pull the lever. A grinding sound 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." -``` - -### Multiple names (`aliases`) - -By default an object matches its `name` (word-prefix matching) and the words in -its `id`. Add `aliases` to accept extra names: - -```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?") instead of guessing — so keep aliases -specific enough to avoid overlap between objects in the same room. - -### Descriptions that react to flags - -An object's `description` can be a plain string or a list of conditional variants -(first matching condition wins). This is the simplest way to fake per-player -scenery — a crowd that becomes a queue, an NPC that "appears" partway through a -scene — without spawning real objects or mobs. - -**If none of the variants match, the object is treated as absent for that -player** — `look ` reports nothing is there. Combined with `hidden: true`, the -same shared 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 as before. - -### Safespot Objects - -Safespot objects should always be `hidden: true`. They only become visible in `look` when the player's effective safespot tier meets the object's `safespot.tier` requirement. This creates a natural discovery mechanic: completing quests and achievements reveals new coverage opportunities. - -Players can discover safespots by: -- Reading room descriptions for hints -- Trying `hide ` (name matching works even for hidden objects) -- Using `look ` once they know the name -- Gaining tier through progression, which reveals safespots in `look` - -See [Safespots](objects.md#safespots) for the full YAML format. - ---- - diff --git a/building_guide/items.md b/building_guide/items.md index e03622d..6280993 100644 --- a/building_guide/items.md +++ b/building_guide/items.md @@ -162,52 +162,24 @@ heal_value: 5 # positive = heal, negative = damage eat_message: "You eat the bread. Warm and satisfying." ``` -## MadeFrom — Item Combinations +## Item Combinations -Define what items combine to make this item. Used by the `use` command. Combinations run through the production system — players are prompted "How many?" and the action loops with a timer. +Item-on-item combinations use the `craft:` block (same as all other recipes). Define the +output item with `type: ""` (or omit it) and the ingredients. See `recipes.md` for the full +craft reference — byproducts, multi-item ingredient slots, message variables, and multi-step +assembly. -```yaml -name: bread dough -color: "DE" -ticks: 2 -made_from: - - items: [pot_of_flour] - quantity: 1 - byproducts: [empty_pot] - - items: [bucket_of_water, pitcher_of_water] - quantity: 1 - byproducts: [empty_bucket, empty_pitcher] -``` - -Each entry is an ingredient slot. Multiple `items` means any of them works. The first matching item found in inventory is consumed. - -`ticks` controls how many game ticks each production cycle takes (default 2). - -### Byproducts - -`byproducts` is a list matching the `items` list by index. When `bucket_of_water` (items[0]) is consumed, `empty_bucket` (byproducts[0]) is returned to inventory. When `pitcher_of_water` (items[1]) is consumed, `empty_pitcher` (byproducts[1]) is returned instead. - -Use `""` for items that produce no byproduct: - -```yaml -made_from: - - items: [pot_of_flour, loose_flour] - quantity: 1 - byproducts: [empty_pot, ""] # pot returns empty_pot, loose flour returns nothing - - items: [bucket_of_water] - quantity: 1 - byproducts: [empty_bucket] -``` - -Omit `byproducts` entirely if no items in that slot produce a byproduct: +Quick example — filling a bucket: ```yaml -made_from: - - items: [pot_of_flour] - quantity: 1 - byproducts: [empty_pot] # returns empty_pot - - items: [salt] - quantity: 1 # no byproducts field — salt is consumed entirely +name: bucket of water +craft: + ticks_per_cycle: 0 + ingredients: + - items: [vial_of_water, jug_of_water] + quantity: 1 + byproducts: [empty_vial, empty_jug] + - items: [empty_bucket] + quantity: 1 + success_message: "You pour the %i1 into the %i2." ``` - -See also `recipes.md` for station-based recipes. diff --git a/building_guide/mobs.md b/building_guide/mobs.md index 470a00e..e7790c0 100644 --- a/building_guide/mobs.md +++ b/building_guide/mobs.md @@ -208,6 +208,13 @@ shop: Note: `wander_rooms` and `wander_interval` are NOT set on the mob definition. Wander config is per-instance in the room YAML (see Rooms > Mobs section above). +### On Kill Interactions + +Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the +ground first, then the first matching interaction fires. An entry with `item_id` only fires +if the player is wielding that weapon. See the [On Kill section of +behaviors](behaviors.md#on-kill--mob-interactions) for full examples. + ### Stealable Mobs Stealable mobs have a root-level `steal:` block. On failed steal, the mob turns hostile and attacks. 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: ` 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: ` 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 ` 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 ` (bare) or `use on ` +(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 `): -```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 `). `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 ` for -explicit targeting, including `steal .` 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 ` 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 `. +discover them through room text hints, quest guidance, or by trying `look `. 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! **{/}"` - ---- - diff --git a/building_guide/quests.md b/building_guide/quests.md index ff04118..2c0cbac 100644 --- a/building_guide/quests.md +++ b/building_guide/quests.md @@ -7,6 +7,8 @@ name: "Elder" unique: true protected: true +idle_descriptions: + - "mutters about the rat infestation" talk: nodes: start: @@ -41,19 +43,6 @@ talk: heal: 10 options: - text: "\"Thanks!\"" -combat: - kind: combat - aggressive: false - attack_types: [crush] - respawn_ticks: 30 - stats: - attack: 1 - strength: 1 - defense: 1 - hp: 20 - speed: 5 -idle_descriptions: - - "mutters about the rat infestation" ``` Talk configs are inline on the mob under the `talk:` key. No separate behavior file needed. diff --git a/building_guide/recipes.md b/building_guide/recipes.md index 96bf0f9..67048c1 100644 --- a/building_guide/recipes.md +++ b/building_guide/recipes.md @@ -1,74 +1,97 @@ -# The House of Icarus - World Building Guide +## Crafting & Recipes -## Crafting +Crafting information lives on the **output item's YAML file** via the `craft:` block. There is no +separate recipe directory. To find out how to make `bronze_dagger`, open +`data/items/equipment/bronze_dagger.yaml` and look for the `craft:` block. -Crafting information lives on the **output item's YAML file** via the `craft:` block. There is no separate recipe directory. To find out how to make `bronze_dagger`, open `data/items/equipment/bronze_dagger.yaml` and look for the `craft:` block. (The item's **filename is its ID** — `bronze_dagger.yaml` → `bronze_dagger`; there is no `id:` field. `ingredients`/output entries reference items by filename.) +**The filename is the ID** (`bronze_dagger.yaml` → `bronze_dagger`). Ingredients reference items by +filename. The output item IS the craft — no separate `output` field. ### Craft YAML Reference -| Field | Type | Description | -| -------------- | -------------- | ----------------------------------------------- | -| `type` | string | Craft type (smithing, cooking, crafting, fletching, pharmacy, construction, combine, or "" for skill-less). Always matches a player skill name. | -| `subtype` | string | Production method override (`smelt` for furnace recipes, `clean` for herb cleaning). Defaults empty (no override). | -| `level` | int | Required skill level | -| `xp` | int | XP awarded on success | -| `wait` | float64 | Ticks per craft cycle | -| `station` | []string | Station object IDs required (optional) | -| `tool` | string | Required tool_type (optional) | -| `ingredients` | []IngredientEntry | Ingredients consumed (see below) | -| `output_qty` | int | Quantity produced (default 1, for stackables) | -| `fail` | string | ItemID produced on failure (optional) | -| `success_message` | string | Success message per cycle (optional, see message defaults) | -| `fail_message` | string | Failure message (optional, empty = silent fail) | -| `start_message`| string | Message when action begins (optional, see message defaults) | -| `end_message` | string | Message when action completes (optional, see message defaults) | -| `steps` | []CraftStep | Multi-step messages at tick intervals (optional)| -| `success` | SuccessFormula | Optional skill check formula (see Behaviors) | - -The output is the item itself — no `output` field needed. The item ID IS the craft ID. +| Field | Type | Description | +|-------|------|-------------| +| `type` | string | Skill name: `smithing`, `cooking`, `crafting`, `fletching`, `pharmacy`, `construction`, or `""` for skill-less. | +| `subtype` | string | Production method override: `smelt` for furnace, `clean` for herb cleaning. | +| `level` | int | Required skill level | +| `xp` | int | XP awarded on success | +| `ticks_per_cycle` | float64 | Ticks per craft cycle | +| `station` | []string | Station object IDs required (e.g. `[anvil]` or `[furnace]`) | +| `tool` | string | Required tool_type (e.g. `knife`, `saw`). Tool is NOT consumed. | +| `ingredients` | []IngredientEntry | Ingredients consumed (see below) | +| `output_qty` | int | Quantity produced per cycle (default 1) | +| `fail` | string | Item ID produced on failure | +| `success_message` | string | Success message per cycle (%n, %i1, %i2, %b1, %b2) | +| `fail_message` | string | Failure message; empty = silent fail | +| `start_message` | string | Message when action begins | +| `end_message` | string | Message when action completes | +| `steps` | []CraftStep | Multi-step messages at tick offsets | +| `success` | SuccessFormula | Optional skill check formula (base, per_level, cap) | ### Message Variables -All message fields (`success_message`, `fail_message`, `start_message`, `end_message`, and `steps[].message`) support variables that expand to colorized item names: +| Variable | Expands to | +|----------|-----------| +| `%n` | Output item name (colored) | +| `%i1` | 1st ingredient's matched item (colored) | +| `%i2` | 2nd ingredient's matched item (colored) | +| `%b1` | 1st ingredient's byproduct name (colored, success only) | +| `%b2` | 2nd ingredient's byproduct name (colored, success only) | -| Variable | Expands to | -| -------- | ----------------------------------------------- | -| `%n` | Output item name (colored, e.g. "stim potion") | -| `%i1` | 1st ingredient entry's matched item (colored) | -| `%i2` | 2nd ingredient entry's matched item (colored) | -| `%b1` | 1st byproduct item name (colored, success only) | -| `%b2` | 2nd byproduct item name (colored, success only) | - -Numbering follows YAML ingredient/byproduct order. If an ingredient entry has multiple alternative items (`items: [a, b]`), the variable expands to whichever the player actually possesses, colored with that item's `color:` field. - -Variables work alongside inline color tags (`{C4}text{/}`) which are expanded after variable substitution. +Numbering follows YAML ingredient/byproduct order. If an ingredient entry has multiple +alternatives (`items: [a, b]`), the variable expands to whichever the player has. Inline +color tags (`{C4}text{/}`) work alongside these variables. ### Message Defaults -If a message field is omitted from the YAML, the game uses a skill-level default based on the craft `type`: +If a message field is omitted, the game uses a skill-level default: -| Type | Default `success_message` | Default `start_message` | -|------|-------------------|------------------------| +| Type | Default success_message | Default start_message | +|------|------------------------|----------------------| | `cooking` | `"Cooked to perfection. %n looks great!"` | `"You start cooking %i1."` | -| `smelt` | `"You remove a white hot %n!"` | `"You place the %i1 into the furnace."` | +| `smelt` (subtype) | `"You remove a white hot %n!"` | `"You place the %i1 into the furnace."` | | `smithing` | `"You smith a %n."` | `"You begin smithing %i1."` | | `crafting` | `"You craft a %n."` | `"You begin crafting %i1."` | | `fletching` | `"You fletch a %n."` | `"You begin fletching %i1."` | | `pharmacy` | `"You mix a %n."` | `"You start mixing %i1."` | | `construction` | `"You construct a %n."` | `"You begin constructing %i1."` | -| `clean` | `"You clean the %i1."` | `"You begin cleaning herbs."` | +| `clean` (subtype) | `"You clean the %i1."` | `"You begin cleaning herbs."` | | (unset) | `"You produce %n."` | `"You start %i1."` | -Default messages are keyed by `subtype` when set, otherwise by `type`. For skills that never fail (smithing, crafting, fletching, pharmacy, construction), `fail_message` defaults to empty — no message is shown on failure. +For skills that never fail (smithing, crafting, fletching, pharmacy, construction), +`fail_message` defaults to empty — no message on failure. + +### IngredientEntry — Multi-Item Ingredient Slots + +Each ingredient entry defines a slot with multiple valid alternatives. The first matching +item in the player's inventory is consumed. + +```yaml +ingredients: + - items: [item_id, alternative_id, ...] + quantity: 1 + byproducts: [byproduct_for_item, byproduct_for_alt, ...] +``` + +`byproducts` matches the `items` list by index. Use `""` for items with no byproduct: + +```yaml +ingredients: + - items: [bucket_of_water, vial_of_water] + quantity: 1 + byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely + - items: [herb] + quantity: 1 # no byproducts — herb is consumed entirely +``` ### CraftStep — Multi-Step Messages -Optional interim messages fired at specific tick offsets during the craft cycle. The `tick` is the number of ticks elapsed since the start of the cycle. +Interim messages fired at specific tick offsets during the cycle: ```yaml craft: type: "" - wait: 6 + ticks_per_cycle: 6 ingredients: - items: [map_piece_1] quantity: 1 @@ -84,105 +107,90 @@ craft: success_message: "You assemble the map!" ``` -### IngredientEntry — Multi-Item Ingredient Slots - -Each ingredient entry defines an ingredient slot with multiple valid items. The first matching item found in the player's inventory is consumed. - -```yaml -ingredients: - - items: [item_id, alternative_id, ...] - quantity: 1 - byproducts: [byproduct_for_item, byproduct_for_alt, ...] -``` - -`byproducts` is optional. When present, it matches the `items` list by index — consuming `items[0]` returns `byproducts[0]` to inventory. Use `""` for items with no byproduct: - -```yaml -ingredients: - - items: [bucket_of_water, vial_of_water] - quantity: 1 - byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely - - items: [herb] - quantity: 1 # no byproducts — herb is consumed entirely -``` - ### Skill-Based Production ```yaml # data/items/consumables/stim_potion.yaml +name: stim potion craft: type: pharmacy level: 3 xp: 25 - wait: 4 + ticks_per_cycle: 4 ingredients: - items: [guam_potion_unf] quantity: 1 - items: [eye_of_newt] quantity: 1 - success_message: "You mix a %n." # %n expands to "stim potion" colored + success_message: "You mix a %n." ``` ### Station-Based Production +Furnace (smelting): + ```yaml # data/items/materials/bronze_bar.yaml +name: bronze bar craft: type: smithing subtype: smelt level: 1 xp: 6 - wait: 4 + ticks_per_cycle: 4 station: [furnace] ingredients: - items: [copper_ore] quantity: 1 - items: [tin_ore] quantity: 1 - success_message: "You smelt a %n." # default is "You remove a white hot %n!" - # overridden here for simpler flavor + fail_message: "You fail to smelt a usable bar." ``` +Anvil (smithing): + ```yaml # data/items/equipment/bronze_dagger.yaml +name: bronze dagger craft: type: smithing level: 1 xp: 12 - wait: 4 + ticks_per_cycle: 4 station: [anvil] ingredients: - items: [bronze_bar] quantity: 1 - # success_message omitted — uses default "You smith a %n." ``` -For stackable outputs, use `output_qty`: +Stackable outputs use `output_qty`: ```yaml # data/items/ammo/bronze_nails.yaml +name: bronze nails +stackable: true craft: type: smithing level: 4 xp: 12 - wait: 4 + ticks_per_cycle: 4 station: [anvil] ingredients: - items: [bronze_bar] quantity: 1 output_qty: 15 - # success_message omitted — uses default "You smith a %n." ``` ### Tool-Based Production ```yaml # data/items/ammo/arrow_shafts.yaml +name: arrow shafts craft: type: fletching level: 1 xp: 5 - wait: 3 + ticks_per_cycle: 3 tool: knife ingredients: - items: [logs, oak_logs, willow_logs] @@ -190,31 +198,31 @@ craft: output_qty: 15 ``` -The `tool` field requires the player to have an item with that `tool_type` equipped or in inventory. The tool is NOT consumed. +### Skill-Less Combinations -### Skill-Less Combinations (Item-on-Item) - -Omit `type` (or leave it empty) for combinations with no skill check, no XP, and 100% success: +Omit `type` (or leave empty) for combinations with no skill check and 100% success: ```yaml # data/items/consumables/bucket_of_water.yaml +name: bucket of water craft: - wait: 0 + ticks_per_cycle: 0 ingredients: - items: [vial_of_water, jug_of_water] quantity: 1 byproducts: [empty_vial, empty_jug] - items: [empty_bucket] quantity: 1 - success_message: "You pour the %i1 into the %i2." # %i1 = water source, %i2 = bucket + success_message: "You pour the %i1 into the %i2." ``` -### Multi-Piece Assembly (3+ items → 1) +### Multi-Piece Assembly ```yaml # data/items/quest/ancient_map.yaml +name: ancient map craft: - wait: 0 + ticks_per_cycle: 0 ingredients: - items: [torn_page_1] quantity: 1 @@ -230,46 +238,31 @@ craft: success_message: "You assemble the %n." ``` -### Clean Recipes +### Clean Recipes (Herb Cleaning) -Clean herb recipes use `type: pharmacy` with `subtype: clean` and are handled as background actions (one herb per cycle, directly mutating inventory): +Use `type: pharmacy` with `subtype: clean`: ```yaml -# data/items/materials/guam.yaml (clean guam) +# data/items/materials/guam.yaml (clean herb) +name: clean guam craft: type: pharmacy subtype: clean level: 3 xp: 3 - wait: 2 + ticks_per_cycle: 2 ingredients: - items: [grimy_guam] quantity: 1 - # success_message omitted — uses default "You clean the %i1." - # %i1 expands to the grimy herb name with its color ``` -### Food/Healing Items - -Items with `heal_value` and `eat_message` can be consumed via the `eat` command. - -```yaml -name: bread -color: "DE" -description: "A fresh loaf of bread, still warm from the oven." -value: 5 -heal_value: 5 -eat_message: "You eat the bread. Warm and satisfying." -``` +### Food / Healing -### Architecture +Items with `heal_value` and `eat_message` can be consumed via the `eat` command. See +`items.md` for the full food item format. -The `CraftIndex` (`internal/game/production_index.go`) is built once at startup from all item YAMLs that have a `craft:` block. It provides `O(1)` lookups by type, subtype, input item, and station. All crafting commands query the CraftIndex — no runtime file scanning. +### Construction -**Indexes:** -- `byType["pharmacy"]` → all pharmacy-craftable items (used by `mix`) -- `bySubtype["clean"]` → all items with subtype `clean` (grimy herb cleaning) -- `bySubtype["smelt"]` → all items with subtype `smelt` (furnace smelting) -- `byInput["guam"]` → all items that use guam (used by `use` to find combinations) -- `byStation["anvil"]` → all items craftable at an anvil (used by `use bar on anvil`) -- `FindByTwoInputs(a, b)` → items consuming both inputs in different ingredient entries +`type: construction` recipes use `station: [workbench]`. Log-to-plank recipes additionally +require `tool: saw`. See `construction.md` for plank tiers, furniture, NPCs, and player +housing. diff --git a/building_guide/rooms.md b/building_guide/rooms.md index f77aa42..3834a56 100644 --- a/building_guide/rooms.md +++ b/building_guide/rooms.md @@ -15,48 +15,65 @@ exits: east: 3 ``` -### Map color — default symbol color on the map +### Map color -A room may set a default `color` that tints its node on the map and the links connecting it to -neighbouring nodes. A player's per-room `symbol` color (set via the `symbol` command) overrides it. +A room may set a `color` that tints its node on the map and the links connecting it: ```yaml color: "5E" ``` +A player's per-room `symbol` (set via the `symbol` command) overrides the room color. + ### Inline Color Tags -Room descriptions support inline color tags using `{spec}text{/}` syntax. Untagged text uses the `room_desc` color. +Room descriptions support inline color tags using `{spec}text{/}` syntax: ```yaml description: "On the table lies a {B6 bold}mysterious vase{/} with a rose in it." ``` -Tag spec format: `{<00-FF> [bold] [dim] [underline]}text{/}` +Format: `{<00–FF> [bold] [dim] [underline]}text{/}` Gradients: `{g:C4,52}gradient text{/}`. Multi-stop: `{g:2D,27,3B}three stops{/}`. -### Exits — simple vs conditional +Untagged text uses the `room_desc` theme color. + +--- + +### Exits + +#### Simple exits + +Always passable, any direction: -Simple exit — always passable: ```yaml exits: north: 2 - ne: 3 # ne/nw/se/sw also work + ne: 3 southeast: 4 ``` -Conditional exit — blocked until a global flag is set: +#### Conditional exits + +Blocked until a condition passes: + ```yaml exits: north: room: 11 condition: - flag: gate_open + global_flag: gate_open blocked_message: "A heavy iron gate blocks the way north." ``` -Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state): +The `condition:` field uses the same condition vocabulary as everywhere else — see the +[Conditions Reference](#conditions-reference) below. + +#### Player-flag exits + +Blocked unless the PLAYER has a flag: + ```yaml exits: east: @@ -66,39 +83,57 @@ exits: blocked_message: "The vault door is locked. You need a key." ``` -Conditional exit with compound condition — requires both a global flag AND a player flag: +#### Inventory-check exit + +Blocked unless the player has an item: + +```yaml +exits: + east: + room: 6 + condition: + has_item: rusty_key + blocked_message: "The iron door is locked. You need a key." +``` + +#### Compound conditions + ```yaml exits: north: room: 20 condition: all_of: - - flag: bridge_repaired + - global_flag: bridge_repaired - player_flag: paid_toll blocked_message: "The bridge is out, and the toll collector blocks the path." ``` -Exit that sets flags when used — `set_global_flags` / `set_player_flags` are applied -only when the player actually moves through the exit (not when it's blocked): +#### Exit on_traverse — actions when walked through + +An exit can carry an `on_traverse` list. Interactions fire when the player moves through the +exit (not when it's blocked). The first whose condition passes wins. Supports the full +`Interaction` shape — condition, message, and action: + ```yaml exits: north: room: 21 condition: player_flag: lined_up - set_player_flags: - boarded_shuttle: true # marks "left this area" on the way out + blocked_message: "You're not in line yet." + on_traverse: + - set_player_flags: + boarded_shuttle: true ``` -### Hidden exits +#### Hidden exits -Hidden exits are invisible in `look` and `verbs` output until the player -discovers them by traversing them once. Even hidden, the exit is still -traversable — just not listed. Once a player moves through a hidden exit, it -becomes "discovered" for that character and appears in listing with a -`(HIDDEN)` tag. On the map, hidden exits (and rooms only reachable via them) -do not appear until the character discovers them. Gods in `god` mode see all -hidden exits regardless of discovery. +Hidden exits are invisible in `look` and `verbs` output until the player discovers them by +traversing them once. Even hidden, the exit is traversable — just not listed. Once a player +moves through a hidden exit, it becomes "discovered" for that character and appears with a +`(HIDDEN)` tag. On the map, hidden exits (and rooms only reachable via them) do not appear +until the character discovers them. Gods in `god` mode see all hidden exits. ```yaml exits: @@ -107,14 +142,12 @@ exits: hidden: true ``` -### Always-blocked exits +#### Always-blocked exits -Always-blocked exits are blocked to players but always visible. They appear in -`look`/`verbs`/`exits` tagged `(blocked)` (same as a conditionally-blocked exit), -refuse movement with the standard "The way is blocked." message, and -render on the map as blocked `X` connectors. Gods in `god` mode can traverse -them (and see no `(blocked)` tag). Primarily used for agility course -room-to-room links so the map can lay the course out visually. +Always-blocked exits are blocked to players but always visible. They show `(blocked)` in +`look`/`verbs`/`exits`, refuse movement, and render on the map as blocked `X` connectors. +Gods in `god` mode can traverse them. Used for agility course room-to-room links so the map +can lay the course out visually. ```yaml exits: @@ -123,47 +156,43 @@ exits: always_blocked: true ``` -`always_blocked` is absolute: when set, a `condition:` on the same exit is not -evaluated (the exit is blocked unconditionally). Don't combine the two — if you -need a conditionally-gated exit, use `condition:` alone. +`always_blocked` is absolute — a `condition:` on the same exit is not evaluated. Don't +combine them. Hidden and always_blocked can be combined: the exit is invisible until +discovered, then shows as blocked. -Hidden and always_blocked can be combined: the exit is invisible until -discovered (or seen by a god), and then shows as blocked. +--- ### Map grid & one-way exits -All exits (north/south/east/west/ne/nw/se/sw) must form a consistent 3D grid: from -any starting room, walking horizontal directions should never land two different -rooms on the same 3D coordinate, and a given room must always resolve to the same -spot. Up/down move on the z-axis within this same grid (different floors share the -same x/y coordinate space). +All horizontal exits (n/s/e/w/ne/nw/se/sw) must form a consistent 3D grid. Startup +validation enforces this from `startup_validation.root_rooms` in config.yaml — it reports +**overlap** (two rooms on one cell) or **twist** (one room on two cells) as errors. -Startup validation enforces this, starting from `startup_validation.root_rooms` -(see config.yaml). It reports an **overlap** (two rooms on one grid cell) or a -**twist** (one room on two cells) as an ERROR so you can fix the wiring. +Exits don't have to be reciprocal. A one-way link renders on the map as a directional arrow +instead of a two-way bar. -Exits don't have to be reciprocal. A one-way link (e.g. room 2001 has `east: 2006` -but 2006 has no `west` back) renders on the map as a directional arrow -(`← ↑ → ↓`, or `< ^ > v` in ASCII, with `↗↖↘↙` for diagonal) instead of a -two-way bar. If a link is open in one direction but blocked (failing condition) in -the other, the open direction's arrow is shown; only a link with no traversable -direction shows the blocked `X`. Blocked up/down exits also show the blocked `X`. +--- + +### Item Spawns -### Item Spawns — ground items that respawn +Ground items that respawn after being picked up: ```yaml item_spawns: - id: bronze_pickaxe quantity: 1 - respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up + respawn_ticks: 30 # reappears 30 ticks (18s) after pickup - id: copper_ore quantity: 3 respawn_ticks: 50 ``` +--- + ### Mobs — NPCs placed in the room -Simple string (no wandering): +Simple placement (no wandering): + ```yaml mobs: - "newbie_trainer" @@ -171,6 +200,7 @@ mobs: ``` With wander config per-instance: + ```yaml mobs: - id: man @@ -180,27 +210,61 @@ mobs: wander_rooms: [1, 4, 5] # optional — only exit to these rooms ``` -Mob wander config lives in the room YAML, not in the mob definition. This keeps mobs generic -so the same `man` can wander differently depending on where it's placed. Mobs wander through -legal (unconditioned) room exits. If no legal exits exist, the mob stays still. Mobs with -no `wander_interval` never wander. +Wander config lives in the room YAML, not the mob definition. The same `man` can wander +differently depending on where it's placed. Mobs wander through un-conditioned room exits. +If no legal exits exist, the mob stays still. Mobs with no `wander_interval` never wander. +Mobs stop wandering while in combat. Dead mobs respawn at their home room. + +--- ### Objects — interactive fixtures +Simple placement: + +```yaml +objects: + - id: copper_rock + - id: copper_rock # second instance +``` + +With wandering config: + ```yaml objects: - - id: copper_rock # simple placement - - id: copper_rock # second instance - id: fishing_spot - wander_rooms: [7, 8, 9] # teleports between these rooms - wander_interval: 12 # every 12 ticks - - id: iron_gate # hidden object (see below) + wander_rooms: [7, 8, 9] # teleports between these rooms + wander_interval: 12 # every 12 ticks +``` + +Wandering objects teleport between rooms in their `wander_rooms` list. Players gathering +from a wandering object are silently interrupted when it moves. + +Local objects (defined directly in the room, no file needed): + +```yaml +objects: + - id: workbench # reference: resolves to data/objects/workbench.yaml + - name: window # local: no file, identity comes from name + 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: "Safety instructions are printed in bold lettering." + on_look: + - set_player_flags: + 1001_look_sign: true ``` -### On-enter scripts — messages and timed sequences when a player arrives +See `objects.md` for the full object reference — local objects, file objects, behaviors, +interactions, safespots, stealing, and hidden objects. + +--- + +### On-enter scripts -Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps -whose condition fails are skipped. +Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps whose +condition fails are skipped. ```yaml on_enter: @@ -214,10 +278,11 @@ on_enter: player_flag: talked_to_guard # subsequent visits ``` -**Timed sequences.** A step may also carry a `delay` (ticks to wait before it -fires) and/or set flags (`set_global_flags` / `set_player_flags`). If any surviving step -has a delay or sets a flag, the whole sequence runs as a scheduled enter sequence; -plain message-only scripts still print instantly. +#### Timed sequences + +A step may carry a `delay` (ticks to wait before firing) and/or set flags. If any step has a +delay or sets a flag, the whole sequence runs as a scheduled enter sequence; plain +message-only scripts print instantly. ```yaml on_enter: @@ -226,17 +291,19 @@ on_enter: message: "Some of the crowd look you up and down." - condition: { player_flag: boarded, not: true } delay: 5 - message: "The pilot calls out: \"Tickets, please! Nice and orderly!\"" + message: "The pilot calls out: \"Tickets, please!\"" set_player_flags: - lined_up: true # opens an exit, flips a description, etc. + lined_up: true # opens an exit, flips a description, etc. - condition: { player_flag: boarded, not: true } message: "The crowd forms a single-file line." ``` -**Step actions.** On-enter steps support all the same actions as trigger steps: -`broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, `give_item`, -`take_item`, `teleport`, and `heal`. See the [trigger step actions](triggers.md#step-actions) -table for details. +#### Step actions + +On-enter steps support all the same actions as triggers: `broadcast`, `broadcast_global`, +`spawn_mob`, `despawn_mob`, `give_item`, `take_item`, `teleport`, `heal`, `credits`, +`aps_node`, `set_global_flags`, and `set_player_flags`. See the +[trigger step actions](triggers.md#step-actions) table. ```yaml on_enter: @@ -252,57 +319,76 @@ on_enter: despawn_on_leave: true ``` +#### Step-level conditions + +Each step can have a `condition:` that gates it individually. Unlike the step's action +fields, the condition is evaluated **once** on entry — a flag set by a later step won't +cancel an earlier step. + +```yaml +on_enter: + - condition: + player_flag: 1001_welcome + not: true + delay: 5 + message: "Welcome to The House of Icarus" +``` + Notes: -- `delay` counts ticks before the step fires; `delay: 0` (or omitted) fires on the next tick. -- Conditions are evaluated **once** on entry, so a flag a step sets won't cancel a later step in the same sequence. -- Gate a sequence on a flag the sequence itself sets (above, `lined_up`) so it doesn't replay on a return visit. -- If a player disconnects mid-sequence it resumes on reconnect, so they can't get stuck behind an exit the sequence was meant to open. Plain `on_enter` messages never replay on login (except the first room for new characters). -- Setting player flags from on_enter steps fires room triggers watching those flags — this is how room 1001's touchdown sequence works. +- `delay: 0` (or omitted) fires on the next tick. +- Gate a sequence on a flag the sequence itself sets so it doesn't replay on return visits. +- If a player disconnects mid-sequence it resumes on reconnect. +- Setting player flags from on_enter steps fires room triggers watching those flags. + +--- ### Conditional room descriptions -A room's `description` can be a plain string or a list of conditional variants. -Entries are checked top-to-bottom; the first whose condition passes wins. An -entry with no condition always matches — put it last as the fallback. +A room's `description` can be a plain string or a list of conditional variants. Entries are +checked top-to-bottom; the first whose condition passes wins. An entry with no condition +always matches — put it last as fallback. ```yaml description: - condition: player_flag: boarded not: true - text: "A concrete pad swarming with a couple dozen anxious passengers." + text: "A concrete pad swarming with anxious passengers." - text: "A large, empty concrete pad in the middle of the ocean." ``` -### Hazardous rooms — environmental danger +--- + +### Hazardous rooms -A room can reference a shared hazard by ID. The hazard rolls an attack against -everyone in the room every few ticks using the combat formulas (see `hazards.md`). +A room can reference a shared hazard by ID. The hazard rolls an attack against everyone in +the room every few ticks using combat formulas. ```yaml name: "Exposed Solar Array" -hazard: solar_radiation # ID of a data/hazards/.yaml definition +hazard: solar_radiation # ID of data/hazards/solar_radiation.yaml exits: south: 99999180 objects: - - id: rock_outcrop # a safespot object shields players from the hazard + - id: rock_outcrop # safespot object shields players from the hazard mobs: - - solar_panel_frame # a task worksite (see mobs.md > Task Mobs) + - solar_panel_frame # task worksite (see mobs.md) ``` -- Players are warned with a `[Y/n]` prompt before walking from a **safe** room into a - **hazardous** one (unless they disable the `danger_warning` option). Moving between - two hazardous rooms does not re-prompt. -- A safespot object (an overhang, alcove, rock outcrop, …) shields a hidden player from - **all** hazard damage — but performing a melee attack/work step forces you out of cover. -- Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the room - hazard also rolls against you. +- Players get a `[Y/n]` confirmation when walking from a safe room into a hazardous one + (unless `danger_warning` option is disabled). Moving between two hazardous rooms does not + re-prompt. +- Safespot objects shield hidden players from ALL hazard damage. +- Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the hazard + also rolls. See `hazards.md` for the full hazard definition reference. -### Room triggers — scripted events when flags change +--- + +### Room triggers -A room can carry a `triggers:` block. Each trigger watches a player or global flag -and fires a sequence of timed steps when that flag's value changes. See `triggers.md` -for the full reference. +A room can carry a `triggers:` block. Each trigger watches a player or global flag and fires +a sequence of timed steps when the flag's value changes. See `triggers.md` for the full +reference. ```yaml triggers: @@ -316,9 +402,175 @@ triggers: 1001_touchdown: true ``` -Room triggers are scoped to the room — broadcasts go to that room, mobs spawn there, -and the trigger only fires when the flag-setting player is in that room. For -server-wide events, use global triggers in `data/triggers/` instead. +Room triggers are scoped to the room — broadcasts go to that room, mobs spawn there, and the +trigger only fires when the flag-setting player is in that room. For server-wide events, use +global triggers in `data/triggers/` instead. + +--- + +### Conditions Reference + +Conditions are used in exit gates, on-enter steps, room descriptions, on_use/on_look/on_kill +interactions, talk option guards, talk node conditions, and trigger value matching. + +#### Simple conditions + +A bare `global_flag:` / `player_flag:` check passes when the flag is **set to a truthy +value** (`true`, a non-zero number, a non-empty string). Add `value:` to match a specific +value. `not: true` inverts any check. + +```yaml +# Global flag is set +condition: + global_flag: gate_open + +# Global flag is NOT set +condition: + global_flag: gate_open + not: true + +# Player flag is set +condition: + player_flag: finished_tutorial + +# Player flag is NOT set (e.g. only on first visit) +condition: + player_flag: finished_tutorial + not: true + +# Match a specific (non-boolean) value +condition: + player_flag: quest_stage + value: 3 + +# Player has an item +condition: + has_item: bronze_key + +# Player does NOT have an item +condition: + has_item: bronze_key + not: true + +# Player has enough credits +condition: + min_credits: 50 +``` + +#### Compound conditions + +All must pass: + +```yaml +condition: + all_of: + - global_flag: gate_open + - has_item: pass_stub +``` + +Any one must pass: + +```yaml +condition: + any_of: + - has_item: bronze_key + - has_item: iron_key + - player_flag: master_of_unlocking +``` + +Nested compounds: + +```yaml +condition: + all_of: + - player_flag: quest_started + - any_of: + - has_item: wolf_pelt + - has_item: bear_pelt +``` --- +### Door Pattern Examples + +#### Global door: Button in another room (shared state) + +A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is +open for everyone. + +**Button object** (`data/objects/door_button.yaml`): + +```yaml +name: stone button +hidden: true +on_use: + - condition: + global_flag: secret_door_open + not: true + message: "You press the stone button. You hear grinding stone in the distance." + action: + set_global_flags: + secret_door_open: true +``` + +**Room 7** — contains the door: + +```yaml +name: "Hidden Passage" +exits: + south: 2 + north: + room: 8 + condition: + global_flag: secret_door_open + blocked_message: "A heavy stone door blocks the way." +objects: + - id: stone_door + hidden: true +``` + +#### Player door: Key-locked (per-player state) + +A locked door that only opens for a player carrying the key. Each player must find their +own key. + +**Key item** (`data/items/rusty_key.yaml`): + +```yaml +name: rusty key +description: "An old iron key, still functional." +value: 0 +stackable: false +``` + +**Room 5** — locked door: + +```yaml +name: "Locked Storage" +exits: + west: 2 + east: + room: 6 + condition: + has_item: rusty_key + blocked_message: "The iron door is locked. You need a key." +objects: + - id: iron_door + hidden: true +``` + +**Room 6** — the other side (no key needed to exit): + +```yaml +name: "Storage Closet" +exits: + west: 5 # exit back — no condition +item_spawns: + - id: uncut_ruby + quantity: 1 + respawn_ticks: 500 +``` + +The key difference: the button door uses `global_flag` (shared — one player presses, +everyone benefits); the key door uses `has_item` (per-player inventory — each player needs +their own key). diff --git a/building_guide/search.md b/building_guide/search.md deleted file mode 100644 index e6e2a14..0000000 --- a/building_guide/search.md +++ /dev/null @@ -1,15 +0,0 @@ -## The `search` Command - -Used to open searchable items in your inventory (bird's nests, etc.): -``` -Usage: search - -Example: search nest - search birds nest -``` - -Searches your inventory for the named item. If found, removes it and rolls on a drop table. -Currently only bird's nests are searchable. - ---- - diff --git a/building_guide/state.md b/building_guide/state.md deleted file mode 100644 index fe526bf..0000000 --- a/building_guide/state.md +++ /dev/null @@ -1,13 +0,0 @@ -## State: World vs Player - -**Global flags** (`set_global_flags`, checked with `global_flag`) are shared by every player on the server. A door opened by one player is open for everyone. A lever pulled once changes the world for all. Stored in memory (`GlobalFlagStore`) — **lost on server restart**. Numeric values are compared by coercion (int/int64/float64 are normalized), so a YAML-decoded `3` matches a code-set `int(3)`. - -**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins. - -### Triggers — flag-change events - -Any flag change (world or player) can activate a **trigger** — a timed sequence of -messages, broadcasts, spawns, and further flag mutations. See `triggers.md` for -the full system. This is how you turn "player looked at the sign" into "spaceship -landed and stairs opened." - diff --git a/building_guide/tips.md b/building_guide/tips.md index 61c595f..0c598a5 100644 --- a/building_guide/tips.md +++ b/building_guide/tips.md @@ -12,12 +12,10 @@ 6. **The `talk` field on mobs** lets you talk to them directly — `talk guard` finds the guard mob, no duplicate object entry needed. -7. **Exits accept both `int` and `map` formats.** `north: 2` is shorthand for `north: {room: 2}`. Add `condition` and `blocked_message` only when needed. Mob room entries accept both `"man"` and `{id: man, ...}`. +7. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed. -8. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed. +8. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather. -9. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather. +9. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. -10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. - -11. **Use triggers to decouple cause and effect.** Objects set flags; triggers watch flags. This way the same "pull lever" action can spawn a boss in one room, open a door in another, and broadcast to the server — all by watching the same flag from different triggers. Room triggers keep local events local; global triggers (`data/triggers/`) handle server-wide events. +10. **Use triggers to decouple cause and effect.** Objects set flags; triggers watch flags. This way the same "pull lever" action can spawn a boss in one room, open a door in another, and broadcast to the server — all by watching the same flag from different triggers. Room triggers keep local events local; global triggers (`data/triggers/`) handle server-wide events. diff --git a/building_guide/triggers.md b/building_guide/triggers.md index 69cd3d1..38dbda6 100644 --- a/building_guide/triggers.md +++ b/building_guide/triggers.md @@ -1,7 +1,28 @@ ## Triggers Triggers let you script a sequence of timed events that fire when a flag changes -value. They come in two flavors: +value. + +### Flags: Global vs Player State + +**Global flags** (`set_global_flags`, checked with `global_flag`) are shared by every +player on the server. A door opened by one player is open for everyone. A lever pulled once +changes the world for all. Stored in memory — lost on server restart. Numeric values are +compared by coercion (int/int64/float64 are normalized), so a YAML-decoded `3` matches a +code-set `int(3)`. + +**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest +progress, "has read the sign," "paid the toll" — these are different for each player. Saved +to the character YAML and persist across logins. + +**Any flag change** — world or player, set from on_look, on_use, on_kill, talk nodes, +on-enter steps, exit on_traverse, trigger steps, or admin commands — can activate a +trigger. This is how you turn "player looked at the sign" into "spaceship landed and stairs +opened." + +### Trigger flavors + +Triggers come in two flavors: - **Room triggers** — defined in room YAML under a `triggers:` block. The room is the action context (broadcasts go to that room, mobs spawn there). diff --git a/building_guide/wandering_mobs.md b/building_guide/wandering_mobs.md deleted file mode 100644 index 7e2bfb6..0000000 --- a/building_guide/wandering_mobs.md +++ /dev/null @@ -1,20 +0,0 @@ -## Wandering Mobs - -Mob wandering is configured per-instance in the room YAML, not on the mob definition. (`- id: man` here references the mob whose **filename** is `man.yaml`.) -```yaml -# In a room: -mobs: - - id: man - wander_interval: 10 # attempt to wander every 10 ticks - - id: man - wander_interval: 15 - wander_rooms: [5, 6, 7] # optional — restrict which rooms via exits -``` - -Mobs wander through legal (unconditioned) room exits. When the wander interval expires, -the mob picks a random exit with no conditions and moves through it. If `wander_rooms` -is set, only exits leading to those room IDs are legal. Mobs without `wander_interval` -never wander. Mobs stop wandering while in combat. Dead mobs respawn at their home room. - ---- - diff --git a/building_guide/wandering_objects.md b/building_guide/wandering_objects.md deleted file mode 100644 index a106a0c..0000000 --- a/building_guide/wandering_objects.md +++ /dev/null @@ -1,37 +0,0 @@ -## Wandering Objects - -Fishing spots that move between rooms (**the filename is the ID** — `fishing_spot.yaml` → `fishing_spot`, no `id:` field): -```yaml -# data/objects/fishing_spot.yaml -name: fishing spot -gather: - skill: fishing - tools: - - fishing_rod - bait: fishing_bait - success: - base: 0.30 - per_level: 0.01 - cap: 0.90 - gather_message: "You cast your line into the water..." - drops: - - item_id: raw_trout - level: 5 - xp: 20 - weight: 100 - message: "You catch a raw trout!" -``` - -```yaml -# In a room: -objects: - - id: fishing_spot - wander_rooms: [7, 8, 9] - wander_interval: 12 -``` - -Objects teleport between rooms in their `wander_rooms` list. Players gathering from a -wandering object are silently interrupted when it moves. - ---- - diff --git a/data/.admin_history.json b/data/.admin_history.json index f02436d..9562261 100644 --- a/data/.admin_history.json +++ b/data/.admin_history.json @@ -1 +1 @@ -{"history":[{"time":"2026-07-09T05:13:08-04:00","description":"link 2001 west 1001","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"id: 2001\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nobjects:\n - name: hidden guy\n hidden: true\n description:\n - text: YOU FOUND HIM\n - id: tree\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false}],"redo":[{"time":"2026-07-09T05:13:19-04:00","description":"create room 1007","file_path":"data/rooms/intro/1007.yaml","old_content":"","new_content":"exits:\n south: 2002\nid: 1007\nname: 'Room #1007'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/2002.yaml","old_content":"id: 2002\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n north:\n room: 1006\n southeast:\n room: 2004\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 2002\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n north:\n room: 1007\n southeast:\n room: 2004\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T05:13:10-04:00","description":"create room 1006","file_path":"data/rooms/intro/1006.yaml","old_content":"","new_content":"exits:\n south: 2002\nid: 1006\nname: 'Room #1006'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/2002.yaml","old_content":"id: 2002\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n north:\n room: 1005\n southeast:\n room: 2004\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 2002\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n north:\n room: 1006\n southeast:\n room: 2004\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T05:13:09-04:00","description":"create room 1005","file_path":"data/rooms/intro/1005.yaml","old_content":"","new_content":"exits:\n south: 2002\nid: 1005\nname: 'Room #1005'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/2002.yaml","old_content":"id: 2002\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n southeast:\n room: 2004\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 2002\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n north:\n room: 1005\n southeast:\n room: 2004\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T05:13:08-04:00","description":"link 1001 east 2001 (reverse)","file_path":"data/rooms/intro/1001.yaml","old_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: |-\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large framed sign hangs next to the closed cockpit door. The piston-powered staircase is firmly closed at the rear of the craft.\n\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large {11}framed sign{/} hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark.\nexits:\n down:\n blocked_message: The piston-powered staircase is firmly closed.\n condition:\n all_of: []\n any_of: []\n global_flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: false\n player_flag: 1001_touchdown\n value: null\n room: 1002\n east:\n room: 2001\nhazard: \"\"\nid: 1001\nitem_spawns: []\nmobs:\n - id: flight_attendant\nname: Inside Transport Shuttle\nobjects:\n - description:\n - text: \"We hope you enjoy your trip on our shuttle! Your pilot is [TYLER].\\n\\nWhen you arrive at your final destination, please be aware of the following basic commands:\\n\\n{0B bold}Movement:{/} {0A}north{/}, {0A}south{/}, {0A}east{/}, {0A}west{/}, {0A}up{/}, {0A}down{/}\\n{0B bold}Look:{/} {0A}look{/}, {0A}look \u003citem/object/mob\u003e{/}, and {0A}map{/}\\n{0B bold}Items:{/} {0A}inv{/} to see your 28-slot inventory\\n {0A}get \u003citem\u003e{/}, {0A}drop \u003citem\u003e{/}, {0A}wear \u003citem\u003e{/}, {0A}remove \u003citem\u003e{/}\\n {0A}use \u003citem\u003e on \u003citem/object\u003e{/}\\n {0A}eq{/} for equiped gear\\n{0B bold}Comms:{/} {0A}talk \u003cmob\u003e{/} for NPC chat\\n {0A}say \u003cmsg\u003e{/} and {0A}global \u003cmsg\u003e{/} for human chat\\n{0B bold}Character:{/} {0A}score{/} and {0A}skills{/} for stats. \\n{0B bold}Combat{/} {0A}attack \u003cmob\u003e{/}\\n{0B bold}Skills{/} Many skills have shortcuts like {0A}mine{/}, {0A}fish{/}, or {0A}chop{/}\\n{0B bold}Options{/} {0A}options{/} for a list of account options\\n {0A}options \u003copt\u003e \u003cvalue\u003e{/} to change an option\\n\\nType {0C bold}what{/} for a list of possible common actions\\n\\nMake sure to check the {0A}help{/} pages, especially {0A}help newplayer{/}!\"\n hidden: true\n name: framed sign\n on_look:\n - set_player_flags:\n 1001_look_sign: true\n - description:\n - text: A sturdy, locked steel door that keeps passengers out of the cockpit. There is a tiny, thin window at eye level.\n hidden: true\n name: door\n - description:\n - text: A thick trim with rounded bolt heads frames the tiny window. It looks like it was cut into the door at some point, potentially as a simple replacement to computerized security systems. Inside the cockpit there's a pilot finishing up landing procedures on a large panel of analog instruments.\n hidden: true\n name: window\n - aliases:\n - cockpit\n description:\n - text: The cramped cockpit is surrounded by buttons, swiches, dials, and analog gauges. The parts are a mix of technologies from the last century. Part of the panel is obscured by a draped cloth as if to hide it from view. Odd.\n hidden: true\n name: instrument panel\n - aliases: []\n description:\n - text: That's the guy who took you from the Passenger Barge Denali down here to Vesta. A middle-aged man in a neat white jumpsuit with a matching white sort of conductor's hat. He's following a written procedure checklist and seems to take his job seriously.\n hidden: true\n name: pilot\non_enter:\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n global_flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B bold}Welcome to The House of Icarus{/}'\n set_global_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n global_flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 7\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}'\n set_global_flags: {}\n set_player_flags:\n 1001_welcome: true\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n global_flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 7\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B}Type{/} {0A}help newplayer{/} {0B}for the new player''s guide. That''s it. Explore and have fun.{/}'\n set_global_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\ntriggers:\n - id: \"\"\n on_global_flag: \"\"\n on_player_flag: 1001_look_sign\n room: 0\n steps:\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: The cabin shakes as the small craft touches down\n set_global_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: The pistons hiss as the rear staircase opens\n set_global_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 0\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: \"\"\n set_global_flags: {}\n set_player_flags:\n 1001_touchdown: true\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n value: null\n","new_content":"id: 1001\nname: Inside Transport Shuttle\ncolor: \"\"\ndescription:\n - text: |-\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large framed sign hangs next to the closed cockpit door. The piston-powered staircase is firmly closed at the rear of the craft.\n\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large {11}framed sign{/} hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark.\nexits:\n down:\n room: 1002\n condition:\n global_flag: \"\"\n value: null\n not: false\n player_flag: 1001_touchdown\n has_item: \"\"\n min_credits: 0\n all_of: []\n any_of: []\n blocked_message: The piston-powered staircase is firmly closed.\n east:\n room: 2001\nobjects:\n - name: framed sign\n hidden: true\n description:\n - text: \"We hope you enjoy your trip on our shuttle! Your pilot is [TYLER].\\n\\nWhen you arrive at your final destination, please be aware of the following basic commands:\\n\\n{0B bold}Movement:{/} {0A}north{/}, {0A}south{/}, {0A}east{/}, {0A}west{/}, {0A}up{/}, {0A}down{/}\\n{0B bold}Look:{/} {0A}look{/}, {0A}look \u003citem/object/mob\u003e{/}, and {0A}map{/}\\n{0B bold}Items:{/} {0A}inv{/} to see your 28-slot inventory\\n {0A}get \u003citem\u003e{/}, {0A}drop \u003citem\u003e{/}, {0A}wear \u003citem\u003e{/}, {0A}remove \u003citem\u003e{/}\\n {0A}use \u003citem\u003e on \u003citem/object\u003e{/}\\n {0A}eq{/} for equiped gear\\n{0B bold}Comms:{/} {0A}talk \u003cmob\u003e{/} for NPC chat\\n {0A}say \u003cmsg\u003e{/} and {0A}global \u003cmsg\u003e{/} for human chat\\n{0B bold}Character:{/} {0A}score{/} and {0A}skills{/} for stats. \\n{0B bold}Combat{/} {0A}attack \u003cmob\u003e{/}\\n{0B bold}Skills{/} Many skills have shortcuts like {0A}mine{/}, {0A}fish{/}, or {0A}chop{/}\\n{0B bold}Options{/} {0A}options{/} for a list of account options\\n {0A}options \u003copt\u003e \u003cvalue\u003e{/} to change an option\\n\\nType {0C bold}what{/} for a list of possible common actions\\n\\nMake sure to check the {0A}help{/} pages, especially {0A}help newplayer{/}!\"\n on_look:\n - {}\n - name: door\n hidden: true\n description:\n - text: A sturdy, locked steel door that keeps passengers out of the cockpit. There is a tiny, thin window at eye level.\n - name: window\n hidden: true\n description:\n - text: A thick trim with rounded bolt heads frames the tiny window. It looks like it was cut into the door at some point, potentially as a simple replacement to computerized security systems. Inside the cockpit there's a pilot finishing up landing procedures on a large panel of analog instruments.\n - name: instrument panel\n aliases:\n - cockpit\n hidden: true\n description:\n - text: The cramped cockpit is surrounded by buttons, swiches, dials, and analog gauges. The parts are a mix of technologies from the last century. Part of the panel is obscured by a draped cloth as if to hide it from view. Odd.\n - name: pilot\n hidden: true\n description:\n - text: That's the guy who took you from the Passenger Barge Denali down here to Vesta. A middle-aged man in a neat white jumpsuit with a matching white sort of conductor's hat. He's following a written procedure checklist and seems to take his job seriously.\nitem_spawns: []\nmobs:\n - id: flight_attendant\non_enter:\n - condition:\n global_flag: \"\"\n value: null\n not: true\n player_flag: 1001_welcome\n has_item: \"\"\n min_credits: 0\n all_of: []\n any_of: []\n message: '{0B bold}Welcome to The House of Icarus{/}'\n delay: 5\n - set_player_flags:\n 1001_welcome: true\n condition:\n global_flag: \"\"\n value: null\n not: true\n player_flag: 1001_welcome\n has_item: \"\"\n min_credits: 0\n all_of: []\n any_of: []\n message: '{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}'\n delay: 7\n - condition:\n global_flag: \"\"\n value: null\n not: true\n player_flag: 1001_welcome\n has_item: \"\"\n min_credits: 0\n all_of: []\n any_of: []\n message: '{0B}Type{/} {0A}help newplayer{/} {0B}for the new player''s guide. That''s it. Explore and have fun.{/}'\n delay: 7\nhazard: \"\"\nblock_transport: true\ntriggers:\n - id: \"\"\n on_player_flag: 1001_look_sign\n on_global_flag: \"\"\n value: null\n room: 0\n steps:\n - message: The cabin shakes as the small craft touches down\n delay: 5\n - message: The pistons hiss as the rear staircase opens\n delay: 5\n - set_player_flags:\n 1001_touchdown: true\n","is_delete":false,"is_create":false}]} \ No newline at end of file +{"history":[{"time":"2026-07-09T16:06:25-04:00","description":"bulk edit 2 rooms","file_path":"data/rooms/intro/2003.yaml","old_content":"id: 2003\nname: Test Room\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: solar_radiation\nblock_transport: false\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2004.yaml","old_content":"id: 2004\nname: Test Room\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: solar_radiation\nblock_transport: false\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:06:29-04:00","description":"bulk edit 4 rooms","file_path":"data/rooms/intro/2002.yaml","old_content":"id: 2002\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2005.yaml","old_content":"id: 2005\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:06:35-04:00","description":"bulk edit 4 rooms","file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"03\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:06:50-04:00","description":"bulk edit 4 rooms","file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:06:57-04:00","description":"bulk edit 6 rooms","file_path":"data/rooms/intro/2001.yaml","old_content":"id: 2001\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nobjects:\n - name: hidden guy\n hidden: true\n description:\n - text: YOU FOUND HIM\n - id: tree\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2006.yaml","old_content":"name: \"Test Room\"\ndescription:\n - text: \"It's a test room\"\nexits:\n east:\n room: 2005\n north:\n room: 2001\n","new_content":"block_transport: true\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n"},{"file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:06:59-04:00","description":"bulk edit 6 rooms","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2006.yaml","old_content":"block_transport: true\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n","new_content":"block_transport: false\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n"},{"file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:07:00-04:00","description":"bulk edit 6 rooms","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2006.yaml","old_content":"block_transport: false\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n","new_content":"block_transport: true\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n"},{"file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:07:00-04:00","description":"bulk edit 6 rooms","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2006.yaml","old_content":"block_transport: true\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n","new_content":"block_transport: false\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n"},{"file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:07:01-04:00","description":"bulk edit 6 rooms","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2006.yaml","old_content":"block_transport: false\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n","new_content":"block_transport: true\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n"},{"file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:07:02-04:00","description":"bulk edit 6 rooms","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2006.yaml","old_content":"block_transport: true\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n","new_content":"block_transport: false\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n"},{"file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:07:03-04:00","description":"bulk edit 6 rooms","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2006.yaml","old_content":"block_transport: false\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n","new_content":"block_transport: true\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n"},{"file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:07:05-04:00","description":"bulk edit 6 rooms","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\nname: Test Room\nobjects:\n - description:\n - text: YOU FOUND HIM\n hidden: true\n name: hidden guy\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2006.yaml","old_content":"block_transport: true\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n","new_content":"block_transport: false\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n"},{"file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: true\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:07:41-04:00","description":"bulk edit 4 rooms (move to example_unused)","file_path":"data/rooms/intro/2002.yaml","new_file_path":"data/rooms/example_unused/2002.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2005.yaml","new_file_path":"data/rooms/example_unused/2005.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","new_file_path":"data/rooms/example_unused/2003.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","new_file_path":"data/rooms/example_unused/2004.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:07:50-04:00","description":"bulk edit 4 rooms (move to intro)","file_path":"data/rooms/example_unused/2002.yaml","new_file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/example_unused/2005.yaml","new_file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/example_unused/2003.yaml","new_file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/example_unused/2004.yaml","new_file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:08:27-04:00","description":"bulk edit 4 rooms","file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]},{"time":"2026-07-09T16:08:28-04:00","description":"bulk edit 4 rooms","file_path":"data/rooms/intro/2002.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2003\n south:\n room: 2005\n southeast:\n room: 2004\n west:\n room: 2001\nhazard: solar_radiation\nid: 2002\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/2005.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2004\n north:\n room: 2002\n northeast:\n room: 2003\n northwest:\n room: 2001\n west:\n room: 2006\nhazard: solar_radiation\nid: 2005\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2003.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nhazard: solar_radiation\nid: 2003\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"},{"file_path":"data/rooms/intro/2004.yaml","old_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\ncolor: \"31\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2003\n northwest:\n room: 2002\n west:\n room: 2005\nhazard: solar_radiation\nid: 2004\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n"}]}],"redo":null} \ No newline at end of file diff --git a/data/courses/reactor.yaml b/data/courses/reactor.yaml deleted file mode 100644 index a657c21..0000000 --- a/data/courses/reactor.yaml +++ /dev/null @@ -1,96 +0,0 @@ -name: "Reactor Course" -required_level: 50 -start_room: 99999200 -completion_xp: 350 -obstacles: -- room_id: 99999220 - verb: climb - xp: 40 - fail_damage: [3, 6] - exit_dir: north - phases: - - message: "You grip the reactor scaffolding and begin your ascent..." - delay: 3.0 - - message: "The metal groans under your weight as you climb higher, heat radiating from below..." - delay: 3.0 - fail_check: true - - message: "You pull yourself onto the upper platform, the reactor humming beneath you!" - delay: 3.0 -- room_id: 99999221 - verb: swing - xp: 45 - fail_damage: [3, 7] - exit_dir: north - phases: - - message: "You seize the heavy coolant chain dangling above the reactor pit..." - delay: 3.0 - - message: "You swing out over the glowing core, heat blasting your face..." - delay: 3.0 - fail_check: true - - message: "You release and land hard on the maintenance gantry, chain clanging behind you!" - delay: 3.0 -- room_id: 99999222 - verb: balance - xp: 50 - fail_damage: [4, 7] - exit_dir: north - phases: - - message: "You step onto the massive steam pipe spanning the reactor chamber..." - delay: 3.0 - - message: "Steam jets hiss from valves on either side as you shuffle along the pipe..." - delay: 3.0 - fail_check: true - - message: "You reach the junction platform and hop off the pipe with relief!" - delay: 3.0 -- room_id: 99999223 - verb: jump - xp: 45 - fail_damage: [3, 6] - exit_dir: north - phases: - - message: "A section of the reactor platform is missing, leaving a gaping void..." - delay: 2.0 - - message: "You back up, sprint, and leap with everything you've got..." - delay: 2.0 - fail_check: true - - message: "You slam into the far platform and roll to safety!" - delay: 2.0 -- room_id: 99999224 - verb: crawl - xp: 40 - fail_damage: [3, 5] - exit_dir: north - phases: - - message: "You squeeze into the narrow service conduit, radiation warnings plastered on every surface..." - delay: 3.0 - - message: "You drag yourself through on your elbows, sparks showering from damaged wiring above..." - delay: 3.0 - fail_check: true - - message: "You tumble out the far end and gulp down clean air!" - delay: 3.0 -- room_id: 99999225 - verb: vault - xp: 50 - fail_damage: [4, 7] - exit_dir: north - phases: - - message: "A radiation containment barrier blocks the path, humming with energy..." - delay: 2.0 - - message: "You time the pulse cycle, sprint at the barrier, and throw yourself over it..." - delay: 2.0 - fail_check: true - - message: "You clear the barrier and land on the other side, heart pounding!" - delay: 2.0 -- room_id: 99999226 - verb: leap - xp: 55 - fail_damage: [4, 8] - exit_dir: north - phases: - - message: "The final obstacle: a massive chasm over the reactor coolant pool..." - delay: 3.0 - - message: "You sprint along the narrow runway, the abyss yawning below..." - delay: 3.0 - fail_check: true - - message: "You launch into the void, arms windmilling, and crash onto the far platform!" - delay: 3.0 diff --git a/data/courses/rooftop.yaml b/data/courses/rooftop.yaml deleted file mode 100644 index 4be1f71..0000000 --- a/data/courses/rooftop.yaml +++ /dev/null @@ -1,83 +0,0 @@ -name: "Rooftop Course" -required_level: 20 -start_room: 99999200 -completion_xp: 120 -obstacles: -- room_id: 99999210 - verb: climb - xp: 15 - fail_damage: [2, 4] - exit_dir: north - phases: - - message: "You grab the rough permacrete wall and begin to climb..." - delay: 2.0 - - message: "Your fingers find cracks and ledges as you pull yourself higher..." - delay: 2.0 - fail_check: true - - message: "You heave yourself over the edge and onto the rooftop!" - delay: 2.0 -- room_id: 99999211 - verb: jump - xp: 22 - fail_damage: [2, 4] - exit_dir: north - phases: - - message: "You eye the gap between this building and the next..." - delay: 2.0 - - message: "You sprint toward the edge, boots pounding on the rooftop..." - delay: 2.0 - fail_check: true - - message: "You launch yourself across and roll to a stop on the other roof!" - delay: 2.0 -- room_id: 99999212 - verb: swing - xp: 20 - fail_damage: [2, 5] - exit_dir: north - phases: - - message: "You grab the dangling power cable with both hands..." - delay: 3.0 - - message: "You kick off the ledge and swing out over the street far below..." - delay: 3.0 - fail_check: true - - message: "You release at the peak of the arc and land on the opposite platform!" - delay: 3.0 -- room_id: 99999213 - verb: balance - xp: 18 - fail_damage: [2, 4] - exit_dir: north - phases: - - message: "You step onto the narrow structural beam spanning the alley..." - delay: 3.0 - - message: "The beam sways slightly as you inch forward, arms out for balance..." - delay: 3.0 - fail_check: true - - message: "You reach the far side and step gratefully onto solid rooftop!" - delay: 3.0 -- room_id: 99999214 - verb: vault - xp: 15 - fail_damage: [2, 3] - exit_dir: north - phases: - - message: "You run toward the maintenance railing at full speed..." - delay: 2.0 - - message: "You plant one hand on the rail and swing your legs over..." - delay: 2.0 - fail_check: true - - message: "You clear the railing and land in a crouch on the other side!" - delay: 2.0 -- room_id: 99999215 - verb: leap - xp: 20 - fail_damage: [3, 5] - exit_dir: north - phases: - - message: "You stare at the final gap -- the widest yet..." - delay: 2.0 - - message: "You take a deep breath, charge forward, and throw yourself into the air..." - delay: 2.0 - fail_check: true - - message: "You barely catch the far ledge, pull yourself up, and stand triumphant!" - delay: 2.0 diff --git a/data/courses/testcourse.yaml b/data/courses/testcourse.yaml new file mode 100644 index 0000000..a6b44b2 --- /dev/null +++ b/data/courses/testcourse.yaml @@ -0,0 +1,38 @@ +completion_xp: 0 +name: Test Course +obstacles: + - exit_dir: south + fail_damage: + - 0 + - 0 + phases: + - delay: 0 + message: "" + room_id: 2002 + verb: climb + xp: 0 + - exit_dir: east + fail_damage: + - 0 + - 0 + phases: [] + room_id: 2005 + verb: climb + xp: 0 + - exit_dir: north + fail_damage: + - 0 + - 0 + room_id: 2004 + verb: climb + xp: 0 + - exit_dir: west + fail_damage: + - 0 + - 0 + phases: [] + room_id: 2003 + verb: climb + xp: 0 +required_level: 1 +start_room: 2002 diff --git a/data/courses/vent_shaft.yaml b/data/courses/vent_shaft.yaml deleted file mode 100644 index 49e42c3..0000000 --- a/data/courses/vent_shaft.yaml +++ /dev/null @@ -1,70 +0,0 @@ -name: "Ventilation Shaft Course" -required_level: 1 -start_room: 99999200 -completion_xp: 40 -obstacles: -- room_id: 99999201 - verb: scramble - xp: 8 - fail_damage: [1, 2] - exit_dir: north - phases: - - message: "You approach the corroded ventilation wall..." - delay: 2.0 - - message: "You find footholds in the rusted panels and begin to climb..." - delay: 2.0 - fail_check: true - - message: "You scramble up the wall and haul yourself onto the ledge!" - delay: 2.0 -- room_id: 99999202 - verb: balance - xp: 8 - fail_damage: [1, 2] - exit_dir: north - phases: - - message: "You step onto the narrow coolant pipe..." - delay: 2.0 - - message: "Arms outstretched, you carefully place one foot in front of the other..." - delay: 2.0 - fail_check: true - - message: "You reach the other side of the pipe and step onto solid ground!" - delay: 2.0 -- room_id: 99999203 - verb: jump - xp: 7 - fail_damage: [1, 2] - exit_dir: north - phases: - - message: "You peer across the dark gap in the shaft floor..." - delay: 2.0 - - message: "You take a few steps back, then sprint toward the edge..." - delay: 2.0 - fail_check: true - - message: "You leap across the gap and land safely on the other side!" - delay: 2.0 -- room_id: 99999204 - verb: crawl - xp: 7 - fail_damage: [1, 1] - exit_dir: north - phases: - - message: "You drop to your hands and knees at the narrow vent opening..." - delay: 3.0 - - message: "You squeeze through the tight passage, metal scraping against your back..." - delay: 3.0 - fail_check: true - - message: "You emerge from the vent and stand up, brushing dust from your clothes!" - delay: 3.0 -- room_id: 99999205 - verb: slide - xp: 10 - fail_damage: [1, 2] - exit_dir: north - phases: - - message: "You sit at the top of the emergency chute..." - delay: 2.0 - - message: "You push off and accelerate down the smooth metal surface..." - delay: 2.0 - fail_check: true - - message: "You shoot out the bottom of the chute and land on your feet!" - delay: 2.0 diff --git a/data/rooms/example_unused/agility2/99999200.yaml b/data/rooms/example_unused/agility2/99999200.yaml deleted file mode 100644 index 4afaf43..0000000 --- a/data/rooms/example_unused/agility2/99999200.yaml +++ /dev/null @@ -1,13 +0,0 @@ -id: 99999200 -name: Agility Training Grounds -color: "" -description: - - text: 'A cavernous space beneath the asteroid''s surface, repurposed as a training facility. Scaffolding, pipes, and platforms fill the chamber. Signs point to three courses of increasing difficulty: {33}Ventilation Shaft{/} (beginner), {214}Rooftop{/} (intermediate), and {196}Reactor{/} (advanced).' -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999201.yaml b/data/rooms/example_unused/agility2/99999201.yaml deleted file mode 100644 index bbb47c5..0000000 --- a/data/rooms/example_unused/agility2/99999201.yaml +++ /dev/null @@ -1,28 +0,0 @@ -id: 99999201 -name: Ventilation Shaft - Corroded Wall -color: "" -description: - - text: A towering wall of corroded ventilation panels rises before you. Rust-eaten handholds and buckled seams offer a treacherous path upward. The air smells of old metal and recycled atmosphere. -exits: - down: - room: 99999200 -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'scramble' to climb the wall. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999202.yaml b/data/rooms/example_unused/agility2/99999202.yaml deleted file mode 100644 index 346a235..0000000 --- a/data/rooms/example_unused/agility2/99999202.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999202 -name: Ventilation Shaft - Coolant Pipe Walkway -color: "" -description: - - text: A narrow coolant pipe stretches across a dark chasm. Condensation drips from its surface, making it slick. Far below, you can hear the distant hum of machinery. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'balance' to cross the pipe. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999203.yaml b/data/rooms/example_unused/agility2/99999203.yaml deleted file mode 100644 index 37333a1..0000000 --- a/data/rooms/example_unused/agility2/99999203.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999203 -name: Ventilation Shaft - Shaft Gap -color: "" -description: - - text: The ventilation shaft floor is missing here -- a jagged gap drops into darkness. The far side is just barely within jumping distance. Exposed wiring sparks intermittently below. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'jump' to leap across the gap. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999204.yaml b/data/rooms/example_unused/agility2/99999204.yaml deleted file mode 100644 index d8483d6..0000000 --- a/data/rooms/example_unused/agility2/99999204.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999204 -name: Ventilation Shaft - Narrow Vent -color: "" -description: - - text: The passage narrows dramatically here, becoming a tight rectangular vent barely wide enough to fit through. Scratches on the metal walls suggest others have squeezed through before you. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'crawl' to squeeze through the vent. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999205.yaml b/data/rooms/example_unused/agility2/99999205.yaml deleted file mode 100644 index 1e36390..0000000 --- a/data/rooms/example_unused/agility2/99999205.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999205 -name: Ventilation Shaft - Emergency Chute -color: "" -description: - - text: A smooth metal chute angles steeply downward, polished by countless slides. An old emergency evacuation sign hangs crookedly on the wall. This is the final obstacle -- the chute leads back to the training grounds. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'slide' to descend the chute. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999206.yaml b/data/rooms/example_unused/agility2/99999206.yaml deleted file mode 100644 index 77a5971..0000000 --- a/data/rooms/example_unused/agility2/99999206.yaml +++ /dev/null @@ -1,12 +0,0 @@ -id: 99999206 -name: 'Room #99999206' -color: "" -description: [] -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999210.yaml b/data/rooms/example_unused/agility2/99999210.yaml deleted file mode 100644 index ea1f3f8..0000000 --- a/data/rooms/example_unused/agility2/99999210.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999210 -name: Rooftop Course - Hab Block Wall -color: "" -description: - - text: The exterior wall of Hab Block 7 rises three stories above the street. Rough permacrete and maintenance handholds provide a challenging climb. The city spreads out below, neon signs flickering in the perpetual twilight. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'climb' to scale the wall. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999211.yaml b/data/rooms/example_unused/agility2/99999211.yaml deleted file mode 100644 index 156ae67..0000000 --- a/data/rooms/example_unused/agility2/99999211.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999211 -name: Rooftop Course - Building Gap -color: "" -description: - - text: You stand on the edge of Hab Block 7's roof. Across a three-meter gap, the roof of Hab Block 8 awaits. The street below is a dizzying drop. A few old bootprints mark the takeoff point. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'jump' to leap to the next building. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999212.yaml b/data/rooms/example_unused/agility2/99999212.yaml deleted file mode 100644 index d669c2d..0000000 --- a/data/rooms/example_unused/agility2/99999212.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999212 -name: Rooftop Course - Cable Array -color: "" -description: - - text: A tangle of power cables and data lines stretches between two antenna towers. One thick cable hangs low enough to grab. The gap below drops to a dark alleyway between hab blocks. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'swing' to cross on the cable. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999213.yaml b/data/rooms/example_unused/agility2/99999213.yaml deleted file mode 100644 index 86fe54b..0000000 --- a/data/rooms/example_unused/agility2/99999213.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999213 -name: Rooftop Course - Narrow Beam -color: "" -description: - - text: A structural I-beam extends across the gap between two buildings, no wider than your foot. It sways slightly in the recycled air currents. Someone has scratched tally marks into the near end. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'balance' to cross the beam. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999214.yaml b/data/rooms/example_unused/agility2/99999214.yaml deleted file mode 100644 index 523fd7b..0000000 --- a/data/rooms/example_unused/agility2/99999214.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999214 -name: Rooftop Course - Maintenance Railing -color: "" -description: - - text: A high maintenance railing blocks the path forward, topped with sensor equipment and warning labels. It's too high to step over but the right technique could clear it. Beyond the railing, the course continues. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'vault' to clear the railing. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999215.yaml b/data/rooms/example_unused/agility2/99999215.yaml deleted file mode 100644 index cc7e9b1..0000000 --- a/data/rooms/example_unused/agility2/99999215.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999215 -name: Rooftop Course - Rooftop Edge -color: "" -description: - - text: The final jump. The gap here is wider than any before -- a full four meters of empty air between you and the landing platform. Far below, the streets of the asteroid colony pulse with dim light. This is the last obstacle. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'leap' to make the final jump. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999220.yaml b/data/rooms/example_unused/agility2/99999220.yaml deleted file mode 100644 index 8c97248..0000000 --- a/data/rooms/example_unused/agility2/99999220.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999220 -name: Reactor Course - Scaffolding -color: "" -description: - - text: Massive metal scaffolding surrounds the outer reactor housing. The structure vibrates with the reactor's pulse. Heat radiates from every surface, and warning klaxons sound periodically in the distance. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'climb' to ascend the scaffolding. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999221.yaml b/data/rooms/example_unused/agility2/99999221.yaml deleted file mode 100644 index 1f71b7d..0000000 --- a/data/rooms/example_unused/agility2/99999221.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999221 -name: Reactor Course - Coolant Chain -color: "" -description: - - text: A heavy chain hangs from an overhead crane, suspended above the reactor cooling pit. The pit glows with an eerie blue-green light. The chain is your only way across -- the gantry ahead is the landing zone. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'swing' to cross on the chain. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999222.yaml b/data/rooms/example_unused/agility2/99999222.yaml deleted file mode 100644 index 83161af..0000000 --- a/data/rooms/example_unused/agility2/99999222.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999222 -name: Reactor Course - Steam Pipe -color: "" -description: - - text: An enormous steam pipe, two meters in diameter, stretches across the reactor chamber. Steam vents periodically blast from pressure valves along its length. The pipe's surface is warm but not scalding -- yet. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'balance' to traverse the pipe. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999223.yaml b/data/rooms/example_unused/agility2/99999223.yaml deleted file mode 100644 index cb4506f..0000000 --- a/data/rooms/example_unused/agility2/99999223.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999223 -name: Reactor Course - Platform Gap -color: "" -description: - - text: A section of the reactor maintenance platform has collapsed into the void below. Emergency barriers block the edges, but someone has moved them aside here. The gap is intimidating but clearable. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'jump' to clear the gap. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999224.yaml b/data/rooms/example_unused/agility2/99999224.yaml deleted file mode 100644 index ee48e0f..0000000 --- a/data/rooms/example_unused/agility2/99999224.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999224 -name: Reactor Course - Service Conduit -color: "" -description: - - text: A narrow service conduit leads through the reactor shielding. Radiation warning symbols are painted on every surface. Damaged wiring hangs from the ceiling, sparking occasionally. It's the only way forward. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'crawl' to enter the conduit. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999225.yaml b/data/rooms/example_unused/agility2/99999225.yaml deleted file mode 100644 index 158a213..0000000 --- a/data/rooms/example_unused/agility2/99999225.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999225 -name: Reactor Course - Radiation Barrier -color: "" -description: - - text: A containment barrier hums with energy, its surface shimmering with a faint purple glow. It pulses on and off in a regular cycle. Beyond it, the final stretch of the course is visible. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'vault' to clear the barrier. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/agility2/99999226.yaml b/data/rooms/example_unused/agility2/99999226.yaml deleted file mode 100644 index 4568b92..0000000 --- a/data/rooms/example_unused/agility2/99999226.yaml +++ /dev/null @@ -1,26 +0,0 @@ -id: 99999226 -name: Reactor Course - Reactor Chasm -color: "" -description: - - text: The final obstacle. A massive chasm separates you from the exit platform, the reactor's coolant pool churning far below in shades of luminous green. A narrow runway of grating leads to the edge. This is the longest leap on the course. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: - - message: Type 'leap' to make the final jump. - condition: null - delay: 0 - set_global_flags: {} - set_player_flags: {} - broadcast: "" - broadcast_global: "" - spawn_mob: null - despawn_mob: "" - give_item: "" - take_item: "" - teleport: 0 - heal: 0 -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/town/9999915.yaml b/data/rooms/example_unused/town/9999915.yaml index cef822b..30a5b8f 100644 --- a/data/rooms/example_unused/town/9999915.yaml +++ b/data/rooms/example_unused/town/9999915.yaml @@ -4,6 +4,8 @@ color: "" description: - text: Shelves lined with glass vials, dried herbs, and bubbling cauldrons. This area is not yet accessible. exits: + east: + room: 9999917 west: room: 9999914 objects: [] diff --git a/data/rooms/example_unused/town/9999916.yaml b/data/rooms/example_unused/town/9999916.yaml index a915452..9ebebb2 100644 --- a/data/rooms/example_unused/town/9999916.yaml +++ b/data/rooms/example_unused/town/9999916.yaml @@ -3,7 +3,11 @@ name: Construction Site color: "" description: - text: A half-built structure surrounded by planks, nails, and blueprints. A workbench stands ready for crafting. Signs point north toward the Local Neighborhood and east toward the lumberyard. -exits: {} +exits: + north: + room: 9999918 + west: + room: 9999917 objects: - id: workbench item_spawns: [] diff --git a/data/rooms/example_unused/town/9999917.yaml b/data/rooms/example_unused/town/9999917.yaml index c3202ee..be80594 100644 --- a/data/rooms/example_unused/town/9999917.yaml +++ b/data/rooms/example_unused/town/9999917.yaml @@ -5,9 +5,9 @@ description: - text: A ring of stones surrounds a bed of ash. A few unlit logs wait nearby. A narrow passage leads north toward what sounds like echoing footsteps and clanging metal. exits: east: - room: 9999918 - west: room: 9999916 + west: + room: 9999915 objects: [] item_spawns: [] mobs: diff --git a/data/rooms/example_unused/town/9999918.yaml b/data/rooms/example_unused/town/9999918.yaml index 31e81bc..d7ed4b9 100644 --- a/data/rooms/example_unused/town/9999918.yaml +++ b/data/rooms/example_unused/town/9999918.yaml @@ -1,8 +1,17 @@ -name: "Thieves' Den" +id: 9999918 +name: Thieves' Den +color: "" description: - - text: "A shadowy hideout with lockpicks, dark cloaks, and wanted posters on the walls. This area is not yet accessible." + - text: A shadowy hideout with lockpicks, dark cloaks, and wanted posters on the walls. This area is not yet accessible. exits: - east: - room: 9999919 - west: - room: 9999917 + north: + room: 9999920 + south: + room: 9999916 +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/town/9999919.yaml b/data/rooms/example_unused/town/9999919.yaml deleted file mode 100644 index a25b884..0000000 --- a/data/rooms/example_unused/town/9999919.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: "Agility Course" -description: - - text: "A training ground with balance beams, monkey bars, and climbing nets. This area is not yet accessible." -exits: - east: - room: 9999920 - west: - room: 9999918 diff --git a/data/rooms/example_unused/town/9999920.yaml b/data/rooms/example_unused/town/9999920.yaml index 67ad606..a1f1756 100644 --- a/data/rooms/example_unused/town/9999920.yaml +++ b/data/rooms/example_unused/town/9999920.yaml @@ -1,8 +1,15 @@ -name: "Assassin's Bureau" +id: 9999920 +name: Assassin's Bureau +color: "" description: - - text: "A discreet office with job postings pinned to a corkboard. This area is not yet accessible." + - text: A discreet office with job postings pinned to a corkboard. This area is not yet accessible. exits: - east: - room: 9999921 - west: - room: 9999919 + south: + room: 9999918 +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/town/9999921.yaml b/data/rooms/example_unused/town/9999921.yaml deleted file mode 100644 index d1b1739..0000000 --- a/data/rooms/example_unused/town/9999921.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: "Combat Arena" -description: - - text: "A sandy training ground with practice dummies and sparring rings. Several men wander here looking for a fight." -exits: - west: - room: 9999920 -mobs: - - id: man - wander_interval: 10 - - id: man - wander_interval: 12 - - id: man - wander_interval: 15 diff --git a/data/rooms/example_unused/town/9999930.yaml b/data/rooms/example_unused/town/9999930.yaml deleted file mode 100644 index af76ac6..0000000 --- a/data/rooms/example_unused/town/9999930.yaml +++ /dev/null @@ -1,13 +0,0 @@ -id: 9999930 -name: Above Town Square -color: "" -description: - - text: The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/underground2/9999950.yaml b/data/rooms/example_unused/underground2/9999950.yaml deleted file mode 100644 index 2a2d8d0..0000000 --- a/data/rooms/example_unused/underground2/9999950.yaml +++ /dev/null @@ -1,14 +0,0 @@ -id: 9999950 -name: The Assassin's Den -color: "" -description: - - text: A dimly lit basement accessible through a trapdoor. Tactical maps and bounty posters line the walls. The air smells of gun oil and burnt ozone. {141}The Client{/} sits behind a reinforced desk. -exits: {} -objects: [] -item_spawns: [] -mobs: - - id: client -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/underground2/9999951.yaml b/data/rooms/example_unused/underground2/9999951.yaml deleted file mode 100644 index 5f5dddb..0000000 --- a/data/rooms/example_unused/underground2/9999951.yaml +++ /dev/null @@ -1,16 +0,0 @@ -id: 9999951 -name: Sewer Tunnels -color: "" -description: - - text: Dark, damp tunnels beneath the settlement. The floor is slick with moisture and something {82}slimy{/}. The smell is indescribable. -exits: {} -objects: [] -item_spawns: [] -mobs: - - id: slug - - id: slug - - id: slug -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/underground2/9999952.yaml b/data/rooms/example_unused/underground2/9999952.yaml deleted file mode 100644 index 27ab51b..0000000 --- a/data/rooms/example_unused/underground2/9999952.yaml +++ /dev/null @@ -1,15 +0,0 @@ -id: 9999952 -name: Abandoned Sector -color: "" -description: - - text: A decommissioned sector of the asteroid's infrastructure. Broken monitors flicker and exposed wiring {214}sparks{/} dangerously. -exits: {} -objects: [] -item_spawns: [] -mobs: - - id: drone - - id: drone -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/underground2/9999953.yaml b/data/rooms/example_unused/underground2/9999953.yaml deleted file mode 100644 index 3cabbe7..0000000 --- a/data/rooms/example_unused/underground2/9999953.yaml +++ /dev/null @@ -1,17 +0,0 @@ -id: 9999953 -name: Deep Tunnels -color: "" -description: - - text: The tunnels descend deeper into the asteroid's core. Strange chitinous scraping echoes from the darkness. The walls are scarred with {130}claw marks{/}. -exits: - south: - room: 9999954 -objects: [] -item_spawns: [] -mobs: - - id: crawler - - id: crawler -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/underground2/9999954.yaml b/data/rooms/example_unused/underground2/9999954.yaml deleted file mode 100644 index cee1ccd..0000000 --- a/data/rooms/example_unused/underground2/9999954.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: "The Void Chamber" -description: -- text: "A vast cavern where reality seems to thin. The air shimmers with {141}spectral energy{/} and strange whispers fill your mind." -exits: - north: - room: 9999953 - down: - room: 9999955 -mobs: -- id: phantom -- id: phantom diff --git a/data/rooms/example_unused/underground2/9999955.yaml b/data/rooms/example_unused/underground2/9999955.yaml deleted file mode 100644 index f12e556..0000000 --- a/data/rooms/example_unused/underground2/9999955.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: "The Deeper Tunnels" -description: - - text: "A junction deep beneath the void chamber. The walls here are reinforced with pre-regression alloys. Several passages branch off into the darkness, and you can hear the sounds of machinery echoing from all directions. A steep shaft descends further into the depths." -exits: - up: - room: 9999954 - north: - room: 9999956 - east: - room: 9999957 - south: - room: 9999958 - west: - room: 9999960 - down: - room: 9999962 -mobs: - - id: security_turret diff --git a/data/rooms/example_unused/underground2/9999956.yaml b/data/rooms/example_unused/underground2/9999956.yaml deleted file mode 100644 index a82c068..0000000 --- a/data/rooms/example_unused/underground2/9999956.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Irradiated Sector" -description: - - text: "Warning symbols cover every surface, their paint long since faded. A faint green glow emanates from cracks in the walls. The air is thick with the taste of old radiation." -exits: - south: - room: 9999955 -mobs: - - id: ghoul - - id: ghoul - - id: corrupted_scientist diff --git a/data/rooms/example_unused/underground2/9999957.yaml b/data/rooms/example_unused/underground2/9999957.yaml deleted file mode 100644 index 501619d..0000000 --- a/data/rooms/example_unused/underground2/9999957.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: "Drone Assembly" -description: - - text: "A long-abandoned drone manufacturing bay. Conveyor belts lie still, and half-assembled drone chassis hang from the ceiling. A few of them still twitch with residual power." -exits: - west: - room: 9999955 -mobs: - - id: war_drone - - id: war_drone diff --git a/data/rooms/example_unused/underground2/9999958.yaml b/data/rooms/example_unused/underground2/9999958.yaml deleted file mode 100644 index 3b02c46..0000000 --- a/data/rooms/example_unused/underground2/9999958.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Security Checkpoint" -description: - - text: "A fortified security station with blast shields and a decommissioned weapons locker. Scorch marks on the walls tell the story of a last stand." -exits: - north: - room: 9999955 - south: - room: 9999959 -mobs: - - id: enforcer_bot diff --git a/data/rooms/example_unused/underground2/9999959.yaml b/data/rooms/example_unused/underground2/9999959.yaml deleted file mode 100644 index 65797eb..0000000 --- a/data/rooms/example_unused/underground2/9999959.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Plasma Core" -description: - - text: "A massive plasma containment chamber with cracked magnetic coils lining the walls. Streams of superheated gas leak from the containment vessel, forming shimmering wraith-like shapes in the air." -exits: - north: - room: 9999958 -mobs: - - id: plasma_wraith - - id: plasma_wraith - - id: feral_cyborg diff --git a/data/rooms/example_unused/underground2/9999960.yaml b/data/rooms/example_unused/underground2/9999960.yaml deleted file mode 100644 index 12c050d..0000000 --- a/data/rooms/example_unused/underground2/9999960.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Heavy Mech Bay" -description: - - text: "An enormous maintenance bay built to house assault mechs. A gantry crane hangs overhead, and hydraulic lifts dot the floor. The bay's occupant is still active — and it has noticed you." -exits: - east: - room: 9999955 - west: - room: 9999961 -mobs: - - id: heavy_mech diff --git a/data/rooms/example_unused/underground2/9999961.yaml b/data/rooms/example_unused/underground2/9999961.yaml deleted file mode 100644 index e9124d8..0000000 --- a/data/rooms/example_unused/underground2/9999961.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: "Synth Lab" -description: - - text: "A sterile laboratory where combat androids were designed and assembled. White panels line the walls, some splattered with old coolant stains. Diagnostic screens flicker with corrupted data." -exits: - east: - room: 9999960 -mobs: - - id: elite_synth diff --git a/data/rooms/example_unused/underground2/9999962.yaml b/data/rooms/example_unused/underground2/9999962.yaml deleted file mode 100644 index 1e41704..0000000 --- a/data/rooms/example_unused/underground2/9999962.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Abandoned Bunker" -description: - - text: "A fortified bunker from some forgotten conflict. Sandbags are piled in the corners and spent shell casings litter the floor. A solitary figure stands watch over the remains." -exits: - up: - room: 9999955 - down: - room: 9999963 -mobs: - - id: veteran_merc diff --git a/data/rooms/example_unused/underground2/9999963.yaml b/data/rooms/example_unused/underground2/9999963.yaml deleted file mode 100644 index 5963fd2..0000000 --- a/data/rooms/example_unused/underground2/9999963.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Magma Conduit" -description: - - text: "A geothermal vent has been tapped and routed through industrial conduits. The heat is oppressive, and molten rivulets course through channels in the floor. Something moves within the glowing flow." -exits: - up: - room: 9999962 - down: - room: 9999964 -mobs: - - id: molten_elemental diff --git a/data/rooms/example_unused/underground2/9999964.yaml b/data/rooms/example_unused/underground2/9999964.yaml deleted file mode 100644 index 646ad15..0000000 --- a/data/rooms/example_unused/underground2/9999964.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "The Foundry" -description: - - text: "A vast smelting facility where sentinels were once forged. Massive crucibles hang from chains, and rivers of cooled slag form a maze-like floor. A single iron sentinel stands guard over the dormant machinery." -exits: - up: - room: 9999963 - down: - room: 9999965 -mobs: - - id: iron_sentinel diff --git a/data/rooms/example_unused/underground2/9999965.yaml b/data/rooms/example_unused/underground2/9999965.yaml deleted file mode 100644 index 64b83de..0000000 --- a/data/rooms/example_unused/underground2/9999965.yaml +++ /dev/null @@ -1,14 +0,0 @@ -id: 9999965 -name: Titan's Rest -color: "" -description: - - text: A colossal vault built to house something immense. The ceiling vanishes into darkness above, and the walls are etched with maintenance schematics for a machine of staggering proportions. At the center, a titan sentinel awakens. -exits: {} -objects: [] -item_spawns: [] -mobs: - - id: titan_sentinel -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/underground2/9999966.yaml b/data/rooms/example_unused/underground2/9999966.yaml deleted file mode 100644 index f2da0b2..0000000 --- a/data/rooms/example_unused/underground2/9999966.yaml +++ /dev/null @@ -1,14 +0,0 @@ -id: 9999966 -name: Psionic Nexus -color: "" -description: - - text: Reality grows thin here. The walls are covered in strange geometric patterns that hurt to look at directly. A low hum vibrates through your skull, and the air seems to bend around a central point of unseen pressure. -exits: {} -objects: [] -item_spawns: [] -mobs: - - id: psionic_entity -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/underground2/9999967.yaml b/data/rooms/example_unused/underground2/9999967.yaml deleted file mode 100644 index 28ab6ef..0000000 --- a/data/rooms/example_unused/underground2/9999967.yaml +++ /dev/null @@ -1,14 +0,0 @@ -id: 9999967 -name: Ancient Chamber -color: "" -description: - - text: A perfectly spherical room carved from an unknown black metal. Glowing runes spiral across every surface in patterns that predate the asteroid itself. At the center, an ancient construct stirs from its eons-long vigil. -exits: {} -objects: [] -item_spawns: [] -mobs: - - id: ancient_construct -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/example_unused/underground2/9999968.yaml b/data/rooms/example_unused/underground2/9999968.yaml deleted file mode 100644 index 1e9687c..0000000 --- a/data/rooms/example_unused/underground2/9999968.yaml +++ /dev/null @@ -1,14 +0,0 @@ -id: 9999968 -name: The Void -color: "" -description: - - text: The bottom of the world. Light itself seems to hesitate before entering this place. The floor is unnaturally smooth black glass, and the only sound is the beating of your own heart. Something waits in the darkness — something that was here before the asteroid, before the colony, before anything. -exits: {} -objects: [] -item_spawns: [] -mobs: - - id: void_reaper -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/intro/2001.yaml b/data/rooms/intro/2001.yaml index eb7bd59..0f56257 100644 --- a/data/rooms/intro/2001.yaml +++ b/data/rooms/intro/2001.yaml @@ -1,5 +1,4 @@ -id: 2001 -name: Test Room +block_transport: false color: "" description: - text: It's a test room @@ -12,12 +11,8 @@ exits: room: 2005 west: room: 1001 -objects: - - name: hidden guy - hidden: true - description: - - text: YOU FOUND HIM - - id: tree +hazard: "" +id: 2001 item_spawns: - id: bronze_axe quantity: 1 @@ -30,7 +25,12 @@ item_spawns: respawn_ticks: 1 mobs: - id: man +name: Test Room +objects: + - description: + - text: YOU FOUND HIM + hidden: true + name: hidden guy + - id: tree on_enter: [] -hazard: "" -block_transport: false triggers: [] diff --git a/data/rooms/intro/2002.yaml b/data/rooms/intro/2002.yaml index 2ac6ef1..cb7fc97 100644 --- a/data/rooms/intro/2002.yaml +++ b/data/rooms/intro/2002.yaml @@ -1,19 +1,21 @@ -id: 2002 -name: Test Room -color: "" +block_transport: false +color: "31" description: - text: It's a test room exits: east: room: 2003 + south: + room: 2005 southeast: room: 2004 west: room: 2001 -objects: [] +hazard: solar_radiation +id: 2002 item_spawns: [] mobs: [] +name: Test Room +objects: [] on_enter: [] -hazard: "" -block_transport: false triggers: [] diff --git a/data/rooms/intro/2003.yaml b/data/rooms/intro/2003.yaml index 6ca12a1..fa7855b 100644 --- a/data/rooms/intro/2003.yaml +++ b/data/rooms/intro/2003.yaml @@ -1,6 +1,5 @@ -id: 2003 -name: Test Room -color: A5 +block_transport: false +color: "31" description: - text: It's a test room exits: @@ -10,10 +9,11 @@ exits: room: 2005 west: room: 2002 -objects: [] +hazard: solar_radiation +id: 2003 item_spawns: [] mobs: [] +name: Test Room +objects: [] on_enter: [] -hazard: "" -block_transport: false triggers: [] diff --git a/data/rooms/intro/2004.yaml b/data/rooms/intro/2004.yaml index 73d912e..4426c98 100644 --- a/data/rooms/intro/2004.yaml +++ b/data/rooms/intro/2004.yaml @@ -1,6 +1,5 @@ -id: 2004 -name: Test Room -color: "" +block_transport: false +color: "31" description: - text: It's a test room exits: @@ -10,10 +9,11 @@ exits: room: 2002 west: room: 2005 -objects: [] +hazard: solar_radiation +id: 2004 item_spawns: [] mobs: [] +name: Test Room +objects: [] on_enter: [] -hazard: "" -block_transport: false triggers: [] diff --git a/data/rooms/intro/2005.yaml b/data/rooms/intro/2005.yaml index 7af3fd2..a5368c5 100644 --- a/data/rooms/intro/2005.yaml +++ b/data/rooms/intro/2005.yaml @@ -1,21 +1,23 @@ -id: 2005 -name: Test Room -color: "" +block_transport: false +color: "31" description: - - text: It's a test room + - text: It's a test room exits: - east: - room: 2004 - northeast: - room: 2003 - northwest: - room: 2001 - west: - room: 2006 -objects: [] + east: + room: 2004 + north: + room: 2002 + northeast: + room: 2003 + northwest: + room: 2001 + west: + room: 2006 +hazard: solar_radiation +id: 2005 item_spawns: [] mobs: [] +name: Test Room +objects: [] on_enter: [] -hazard: "" -block_transport: false triggers: [] diff --git a/data/rooms/intro/2006.yaml b/data/rooms/intro/2006.yaml index 7ab682f..993216c 100644 --- a/data/rooms/intro/2006.yaml +++ b/data/rooms/intro/2006.yaml @@ -1,8 +1,9 @@ -name: "Test Room" +block_transport: false description: - - text: "It's a test room" + - text: It's a test room exits: east: room: 2005 north: room: 2001 +name: Test Room diff --git a/data/rooms/test222/1.yaml b/data/rooms/test222/1.yaml deleted file mode 100644 index 7834414..0000000 --- a/data/rooms/test222/1.yaml +++ /dev/null @@ -1,13 +0,0 @@ -id: 1 -name: New Room -color: "" -description: - - text: An empty room. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go index 3705cde..f5fd9ee 100644 --- a/internal/admin/api_map.go +++ b/internal/admin/api_map.go @@ -158,11 +158,14 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) { } type RoomEntry struct { - ID int `json:"id"` - X int `json:"x"` - Y int `json:"y"` - Name string `json:"name"` - Color string `json:"color"` + ID int `json:"id"` + X int `json:"x"` + Y int `json:"y"` + Name string `json:"name"` + Color string `json:"color"` + Dir string `json:"dir"` + BlockTransport bool `json:"block_transport"` + Hazard string `json:"hazard"` } type LinkEntry struct { @@ -198,11 +201,14 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) { roomData[rid] = r rooms = append(rooms, RoomEntry{ - ID: rid, - X: coord[0], - Y: coord[1], - Name: r.Name, - Color: r.Color, + ID: rid, + X: coord[0], + Y: coord[1], + Name: r.Name, + Color: r.Color, + Dir: getRoomDir(s, rid), + BlockTransport: r.BlockTransport, + Hazard: r.Hazard, }) if first { diff --git a/internal/admin/api_rooms_bulk.go b/internal/admin/api_rooms_bulk.go new file mode 100644 index 0000000..34ca209 --- /dev/null +++ b/internal/admin/api_rooms_bulk.go @@ -0,0 +1,214 @@ +package admin + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strconv" +) + +type bulkEditPatch struct { + Color *string `json:"color"` + BlockTransport *bool `json:"block_transport"` + Dir *string `json:"dir"` + Hazard *string `json:"hazard"` +} + +type bulkEditRequest struct { + Patch bulkEditPatch `json:"patch"` + IDs []int `json:"ids"` +} + +func (s *AdminServer) handleBulkEdit(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + body, readErr := io.ReadAll(r.Body) + r.Body.Close() + if readErr != nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("read body: %v", readErr)}) + return + } + + var req bulkEditRequest + if err := json.Unmarshal(body, &req); err != nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid json: %v", err)}) + return + } + + if len(req.IDs) == 0 { + writeJSON(w, map[string]any{"error": "no room ids"}) + return + } + + if isEmptyPatch(req.Patch) { + writeJSON(w, map[string]any{"error": "no patch fields"}) + return + } + + patch := req.Patch + + roomPaths := make([]string, 0, len(req.IDs)) + for _, id := range req.IDs { + path, ok := s.world.GetRoomPath(id) + if !ok { + writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d not found", id)}) + return + } + roomPaths = append(roomPaths, path) + } + + type roomWrite struct { + oldContent []byte + newContent []byte + oldPath string + newPath string + wasMoved bool + } + + writes := make([]roomWrite, 0, len(req.IDs)) + dirMovePaths := make(map[string]struct{}) + + for i, id := range req.IDs { + path := roomPaths[i] + + oldContent, snapErr := snapshotFile(path) + if snapErr != nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("snapshot room %d: %v", id, snapErr)}) + return + } + + data, readErr := os.ReadFile(path) + if readErr != nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("read room %d: %v", id, readErr)}) + return + } + + m, yamlErr := yamlToMap(data) + if yamlErr != nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("parse room %d: %v", id, yamlErr)}) + return + } + + if patch.Color != nil { + m["color"] = *patch.Color + } + if patch.BlockTransport != nil { + m["block_transport"] = *patch.BlockTransport + } + if patch.Hazard != nil { + if *patch.Hazard == "" { + delete(m, "hazard") + } else { + m["hazard"] = *patch.Hazard + } + } + + if patch.Dir != nil && *patch.Dir != "" { + destDir := filepath.Join(s.dataDir, "rooms", *patch.Dir) + if _, err := os.Stat(destDir); os.IsNotExist(err) { + writeJSON(w, map[string]any{"error": fmt.Sprintf("directory does not exist: %s", *patch.Dir)}) + return + } + + fileName := strconv.Itoa(id) + ".yaml" + newPath := filepath.Join(destDir, fileName) + if newPath != path { + if _, err := os.Stat(newPath); err == nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("room %d already exists in target directory", id)}) + return + } + } + + newContent, writeErr := writeMapAsYAML(newPath, m) + if writeErr != nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("write room %d: %v", id, writeErr)}) + return + } + + writes = append(writes, roomWrite{ + oldContent: oldContent, + newContent: newContent, + oldPath: path, + newPath: newPath, + wasMoved: true, + }) + dirMovePaths[path] = struct{}{} + } else { + newContent, writeErr := writeMapAsYAML(path, m) + if writeErr != nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("write room %d: %v", id, writeErr)}) + return + } + + writes = append(writes, roomWrite{ + oldContent: oldContent, + newContent: newContent, + oldPath: path, + newPath: path, + wasMoved: false, + }) + } + } + + for _, rw := range writes { + if rw.wasMoved { + if _, alreadyRemoved := dirMovePaths[rw.oldPath]; alreadyRemoved { + delete(dirMovePaths, rw.oldPath) + if err := os.Remove(rw.oldPath); err != nil { + writeJSON(w, map[string]any{"error": fmt.Sprintf("remove old room: %v", err)}) + return + } + } + } + } + + s.world.RebuildRoomIndex(s.dataDir) + + var desc string + if patch.Dir != nil && *patch.Dir != "" { + desc = fmt.Sprintf("bulk edit %d rooms (move to %s)", len(req.IDs), *patch.Dir) + } else { + desc = fmt.Sprintf("bulk edit %d rooms", len(req.IDs)) + } + + var extraFiles []ExtraFile + var firstDesc ChangeDesc + for i, rw := range writes { + ef := ExtraFile{ + FilePath: rw.oldPath, + OldContent: rw.oldContent, + NewContent: rw.newContent, + } + if rw.wasMoved { + ef.NewFilePath = rw.newPath + } + if i == 0 { + firstDesc = ChangeDesc{ + Description: desc, + FilePath: rw.oldPath, + OldContent: rw.oldContent, + NewContent: rw.newContent, + } + if rw.wasMoved { + firstDesc.NewFilePath = rw.newPath + } + } else { + extraFiles = append(extraFiles, ef) + } + } + + firstDesc.ExtraFiles = extraFiles + s.undoStack.Push(firstDesc) + + writeJSON(w, map[string]any{"ok": true, "count": len(req.IDs)}) +} + +func isEmptyPatch(p bulkEditPatch) bool { + return p.Color == nil && p.BlockTransport == nil && p.Dir == nil && p.Hazard == nil +} diff --git a/internal/admin/server.go b/internal/admin/server.go index f06b7cf..c33bd21 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -125,6 +125,7 @@ func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStor apiMux.HandleFunc("/api/rooms/remove", s.handleRoomRemove) apiMux.HandleFunc("/api/rooms/move", s.handleRoomMove) apiMux.HandleFunc("/api/rooms/rename", s.handleRoomRename) + apiMux.HandleFunc("/api/rooms/bulk-edit", s.handleBulkEdit) apiMux.HandleFunc("/api/rooms", s.handleRooms) apiMux.HandleFunc("/api/rooms/new-empty", s.handleCreateEmptyRoom) apiMux.HandleFunc("/api/rooms/", s.handleRoomByID) diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index aa80488..393fafd 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -55,6 +55,10 @@ body{font-family:monospace;background:var(--bg);color:var(--text);display:flex;f .link-mode-badge.oneway{background:rgba(80,50,20,0.92);color:#fca;border:1px solid #a64} #mapSvg:focus{outline:none} #mapSvg,#mapSvg *{user-select:none;-webkit-user-select:none;-moz-user-select:none} +.selection-box{fill:rgba(100,200,255,0.1);stroke:#5cf;stroke-width:1.5;stroke-dasharray:5,5;pointer-events:none} +.selection-box.subtract{fill:rgba(255,80,80,0.1);stroke:#f55} +.rm.multisel{cursor:pointer} +.bulk-swatch{background:#888!important} .notification{position:fixed;bottom:16px;left:16px;background:var(--panel);border:1px solid var(--border);padding:10px 16px;border-radius:4px;font-size:12px;z-index:100;animation:fadeIn .2s} .notification.error{border-color:var(--danger)} .notification.success{border-color:var(--success)} diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js index aeb2d57..1f67275 100644 --- a/internal/admin/static/map.js +++ b/internal/admin/static/map.js @@ -9,6 +9,8 @@ var ctrlHeld = false; var delOneWay = false; var delDrag = false, delFromId = null, delTargetId = null; var roomMap = {}, occupied = {}; +var multiSelectedIds = new Set(); +var multiDrag = false, multiSubtract = false, multiStartSVGX = 0, multiStartSVGY = 0; var dirChangedByUser = false; var currentPanelTab = 0; var gridDirs = [ @@ -33,6 +35,10 @@ function isCtrlKey(e) { } function onMapMouseMove(e) { + if (multiDrag) { + updateSelectionBox(e); + return; + } if (!dragging) return; var moved = Math.abs(e.clientX - startX) >= 4 || Math.abs(e.clientY - startY) >= 4; @@ -65,6 +71,13 @@ function onMapMouseUp(e) { document.removeEventListener('mouseup', onMapMouseUp); _mapMouseCleanup = false; + if (multiDrag) { + finalizeMultiSelect(e); + multiDrag = false; multiSubtract = false; dragging = false; dragRoom = false; + if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); + return; + } + var moved = Math.abs(e.clientX - startX) >= 4 || Math.abs(e.clientY - startY) >= 4; if (delFromId && !delDrag && !moved) { @@ -113,7 +126,9 @@ function cancelMapDrag() { clearLinkHighlight(); clearDelHighlight(); hideDragBadge(); + removeSelectionBox(); dragging = false; dragRoom = false; + multiDrag = false; multiSubtract = false; linkDrag = false; linkFromId = null; linkTargetId = null; linkTargetDir = null; linkGhostDir = null; linkDragOneWay = false; delDrag = false; delFromId = null; delTargetId = null; delOneWay = false; if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); @@ -153,6 +168,7 @@ function refreshDragMode(ctrlDown) { function changeZ(dz) { if (dz === 0) currentZ = 0; else currentZ += dz; panX = 0; panY = 0; scale = 1; + clearMultiSelectSilent(); loadMap(); } @@ -161,6 +177,7 @@ function changeDir(dir) { dirChangedByUser = true; selectedRoom = null; panX = 0; panY = 0; scale = 1; + clearMultiSelectSilent(); loadMap(); } @@ -279,7 +296,12 @@ function renderMap(data) { var fill = resolveColor(r.color); var txtColor = hexLuminance(fill) > 0.35 ? '#111' : '#fff'; var sel = selectedRoom === r.id; - roomHTML += ''; + var multi = multiSelectedIds.has(r.id); + var stroke = '#fff', strokeW = 3; + if (sel && !multi) { stroke = '#fff'; strokeW = 3; } + else if (multi) { stroke = '#5cf'; strokeW = 3.5; } + else { stroke = fill; strokeW = 1.5; } + roomHTML += ''; var dimColor = hexLuminance(fill) > 0.35 ? 'rgba(0,0,0,0.45)' : 'rgba(255,255,255,0.4)'; roomHTML += '#'+r.id+''; @@ -331,6 +353,7 @@ function renderMap(data) { svg.setAttribute('tabindex', '0'); svg.addEventListener('keydown', function(e) { if (isCtrlKey(e)) { ctrlHeld = true; if (dragging) refreshDragMode(true); } + if (e.key === 'Escape' && multiSelectedIds.size > 0) { clearMultiSelect(); e.preventDefault(); } }); svg.addEventListener('keyup', function(e) { if (isCtrlKey(e)) { ctrlHeld = false; if (dragging) refreshDragMode(false); } @@ -345,6 +368,10 @@ function renderMap(data) { var onUdBtn = e.target.classList.contains('ud-btn'); var onNavBtn = e.target.classList.contains('nav-btn'); + if (!e.shiftKey && multiSelectedIds.size > 0) { + clearMultiSelect(); + } + if (onNavBtn) { e.stopPropagation(); var dz = parseInt(e.target.getAttribute('data-dz')); @@ -354,6 +381,26 @@ function renderMap(data) { return; } + if (e.shiftKey && e.button === 2) { + e.preventDefault(); + multiDrag = true; + multiSubtract = true; + dragging = true; + if (svg.focus) svg.focus(); + startX = e.clientX; startY = e.clientY; + prevX = e.clientX; prevY = e.clientY; + var coords = screenToSVGCoords(e); + multiStartSVGX = coords.svgX; multiStartSVGY = coords.svgY; + dragRoom = true; + linkFromId = null; linkDrag = false; linkTargetId = null; linkGhostDir = null; + delDrag = false; delFromId = null; delTargetId = null; + clearLinkHighlight(); clearDelHighlight(); + document.addEventListener('mousemove', onMapMouseMove); + document.addEventListener('mouseup', onMapMouseUp); + _mapMouseCleanup = true; + return; + } + if (e.button === 2 && onRoom) { delFromId = parseInt(e.target.getAttribute('data-id')); delDrag = false; delTargetId = null; delOneWay = false; @@ -378,6 +425,24 @@ function renderMap(data) { return; } + if (e.shiftKey) { + multiDrag = true; + dragging = true; + if (svg.focus) svg.focus(); + startX = e.clientX; startY = e.clientY; + prevX = e.clientX; prevY = e.clientY; + var coords = screenToSVGCoords(e); + multiStartSVGX = coords.svgX; multiStartSVGY = coords.svgY; + dragRoom = true; + linkFromId = null; linkDrag = false; linkTargetId = null; linkGhostDir = null; + delDrag = false; delFromId = null; delTargetId = null; + clearLinkHighlight(); clearDelHighlight(); + document.addEventListener('mousemove', onMapMouseMove); + document.addEventListener('mouseup', onMapMouseUp); + _mapMouseCleanup = true; + return; + } + dragRoom = onRoom || onGhost; dragging = true; if (svg.focus) svg.focus(); @@ -450,6 +515,8 @@ function selectRoom(id) { if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } var prev = selectedRoom; selectedRoom = id; + multiSelectedIds.clear(); + removeSelectionBox(); if (mapData && prev !== id) { renderMap(mapData); @@ -2885,6 +2952,284 @@ function reloadRoomDirs(cb) { }); } +function screenToSVGCoords(e) { + var svgEl = $('#mapSvg'); + if (!svgEl) return null; + var rect = svgEl.getBoundingClientRect(); + var mx = e.clientX - rect.left; + var my = e.clientY - rect.top; + var vb = svgEl.getAttribute('viewBox'); + if (!vb) return null; + var parts = vb.split(' '); + var vbX = parseFloat(parts[0]), vbY = parseFloat(parts[1]); + var vbW = parseFloat(parts[2]), vbH = parseFloat(parts[3]); + var svgX = vbX + mx * vbW / rect.width; + var svgY = vbY + my * vbH / rect.height; + return { + svgX: svgX, svgY: svgY, + gridX: Math.floor(svgX / CELL), + gridY: Math.floor(svgY / CELL) + }; +} + +function updateSelectionBox(e) { + var coords = screenToSVGCoords(e); + if (!coords) return; + + var g = document.querySelector('#mapSvg g'); + if (!g) return; + + var x1 = Math.min(multiStartSVGX, coords.svgX); + var y1 = Math.min(multiStartSVGY, coords.svgY); + var x2 = Math.max(multiStartSVGX, coords.svgX); + var y2 = Math.max(multiStartSVGY, coords.svgY); + + var box = document.getElementById('__multi_box'); + if (!box) { + var ns = 'http://www.w3.org/2000/svg'; + box = document.createElementNS(ns, 'rect'); + box.id = '__multi_box'; + box.classList.add('selection-box'); + g.appendChild(box); + } + box.setAttribute('x', x1); + box.setAttribute('y', y1); + box.setAttribute('width', x2 - x1); + box.setAttribute('height', y2 - y1); + if (multiSubtract) { + box.classList.add('subtract'); + } else { + box.classList.remove('subtract'); + } + + var startCoords = {gridX: Math.floor(multiStartSVGX/CELL), gridY: Math.floor(multiStartSVGY/CELL)}; + var gx1 = Math.min(startCoords.gridX, coords.gridX); + var gy1 = Math.min(startCoords.gridY, coords.gridY); + var gx2 = Math.max(startCoords.gridX, coords.gridX); + var gy2 = Math.max(startCoords.gridY, coords.gridY); + + var allRms = document.querySelectorAll('.rm'); + for (var r = 0; r < allRms.length; r++) { + var rm = allRms[r]; + var rid = parseInt(rm.getAttribute('data-id')); + if (multiSelectedIds.has(rid)) { + rm.setAttribute('stroke', '#5cf'); + rm.setAttribute('stroke-width', '3.5'); + } else { + var fill = rm.getAttribute('fill'); + rm.setAttribute('stroke', fill); + rm.setAttribute('stroke-width', '1.5'); + } + } + + for (var gx = gx1; gx <= gx2; gx++) { + for (var gy = gy1; gy <= gy2; gy++) { + var rId = occupied[gx+','+gy]; + if (rId === undefined) continue; + var rm = document.querySelector('.rm[data-id="'+rId+'"]'); + if (!rm) continue; + if (multiSubtract) { + if (multiSelectedIds.has(rId)) { + rm.setAttribute('stroke', '#f55'); + rm.setAttribute('stroke-width', '3.5'); + } + } else { + if (!multiSelectedIds.has(rId)) { + rm.setAttribute('stroke', '#5cf'); + rm.setAttribute('stroke-width', '3.5'); + } + } + } + } +} + +function removeSelectionBox() { + var box = document.getElementById('__multi_box'); + if (box) box.remove(); +} + +function finalizeMultiSelect(e) { + var coords = screenToSVGCoords(e); + removeSelectionBox(); + + if (!coords) return; + + var startCoords = {gridX: Math.floor(multiStartSVGX/CELL), gridY: Math.floor(multiStartSVGY/CELL)}; + var gx1 = Math.min(startCoords.gridX, coords.gridX); + var gy1 = Math.min(startCoords.gridY, coords.gridY); + var gx2 = Math.max(startCoords.gridX, coords.gridX); + var gy2 = Math.max(startCoords.gridY, coords.gridY); + + var changed = false; + for (var gx = gx1; gx <= gx2; gx++) { + for (var gy = gy1; gy <= gy2; gy++) { + var key = gx+','+gy; + if (occupied[key] !== undefined) { + if (multiSubtract) { + if (multiSelectedIds.delete(occupied[key])) changed = true; + } else { + if (!multiSelectedIds.has(occupied[key])) { + multiSelectedIds.add(occupied[key]); + changed = true; + } + } + } + } + } + + if (multiSelectedIds.size === 0) { + clearMultiSelect(); + return; + } + + selectedRoom = null; + if (mapData) renderMap(mapData); + renderBulkPanel(); +} + +function clearMultiSelect() { + multiSelectedIds.clear(); + multiSubtract = false; + removeSelectionBox(); + if (mapData) renderMap(mapData); + var panel = $('#panelContent'); + if (panel) panel.innerHTML = '

Click a room to view details

'; +} + +function clearMultiSelectSilent() { + multiSelectedIds.clear(); + multiSubtract = false; + removeSelectionBox(); + var panel = $('#panelContent'); + if (panel) panel.innerHTML = '

Click a room to view details

'; +} + +function renderBulkPanel() { + if (multiSelectedIds.size === 0) return; + var panel = $('#panelContent'); + if (!panel) return; + + var ids = Array.from(multiSelectedIds); + var rooms = ids.map(function(id) { return roomMap[id]; }).filter(Boolean); + + var colorMixed = false, firstColor = rooms.length > 0 ? (rooms[0].color || '') : ''; + var blockNone = true, blockAll = true; + var dirMixedSet = new Set(); + var hazardsByRoom = {}; + + rooms.forEach(function(r) { + if ((r.color || '') !== firstColor) colorMixed = true; + if (r.block_transport) blockNone = false; else blockAll = false; + if (r.dir) dirMixedSet.add(r.dir); + var h = r.hazard || ''; + if (h) { + if (!hazardsByRoom[h]) hazardsByRoom[h] = []; + hazardsByRoom[h].push(r.id); + } + }); + + var hazardNames = Object.keys(hazardsByRoom); + + var html = '

' + ids.length + ' rooms selected

'; + html += '
'; + + var colorVal = colorMixed ? '' : firstColor; + var swatchBg = colorVal ? resolveColor(colorVal) : '#888'; + html += '
'; + + html += '
'; + + html += '
'; + + html += ''; + + if (hazardNames.length > 0) { + hazardNames.forEach(function(h) { + html += '
' + esc(h) + '
'; + }); + } + + html += '
'; + panel.innerHTML = html; + + var preloadDirs = dirMixedSet.size === 1 ? dirMixedSet.values().next().value : null; + + setTimeout(function() { + var cf = document.querySelector('#panelContent .color-field'); + if (cf) { + bindColorField(cf); + var ci = cf.querySelector('input'); + if (ci) ci.addEventListener('blur', function() { bulkColorChanged(); }); + } + + var blockCB = document.getElementById('bulkBlockTransport'); + if (blockCB) { + if (blockNone) { blockCB.checked = false; blockCB.indeterminate = false; } + else if (blockAll) { blockCB.checked = true; blockCB.indeterminate = false; } + else { blockCB.indeterminate = true; } + } + + loadRoomDirs(function(dirs) { + var sel = $('#bulkRoomDir'); + if (!sel) return; + sel.innerHTML = ''; + var mixedOpt = document.createElement('option'); + mixedOpt.value = ''; + mixedOpt.textContent = 'Mixed\u2026'; + if (!preloadDirs) mixedOpt.selected = true; + sel.appendChild(mixedOpt); + dirs.forEach(function(d) { + var opt = document.createElement('option'); + opt.value = d; + opt.textContent = d || '(root)'; + if (preloadDirs && d === preloadDirs) opt.selected = true; + sel.appendChild(opt); + }); + }); + }, 50); +} + +function bulkColorChanged() { + if (window._colorPickerActive) return; + var el = $('#bulkRoomColor'); + if (!el) return; + var v = el.value; + if (v.length === 2 || v.length === 0) { + doBulkSave({color: v}); + } +} + +function bulkBlockChanged() { + var el = document.getElementById('bulkBlockTransport'); + if (!el) return; + doBulkSave({block_transport: el.checked}); +} + +function searchBulkHazards(input) { + searchAndShow(input, 'hazards', function(r) { doBulkSave({hazard: r.id}); }); +} + +function doBulkSave(patch, customIds) { + var ids = customIds || Array.from(multiSelectedIds); + if (ids.length === 0) return; + + if (patch.hasOwnProperty('hazard') && patch.hazard === null) delete patch.hazard; + if (patch.hasOwnProperty('dir') && (patch.dir === '' || patch.dir === null)) delete patch.dir; + if (!patch.hasOwnProperty('hazard') && !patch.hasOwnProperty('block_transport') && !patch.hasOwnProperty('color') && !patch.hasOwnProperty('dir')) return; + + API.post('/api/rooms/bulk-edit', {patch: patch, ids: ids}).then(function(res) { + if (res.error) { notify(res.error, 'error'); return; } + notify('Updated ' + res.count + ' rooms', 'success'); + updateUndoBar(); + loadMap().then(function() { renderBulkPanel(); }); + }).catch(function(e) { + notify('Bulk update failed: ' + extractError(e), 'error'); + }); +} + document.addEventListener('DOMContentLoaded', function() { loadRoomDirs(function() { loadMap(); }); initPanelResize(); window.addEventListener('keydown', function(e) { if (isCtrlKey(e)) { ctrlHeld = true; if (dragging) refreshDragMode(true); } }); window.addEventListener('keyup', function(e) { if (isCtrlKey(e)) { ctrlHeld = false; if (dragging) refreshDragMode(false); } }); diff --git a/internal/admin/undo.go b/internal/admin/undo.go index 4c39ec6..53af942 100644 --- a/internal/admin/undo.go +++ b/internal/admin/undo.go @@ -11,9 +11,12 @@ import ( ) type ExtraFile struct { - FilePath string `json:"file_path"` - OldContent []byte `json:"-"` - NewContent []byte `json:"-"` + FilePath string `json:"file_path"` + OldContent []byte `json:"-"` + NewContent []byte `json:"-"` + NewFilePath string `json:"new_file_path,omitempty"` + IsDelete bool `json:"is_delete,omitempty"` + IsCreate bool `json:"is_create,omitempty"` } type ChangeDesc struct { @@ -140,14 +143,12 @@ func (us *UndoStack) applyUndo(c ChangeDesc) error { } } for _, ef := range c.ExtraFiles { - os.MkdirAll(filepath.Dir(ef.FilePath), 0755) - os.WriteFile(ef.FilePath, ef.OldContent, 0644) + applyUndoExtraFile(ef) } } else if c.IsCreate { os.Remove(c.FilePath) for _, ef := range c.ExtraFiles { - os.MkdirAll(filepath.Dir(ef.FilePath), 0755) - os.WriteFile(ef.FilePath, ef.OldContent, 0644) + applyUndoExtraFile(ef) } } else { target := c.FilePath @@ -160,8 +161,7 @@ func (us *UndoStack) applyUndo(c ChangeDesc) error { return fmt.Errorf("revert %s: %w", target, err) } for _, ef := range c.ExtraFiles { - os.MkdirAll(filepath.Dir(ef.FilePath), 0755) - os.WriteFile(ef.FilePath, ef.OldContent, 0644) + applyUndoExtraFile(ef) } } return nil @@ -181,14 +181,12 @@ func (us *UndoStack) applyRedo(c ChangeDesc) error { return fmt.Errorf("recreate %s: %w", target, err) } for _, ef := range c.ExtraFiles { - os.MkdirAll(filepath.Dir(ef.FilePath), 0755) - os.WriteFile(ef.FilePath, ef.NewContent, 0644) + applyRedoExtraFile(ef) } } else if c.IsDelete { os.Remove(c.FilePath) for _, ef := range c.ExtraFiles { - os.MkdirAll(filepath.Dir(ef.FilePath), 0755) - os.WriteFile(ef.FilePath, ef.NewContent, 0644) + applyRedoExtraFile(ef) } } else { target := c.FilePath @@ -201,13 +199,46 @@ func (us *UndoStack) applyRedo(c ChangeDesc) error { return fmt.Errorf("reapply %s: %w", target, err) } for _, ef := range c.ExtraFiles { - os.MkdirAll(filepath.Dir(ef.FilePath), 0755) - os.WriteFile(ef.FilePath, ef.NewContent, 0644) + applyRedoExtraFile(ef) } } return nil } +func applyUndoExtraFile(ef ExtraFile) { + switch { + case ef.IsCreate: + os.Remove(ef.FilePath) + case ef.IsDelete: + os.MkdirAll(filepath.Dir(ef.FilePath), 0755) + os.WriteFile(ef.FilePath, ef.OldContent, 0644) + case ef.NewFilePath != "": + os.Remove(ef.NewFilePath) + os.MkdirAll(filepath.Dir(ef.FilePath), 0755) + os.WriteFile(ef.FilePath, ef.OldContent, 0644) + default: + os.MkdirAll(filepath.Dir(ef.FilePath), 0755) + os.WriteFile(ef.FilePath, ef.OldContent, 0644) + } +} + +func applyRedoExtraFile(ef ExtraFile) { + switch { + case ef.IsDelete: + os.Remove(ef.FilePath) + case ef.IsCreate: + os.MkdirAll(filepath.Dir(ef.FilePath), 0755) + os.WriteFile(ef.FilePath, ef.NewContent, 0644) + case ef.NewFilePath != "": + os.Remove(ef.FilePath) + os.MkdirAll(filepath.Dir(ef.NewFilePath), 0755) + os.WriteFile(ef.NewFilePath, ef.NewContent, 0644) + default: + os.MkdirAll(filepath.Dir(ef.FilePath), 0755) + os.WriteFile(ef.FilePath, ef.NewContent, 0644) + } +} + func (us *UndoStack) Info() UndoInfo { us.mu.Lock() defer us.mu.Unlock() @@ -228,9 +259,12 @@ func (us *UndoStack) Info() UndoInfo { func (us *UndoStack) save() { type extraEntry struct { - FilePath string `json:"file_path"` - OldContent string `json:"old_content"` - NewContent string `json:"new_content"` + FilePath string `json:"file_path"` + NewFilePath string `json:"new_file_path,omitempty"` + OldContent string `json:"old_content"` + NewContent string `json:"new_content"` + IsDelete bool `json:"is_delete,omitempty"` + IsCreate bool `json:"is_create,omitempty"` } type entry struct { Time string `json:"time"` @@ -253,9 +287,12 @@ func (us *UndoStack) save() { var extra []extraEntry for _, ef := range c.ExtraFiles { extra = append(extra, extraEntry{ - FilePath: ef.FilePath, - OldContent: string(ef.OldContent), - NewContent: string(ef.NewContent), + FilePath: ef.FilePath, + NewFilePath: ef.NewFilePath, + OldContent: string(ef.OldContent), + NewContent: string(ef.NewContent), + IsDelete: ef.IsDelete, + IsCreate: ef.IsCreate, }) } entries = append(entries, entry{ @@ -293,9 +330,12 @@ func (us *UndoStack) load() { } log.Printf("undo: loaded history from %s", us.filePath) type extraEntry struct { - FilePath string `json:"file_path"` - OldContent string `json:"old_content"` - NewContent string `json:"new_content"` + FilePath string `json:"file_path"` + NewFilePath string `json:"new_file_path,omitempty"` + OldContent string `json:"old_content"` + NewContent string `json:"new_content"` + IsDelete bool `json:"is_delete,omitempty"` + IsCreate bool `json:"is_create,omitempty"` } type entry struct { Time string `json:"time"` @@ -322,9 +362,12 @@ func (us *UndoStack) load() { var extra []ExtraFile for _, ef := range e.ExtraFiles { extra = append(extra, ExtraFile{ - FilePath: ef.FilePath, - OldContent: []byte(ef.OldContent), - NewContent: []byte(ef.NewContent), + FilePath: ef.FilePath, + NewFilePath: ef.NewFilePath, + OldContent: []byte(ef.OldContent), + NewContent: []byte(ef.NewContent), + IsDelete: ef.IsDelete, + IsCreate: ef.IsCreate, }) } changes = append(changes, ChangeDesc{ -- cgit v1.2.3