# 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/mud 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/use_interactions). 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 - **World flags** (`set_flags` / `flag`): Shared by all players. - **Player flags** (`set_player_flags` / `player_flag`): Per-character, saved to YAML. ## Room Scripting - **`on_enter` steps**: message + optional condition. A step may have `delay` and/or `set_flags`/`set_player_flags`. Timed steps 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. Accepts plain string or list. - **Exits** can `set_flags`/`set_player_flags` on successful traverse. ## 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`). ### Condition System Used by exits, talk options, on-enter, use_interactions: `flag` (world), `player_flag` (per-char), `value` (exact match), `has_item`, `min_credits`, `all_of`, `any_of`, `not`. Bare 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. 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: old `north: 2` still works via custom `UnmarshalYAML`. Same for `RoomMob`. - `checkCondition` is the single condition evaluator — shared across exits, talk, on-enter, use_interactions. - 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.