aboutsummaryrefslogtreecommitdiff
path: root/AGENTS.md
diff options
context:
space:
mode:
Diffstat (limited to 'AGENTS.md')
-rw-r--r--AGENTS.md577
1 files changed, 72 insertions, 505 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 6a5053d..6fdb497 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,588 +1,155 @@
# AGENTS.md
-The House of Icarus — a sci-fi MUD inspired by classic retro MMOs, written in Go. Data-driven via YAML files. No database, no ORM.
-
-Set on a terraformed asteroid where technology has regressed. "Technology" replaces Prayer, "Science" replaces Magic, Scavenging replaces Runecrafting.
+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
-```bash
+```
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 <path>` (defaults to `config.yaml`). If no config found it creates one with defaults (telnet :4000, http :8888, tick 600ms).
-
-Dependencies: `gopkg.in/yaml.v3`, `github.com/gorilla/websocket`.
-
-## Package Map
-
-```
-cmd/mud/main.go Entry point — config loading, multi-listener, tick subscriber registration
-internal/
- behavior/ YAML behavior definitions (GatherConfig, TalkConfig, UseConfig, Condition)
- combat/ Tracker struct + classic formulas (HitChance, MaxHit, AttackRoll)
- config/ YAML config (telnet, telnet_tls, http, https listeners, color targets)
- color/ xterm-256 color system with auto ANSI downgrade, gradients, inline tags
- engine/ Tick scheduler — configurable ms interval, subscriber pattern, ToTicks()
- game/ Game orchestrator (embedded Deps), state machine, command dispatch
- hacking/ Minigame interface + 3 implementations (Wumpus, Mastermind, Liar's Dice)
- item/ ItemDef, ItemStore, EquipSlot, WeaponType, ItemStats (was part of object/)
- net/ Conn interface (tcpConn, wsConn), Hub, Session, session states, IAC echo
- object/ ObjectDef, ObjectStore, UseInteraction, SafespotConfig (room objects only)
- player/ Player struct, skills, inventory, bank, equipment, accounts, XP, validation
- ui/ Pure renderers (progress bars, Unicode tables) — independent of game state
- validate/ Startup integrity checks (duplicate IDs, references, orphan rooms)
- world/ World, RoomDef, MobDef, MobInstance, MobStore, ObjState, ground items
-```
-
-`internal/game/` is organized by file prefix:
+Binary accepts `--config <path>` (defaults to `config.yaml`). Dependencies: `gopkg.in/yaml.v3`, `github.com/gorilla/websocket`.
-| Prefix | Purpose |
-|---|---|
-| `game.go`, `cmd_registry.go`, `tick.go` | Core state machine, command dispatch, tick callbacks |
-| `core_*.go` | Infrastructure — types, utils, login, skill XP, flags, courses, stations |
-| `act_*.go` | Tick-based action lifecycles — gather, firemaking, talk, use, search, farming, agility, etc. |
-| `cmd_*.go` | Command handlers — one file per command or command group |
-| `sys_*.go` | Game subsystems — technology, science, combat/death/aggro, buffs, construction, hazards, labor |
-| `render_*.go` | Session-coupled rendering — colour resolution, prompt, map, combat, help, XP drops |
-| `production_*.go` | Unified production engine + menu/selection UI (split from `core_production.go`) |
-| `combat_*.go` | Player-side attack and mob-side/hit-resolution logic (split from `cmd_attack.go`) |
-| `look_*.go` | Room/entity/target display helpers (split from `cmd_look.go`) |
-| `flagstore.go`, `safespot_manager.go`, `command_queue.go` | Extracted state managers with own mutexes |
-| `validate_source.go` | Adapter that populates the `validate.Source` from Game stores |
+## Prefixes
-The Game struct embeds a `Deps` struct (World, ItemStore, ObjectStore, MobStore, AccountStore, CraftIndex, CourseStore, Ticks, ColorConfig, DataDir) to make injected dependencies explicit while keeping `g.World`, `g.ItemStore` etc. working unchanged.
+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 are YAML files under `data/`. Read from disk on every access — edit and it takes effect immediately, no restart. Crafting information lives in `craft:` blocks on item YAML files — no separate recipe directory. Items are organized into subdirectories by category (items: `equipment/`, `tools/`, etc.; rooms: `town/`, `wilderness/`, etc.; objects: room-specific one-offs in `unique/`, generic shared objects flat). **The filename is the ID for every data type** (objects, items, mobs, rooms, drops, courses, techs, modules, hazards, help): lookup is by filename stem via the recursive path index, and every loader derives the struct's `ID` from that filename. **Do not put a top-level `id:` field in any data file — it is ignored.** (A nested `- id:` under a room's `objects:`/`mobs:` list is different: a *reference* to an object/mob by its filename, and is still required.) All IDs are globally unique across subdirectories via path-indexed loading; subdirectories are organizational only and do not namespace IDs.
-
-**Live state.** Player data (characters, accounts) is written to YAML on every state change. Ground items, mob instances, and object states live in memory only and are lost on restart.
-
-**No database.** Everything is flat YAML files. Account passwords use sha256 + 16-byte random salt.
-
-**Transport-agnostic.** The `Conn` interface (`internal/net/conn.go`) abstracts the transport layer. `tcpConn` wraps a raw `net.Conn` for telnet. `wsConn` wraps `gorilla/websocket.Conn` for the web client. Game code works with `*Session` regardless of transport.
+**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.
-**Input sanitization.** `HandleSession` strips Unicode control characters from every input line via `player.StripControlCharacters`. Account/character names validated to `[A-Za-z0-9 ]{1,30}`. Input truncated to 1024 bytes. Password echo suppressed via IAC ECHO (telnet) and OSC sequences (web). WebSocket origin checked against host header + loopback.
+**Live state.** Player data persisted to YAML on every change. Ground items, mob instances, object states are in-memory only.
-**Tick-driven.** The world runs on a configurable tick (default 600ms, min 50ms). All game advancement happens per tick.
+**No database.** Everything flat YAML. Passwords use sha256 + 16-byte random salt.
-**Fractional ticks.** All YAML timer fields accept `float64` values. `engine.ToTicks(base)` probabilistically rounds to an integer — a tool_speed of 4.5 means each action cycle has a 50% chance of 4 ticks and 50% of 5 ticks.
+**Transport-agnostic.** `Conn` interface (`internal/net/conn.go`) — telnet and websocket. Game code works with `*Session`.
-**Concurrency.** Every state-bearing package uses its own `sync.Mutex`:
-- `world.World` — groundItems, objStates, seeded, objMoves
-- `world.MobStore` — defs, instances
-- `behavior.Store` — cache
-- `engine.Engine` — subscribers
-- `Game` — charsMu (loggedInChars map), enterMu (enterSeqs map)
-- `net.Hub` — sessions, rooms maps (protects `Add`/`Remove`/`EnterRoom`/`LeaveRoom`/`AllSessions`/`PlayersInRoom` from concurrent access across accept/session/tick goroutines)
-- `net.Session` — per-session writeMu serializes writes to the Conn (protects against interleaved bytes from cross-session broadcasts and tick-driven writes)
-- `FlagStore` — world flags (shared by all players)
-- `SafespotManager` — per-player safespot states (value-copy API avoids lock-copy-pointer anti-pattern)
-- `CommandQueue` — free/active/consume command queues (written from session goroutines, drained by tick goroutine)
-- `combat` — combatants, mobTargets
+**Tick-driven.** 600ms default (min 50ms). All game advancement per tick. YAML timer fields accept `float64`; `engine.ToTicks(base)` probabilistically rounds.
-The pattern is: lock, snapshot/unmarshal, unlock, then process. The engine creates a subscriber snapshot each tick.
+**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` / checked with `flag`): Shared by all players. A door opened by one player is open for everyone.
-- **Player flags** (`set_player_flags` / checked with `player_flag`): Per-character, saved to character YAML. Quest progress, dialog history.
+- **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** fire when a player enters. Each has a `message` + optional `condition`. A step may also have a `delay` (ticks) and/or `set_flags`/`set_player_flags`. If any surviving step is timed (has a delay or sets a flag) the sequence runs as a scheduled **enter sequence** (driven by `EnterSeqTick`); otherwise messages print synchronously. Step conditions are snapshotted once at entry. An enter sequence persists `Player.EnterSeqRoom` and **resumes on reconnect** if interrupted; plain on_enter never runs on login (except the first room for new characters).
-- **Conditional descriptions** (`description: [{text, condition}]`) on rooms and objects pick the first variant whose condition passes (per-looker), else the first entry with no condition. Accepts either a plain string or a list. For an **object**, if it has a `description` list and no variant matches, the object is **absent** for that player (`look` falls through). Objects also support `aliases: [...]` for extra names. `look` lists candidates when multiple distinct objects match.
-- **Exits** support `set_flags`/`set_player_flags`, applied only on a successful traverse.
+- **`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/<id>.yaml Room definitions (exits, objects, mobs, item_spawns, on_enter, description)
-data/rooms/player_housing/ Player house rooms
-data/items/<id>.yaml Item definitions (stats, equip_slot, tool_type, craft, burn_ticks, search_table, etc.)
-data/mobs/<id>.yaml Mob definitions (combat stats, drops, behavior, steal config; kind: task for worksites)
-data/objects/<id>.yaml Object definitions (name, aliases, behavior, hidden, inroom_description, description, removal_item)
-data/objects/unique/ Room-specific one-off objects, prefixed with the room number (e.g. unique/1001_sign.yaml)
-data/drops/<id>.yaml Shared drop tables (weighted item lists, sub-table references)
-data/hazards/<id>.yaml Shared room hazard definitions (attack_type, attack, max_hit, speed, messages, required_item)
-data/help/<id>.yaml Help topics
-data/modules/<id>.yaml Science module definitions (level, max_hit, base_xp, junk_cost, category)
-data/techs/<id>.yaml Technology definitions (name, level, drain_rate, category, group, effects)
-data/courses/<id>.yaml Agility course definitions (name, required_level, start_room, obstacles)
-data/players/accounts/ Account YAML (gitignored)
-data/players/characters/ Character YAML (gitignored)
+data/rooms/<id>.yaml data/items/<id>.yaml data/mobs/<id>.yaml
+data/objects/<id>.yaml data/objects/unique/ data/drops/<id>.yaml
+data/hazards/<id>.yaml data/help/<id>.yaml data/modules/<id>.yaml
+data/techs/<id>.yaml data/courses/<id>.yaml data/players/accounts|characters/
```
-Wander config (`wander_rooms`, `wander_interval`) is set per-instance in room YAML — mob defs are generic. Mobs wander via legal (unconditioned) room exits; objects teleport between rooms in their assigned list.
-
-See `building_guide/` for full YAML format reference with examples.
+Wander config per-instance in room YAML, not on MobDef. See `building_guide/` for full YAML format reference.
## Startup Validation
-At startup, the server runs comprehensive integrity checks on all data files. Errors and warnings are logged to stderr; the server continues starting regardless (a "may behave unexpectedly" banner is printed if errors are found).
-
-**Duplicate ID checks** across all data directories (items, rooms, mobs, objects, drops, courses, modules, help). Detects collision from two files with the same ID in different subdirectories.
-
-**Referential integrity checks:**
-- Room exits → target rooms exist
-- Room mobs/objects/spawns → mob defs/object defs/items exist
-- Mob drops, steal tables, finishing blow items → referenced items/drop tables exist
-- Object removal items, steal tables, guard mobs, use_interaction items → exist
-- Object gather config drops → items/tables exist; bait items exist
-- Object talk config node actions (give_item, take_item, teleport, shop items) → exist
-- Item search tables, farm_product → exist
-- Item craft blocks → consume items, fail products, station objects, tools → exist
-- Drop table sub-references → exist
-- Course start_room and obstacle rooms → exist
-
-**World wiring checks** (config-driven):
-- Orphan rooms (no exit from any room leads to them)
-
-### Config: `startup_validation`
-
-```yaml
-game:
- tick_length: 600
- startup_validation:
- check_sources: [1] # rooms to start reachability walk from
- ignore_unreachable: [] # suppress "no exit leads here" for these rooms
-```
-
-Defaults: `check_sources: [1]`, ignore list empty. Set `check_sources` to include rooms accessible only via teleport (e.g., `[1, 1000]`) to suppress false orphan warnings. Use `ignore_unreachable_to` for rooms that are intentionally isolated.
-
-## Inline Color Tags
-
-Room descriptions and prompts support inline color tags using `{spec}text{/}` syntax.
-
-```yaml
-description: "On the table lies a {182 bold}mysterious vase{/} with a rose in it."
-```
-
-Tag spec format: `{<0-255> [bold] [dim] [underline]}text{/}`.
-
-Gradients: `{g:196,82}gradient text{/}`. Multi-stop: `{g:45,39,59}three stops{/}`. Item and object YAML `color` fields also support gradient syntax: `color: "g:196,208,226"`.
-
-Gradients interpolate in RGB space across the xterm-256 palette. In ANSI mode, each character maps to the nearest ANSI color.
+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.check_sources`).
## Command Dispatch
-`handleGameCommand()` in `game.go`:
-1. Expands account aliases (aliases can shadow built-in commands)
-2. Splits input, lowercases the command word
-3. Special-cases `equip`/`wear`/`wield` with no args → display equipment directly
-4. Calls `classifyCommand(cmd)` from `cmd_registry.go` → `ClassInstant` / `ClassFree` / `ClassActive` / `ClassUnknown`
-5. ClassInstant executes `executeCommand()` immediately + writes prompt
-6. ClassFree appends to `freeQueue[playerName]` for next tick
-7. ClassActive replaces entry in `activeQueue[playerName]`, sorted by timestamp on execution
-8. ClassUnknown falls through to `verbAliases` and `obstacleVerbs` maps, then prints error
+`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()` runs each tick:
-1. Clears stale `ActionState` from one-tick actions (move, get, drop)
-2. Executes all free commands per player in queued order
-3. Executes all active commands sorted globally by timestamp (first-to-queue wins conflicts)
-4. Advances walk sequences (one step per tick)
-5. Flushes pending shared depletions
-
-**Conflict resolution:** multi-player attack on same mob, get on same item, etc. — first to queue wins (timestamp-order). Shared depletion awards all successful gatherers on depletion tick.
-
-**ActionState** tracks player activity for `look` display. Timed actions persist until interrupted; one-tick actions (move, get, drop) clear after one tick.
-
-Queue feedback is suppressed by default via the `queue_silently` option.
+`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 suppressed by `queue_silently`.
## Action System
-Actions that interact with objects/mobs route through `startAction()` in `act.go`:
-1. Normalizes verb via `verbAliases` map (mine/chop/fish/cut → gather, talk/speak/ask → talk)
-2. Multiple object types → "That's ambiguous, which one?"
-3. Searches object instances in room (numbered targeting via `N.name`)
-4. Falls back to mob instances if no object matches
-5. Loads the behavior from YAML
-6. Routes to type-specific handler (startGather, startMobTalk, startTalk, startUse)
-
-The `verbSkill` map maps verb → skill name for default target resolution:
-- mine → mining, chop/cut → woodcutting, fish → fishing
-
-### Action Types (from act_state.go)
-
-| ActionType | Trigger | Type |
-|---|---|---|
-| `gathering` | mine/chop/cut/fish | Active |
-| `combating` | attack/kill | Active |
-| `using` | use / pull / push | Active |
-| `talking` | talk/speak/ask | Active |
-| `burning` | burn | Active |
-| `stoking` | stoke | Active |
-| `searching` | search | Active |
-| `resting` | quit | Active |
-| `walking` | walk | Active |
-| `moving` | n/s/e/w/u/d | Active |
-| `picking_up` | get | Active |
-| `dropping` | drop | Active |
-| `producing` | cook/smelt/smith/craft/mix/construct | Active |
-| `fletching` | fletch | Free (background) |
-| `eating` | eat | Free |
-| `cleaning` | clean | Free (background) |
-| `identifying` | identify/id | Active |
-| `stealing` | steal/thieve | Active |
-| `planting` | plant | Active |
-| `harvesting_crop` | harvest | Active |
-| `raking` | rake | Active |
-| `watering` | water | Active |
-| `curing` | cure | Active |
-| `traversing` | obstacle verbs (scramble/jump/etc.) | Active |
-| `hacking` | jack/jackin | Active |
-| `working` | work/attack on a task mob (worksite) | Active |
-
-### Behavior Types (YAML-driven in behavior.go)
-
-| Type | Verbs | Description |
-|---|---|---|
-| `gather` | mine, chop, cut, fish | Resource gathering with skill checks, tool requirements, depletion/respawn |
-| `use` | use | Crafting stations — consume items, produce output, optional skill checks |
-| `talk` | talk, speak, ask | NPC conversation trees with choices, conditions, node actions |
-
-### Hardcoded Actions (not YAML-driven)
-
-| Action | File | Description |
-|---|---|---|
-| burn/stoke | `act_burn.go` | 3-phase firemaking: drop log → try → skill check → create fire. Stoke adds logs. |
-| search | `act_search.go` | Search items with search_table, consumes one, rolls on drop table |
-| cook/smelt/smith/craft/mix | `production_core.go` | Unified recipe-driven production — station checks, timed loops, success/fail, XP, byproducts |
-| fletch | `act_fletch.go` | Background fletching — instant or timed, stackable output |
-| clean | `act_clean.go` | Background herb cleaning via recipe matching |
-| steal | `act_steal.go` | Thieving — pickpocket, stall stealing, guard watching, sneak mode |
-| agility | `act_agility.go` | 3-phase obstacle traversal with fail chance, course lap tracking |
-| farm | `act_farm.go` | Planting, growth stages (500-tick), watering, disease, harvesting |
-| identify | `act_identify.go` | Scavenging — scrap → junk at altars |
-| finishing blow | `act_finishing_blow.go` | Assassin special item on mob at 1 HP |
-
-### GatherConfig Fields
-
-| Field | Description |
-|---|---|
-| `skill` | Skill name for level check |
-| `level` | Required level |
-| `xp` | XP per successful gather |
-| `base_wait` | Base ticks per action cycle |
-| `tools` | Required tool_type list |
-| `bait` | Required bait item |
-| `success` | SuccessFormula (base + per_level * (level - required), capped) |
-| `drops` | Weighted drop entries (item_id, table ref, depletes, quantity, message, level, xp) |
-| `respawn_timer` | Ticks until depleted object respawns |
-| `deplete_timer` | Ticks for shared depletion (trees) |
-| `nest_chance` | 1/N chance for bird's nest alongside normal drop |
-| `gather_message` | Custom "You gather..." message |
-| `depleted_message` | Custom "The rock is depleted" message |
-| `exhausted_message` | Custom "The tree falls" message |
-| `fail_message` | Custom failure message |
-| `respawn_broadcast` | Broadcast message when object respawns |
-
-### SuccessChance Formula
-
-```go
-chance = cfg.Base + (level - requiredLevel) * cfg.PerLevel
-clamped to [0, cfg.Cap]
-```
+Actions route through `startAction()` in `act.go`: normalize verb → resolve target (objects then mobs) → load behavior → route to handler.
-### Node Actions (talk dialog)
-
-| Field | Effect |
-|---|---|
-| `set_flags` | Sets world flags (global) |
-| `set_player_flags` | Sets player-local flags (per-character, saved to YAML) |
-| `give_item` | Gives an item to inventory |
-| `take_item` | Removes an item from inventory |
-| `teleport` | Moves player to a room ID |
-| `heal` | Restores hitpoints |
-| `cost` | Deducts credits from player |
-| `shop` | Opens a buy/sell interface |
-| `assign_task` / `skip_task` / `extend_task` | Assassin task management |
-| `sawmill` | Opens sawmill interface (construction) |
+**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 scripts, and use_interactions checks. A bare `flag`/`player_flag` passes when the flag is set to a truthy value; `value` matches a specific value; `not` inverts.
-
-| Field | Scope | Example |
-|---|---|---|
-| `flag` | World (shared by all) | `flag: gate_open` |
-| `player_flag` | Per-character | `player_flag: finished_tutorial` |
-| `value` | Exact value to match (else truthy) | `value: 3` |
-| `has_item` | Player inventory | `has_item: bronze_key` |
-| `min_credits` | Player credits | `min_credits: 100` |
-| `all_of` | All sub-conditions pass | Nested list |
-| `any_of` | Any sub-condition passes | Nested list |
-| `not` | Invert the check | `not: true` |
+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 depletion** (mining, regular trees): Set `depletes: true` on a drop entry. The resource depletes on first successful gather of that drop.
-
-**Shared depletion** (higher-tier trees): Set `deplete_timer: <ticks>` on the behavior. A shared timer counts down while anyone is chopping; when it hits 0, the next successful gather depletes the tree for all players. Timer regenerates when no one is chopping. Managed by `SharedDepletionTick`.
+- **Per-drop** (`depletes: true`): depletes on first successful gather of that drop.
+- **Shared** (`deplete_timer: <ticks>`): timer counts down while anyone gathers; at 0, next gather depletes for all. Regenerates when idle.
## Ground Item System
-- `DropDespawnTicks = 1000` — dropped items despawn after 1000 ticks
-- `ReserveTicks = 100` — mob loot is reserved for the killer for 100 ticks
-- Items with same ID in the same room are NOT merged — independent despawn timers
-- Spawn items respawn after their delay; nearby matching items are merged on respawn
-- Despawn timers visible via `despawn` option; reserve info via `reserve` option
+`DropDespawnTicks = 1000`. `ReserveTicks = 100` (mob loot reserved for killer). Same-ID items NOT merged. Spawn items respawn with merge. Despawn/reserve visible via options.
## Combat System
-**Attack types.** Weapons have an `attack_type` field (stab/slash/crush/ranged/science). The attack type determines which player attack bonus and which mob defense bonus to use.
-
-**Accuracy formula.** `HitChance(a, d)`: if `a > d` → `1 - (d+2)/(2*(a+1))`, else `a/(2*(d+1))`. Equal rolls ≈ 50%.
+`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`.
-**Ranged combat.** Weapon type "ranged" uses Ranged level for attack rolls, consumes 1 ammo per attack. If ammo runs out, combat ends. XP: 75% Ranged, 25% Hitpoints.
+## Labor System (`sys_labor.go`, `sys_hazard.go`)
-**Agile mobs.** Mobs with `aggressive: true` auto-attack players entering their room (1-tick delay) if `playerLevel <= mobLevel * 2`.
+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.
-**Equipment requirements.** Items can have `requirements: { accuracy: 20, defense: 10 }` — checked before equipping.
+**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.
-**Fleeing combat:** Movement during combat is a flee attempt. Mobs continue attacking during the movement countdown; a hit aborts the flee ("Can't escape!"). Cape of Agility makes fleeing instant.
+## Technology System (`tech.go`)
-**Death mechanic:** Player drops credits to ground. If >3 items total (inventory + equipment), the 3 most valuable are kept, rest dropped. Player teleports to room 1 at full HP. All active techs deactivated.
+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.
-**XP formula:** `baseXP = dmg * 4`, distributed by attack style:
-- Accurate: +3 accuracy, 75% Accuracy XP / 25% HP
-- Aggressive: +3 strength, 75% Strength XP / 25% HP
-- Defensive: +3 defense, 75% Defense XP / 25% HP
-- Balanced: +1 each, 25% each Accuracy/Strength/Defense/HP
-- Ranged styles: varied bonus, 75% Ranged XP / 25% HP
+## Science System (`science.go`)
-**Mob combat level:** `floor((def+hp)/4 + max((atk+str)/4, rng*3/8, sci*3/8))`.
+Mods from `data/modules/`: combat/utility/transport/enchant/processing. `trigger <mod>` fires once. `autotrigger <mod>` 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`.
-**Player combat level:** Ranged-or-melee dominant, includes Technology (0.25) and Science (0.125).
+## Hacking System
-**Mob DropTable:** `Remains` (guaranteed drop item), `Loot` (weighted DropEntry table).
+Minigame interface: 3 games at terminals (levels 1/20/40). Interface in `game/hacking/`.
-## Labor System (sys_labor.go, sys_hazard.go)
-
-A **non-violent reuse** of the entire combat engine for worksites — building solar panels, surveying flora, prospecting rocks — so combat skills/weapons train without the theme of killing.
-
-**Task mobs.** A `MobDef` with `kind: task` (default `combat`). Internally its `HP` drains to 0 to **complete** the work; the player sees a progress bar filling to 100% (`progressBar`, the inverse of `hpBar`). It reuses the whole pipeline: `startCombat`/`playerAttack`/`applyPlayerHit`/`endCombat`, all formulas, drops (= "payment"), respawn, the 1-worker `IsMobInCombat` lock. Differences: it **never attacks back** (`startCombat` skips the mob-attack subscription for tasks), display/messages are labor-flavored, and `ActionWorking` is used for `look`. Progress **decays** when unattended — this is the standard mob HP regen, unchanged, shown inverted. Commands `work` and `attack`/`kill` all route to `doAttack`, which auto-detects the kind. Flavor fields: `verb`, `progress_noun`, `complete_message`. Per-type defenses (`crush_defense`…`science_defense`) become **method efficiency** — every style works, designers steer which is efficient. XP/buffs are the normal style-based combat split (no change).
-
-**Hazards.** A room references a shared hazard by ID: `hazard: <id>` → `data/hazards/<id>.yaml` (`world.HazardDef`, cached via `World.LoadHazard`). `HazardTick()` (registered in `main.go`) rolls an attack against **everyone in a hazardous room every `speed` ticks** (cadence in non-persisted `Player.HazardTimer`, reset on room change) using the same `EffectiveRoll`/`HitCheck`/`MaxHit`/`RollDamage`. Player defends with Defense level + equip bonus selected by `attack_type`; `damageAfterTechProtection` applies the matching protect tech. `required_item` equipped negates it. A safespot blocks **all** hazard damage (`safespotBlocksHazard` = `isSafespotted`, type-agnostic). A lethal hazard hit calls `killPlayer` (extracted from `endCombat`). Hazards **do not interrupt movement** (no flee-abort), and stack with mobs. Entering a safe→dangerous room prompts `[Y/n]` (`StateDangerConfirm`) unless the `danger_warning` option is off.
-
-## Technology System (tech.go)
-
-Tech definitions loaded from `data/techs/` via `LoadTechs()`. Techs in 4 categories, unlocked by Technology level:
-- **Attack/Strength/Defense/Ranged/Science tiers:** I/II/III at 5%/10%/15% level boost
-- **Protection techs:** Kinetic Barrier (melee), Projectile Screen (ranged), Neural Firewall (science) — 40% damage reduction each
-- **Utility:** Nano Repair (2x HP regen), Rapid Repair (4x), Power Saver (20% drain reduction), Dead Man's Switch (25% max HP retribution on death)
-- **Combo:** Overclock (15% atk+str), Fortify (15% def + 2x regen)
-
-Reserved tech IDs whose semantics are coupled to code logic:
-- `protect_melee` / `protect_ranged` / `protect_science` — mapped by attack type in `damageAfterTechProtection`. Must exist in YAML; startup validation enforces this.
-- `retribution` — checked by name in `killPlayer` (cmd_attack.go) for the death retribution mechanic. Must exist in YAML.
-
-The Science tier techs (Neural Focus I/II/III) boost Science accuracy via `techLevelBonus(p, "science")`, which is included in the attack roll of both the melee/ranged `calculatePlayerAttackRoll` and the deck `scienceAttack` path.
-
-Battery = Technology level (float64). Each tech has a drain rate (0.05–0.25 per tick). Equipment TechnologyBonus reduces drain (capped at 50%). Active techs deactivate when battery hits 0. 1-tick "flicking" grace period supported. ActiveTechs is not persisted (resets on reconnect).
-
-## Science System (science.go)
-
-Mods loaded from `data/modules/` — categories: combat, utility, transport, enchant, processing. Each mod has a junk cost. Deck equipment (science weapons) provides junk and removes scrap cost.
-
-**Triggering mods:**
-
-- `trigger <mod>` — triggers a mod. Combat mods in non-deck combat queue as a one-shot attack. Transport/utility/enchant mods resolve immediately. Module matching searches by partial name (all words); if ambiguous, picks the highest-level module the player can cast (at or below their Science level).
-- `autotrigger <mod>` — sets a combat mod to auto-fire each attack round when a deck is equipped. `autotrigger none` disables. Without a deck, autotrigger has no effect.
-
-**Deck combat:** When a deck weapon is equipped, attacks fire at the weapon's speed. If an autotrigger mod is set and the player has the required junk, the mod fires instead of a normal attack. If no autotrigger or out of junk, an error is shown and the player does an unarmed attack with no bonuses.
-
-**Non-deck manual triggers:** `trigger <mod>` during combat without a deck queues one module to replace the next combat attack. Only one can be queued at a time.
+## Skills (23 total, 1-99, classic XP table)
-Mod list viewable with `mods`, `modules`, or `module` command.
+| 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 |
-## Hacking System (game/hacking/)
+## Session States
-Minigame interface: `Init(level) string`, `HandleInput(input) (output, done, won)`, `Name() string`.
+20 states in `net/server.go` covering login flow, game, NPC talk, menus (bank/shop/recipe/confirm), and hacking.
-Three minigames at 3 terminal levels (1, 20, 40):
-- **WumpusGame** — Hunt the Rogue AI: 20-node dodecahedron, probes, hazards
-- **MastermindGame** — Code Breaker: 4-digit cipher, 10 guesses, feedback
-- **LiarsDiceGame** — Signal Bluff: hidden dice bidding vs AI, wilds, exact calls
+## ItemDef Fields
-XP scales with Hacking level.
+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.
-## Skills (23 total, 1-99, classic XP table)
+## CraftIndex
-| 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 |
-
-All fully implemented.
-
-## Option System
-
-Account-wide options set via `option <name> <value>`. Stored in account YAML, shared by all characters on the account.
-
-| Option | Type | Default | Description |
-|---|---|---|---|
-| `description` | bool | true | Long room descriptions when moving |
-| `tiny_map` | string | "right" | off/right/left — mini-map display |
-| `xp_drops` | bool | true | XP drop messages |
-| `exits` | bool | true | Long exit display in look |
-| `mob_enter` | bool | true | Messages when mobs enter |
-| `mob_leave` | bool | true | Messages when mobs leave |
-| `mob_spawn` | bool | true | Messages when mobs spawn |
-| `reserve` | bool | true | Show full reserved item details |
-| `depletion` | bool | false | Show depletion timers |
-| `despawn` | bool | false | Show ground item despawn timers |
-| `color` | string | "xterm256" | none/ansi/xterm256 — color output mode |
-| `map_width` | int | 30 | Map viewport width |
-| `map_height` | int | 20 | Map viewport height |
-| `map_padding` | string | "none" | none/x/y/xy — blank-row stripping |
-| `automap` | bool | false | Auto-show map after moving |
-| `queue_silently` | bool | true | Suppress queue messages |
-| `danger_warning` | bool | true | Confirm `[Y/n]` before entering a hazardous area from a safe one |
-| `room_desc_width` | int | 70 | Room desc line wrap width |
-| `wrap_width` | int | 120 | Wrap ALL output to this many columns (clamped to min 80; applied in `net.Session.WriteLine/WriteLines/Writef`, not `Write`) |
-| `unicode` | bool | true | Unicode box-drawing characters |
-| `run_countdown` | bool | false | Flee countdown messages |
-| `visual_ticks` | bool | false | Tick marker display |
-| `visual_tick_count` | int | 0 | Tick counter cycle length |
-| `visual_tick_text` | string | "Tick" | Text for visual tick marker |
-| `fletch_all` | bool | false | Auto-start fletching |
-| `smith_all` | bool | false | Auto-start smithing |
-| `cook_all` | bool | false | Auto-start cooking |
-| `smelt_all` | bool | false | Auto-start smelting |
-| `mix_all` | bool | false | Auto-start mixing |
-| `construct_all` | bool | false | Auto-start constructing |
-| `craft_all` | bool | false | Auto-start crafting |
-
-Prompt is NOT an option — per-character via the `prompt` command. Default: `"> "`. `prompt none` sets it to blank.
-
-Prompt variables: `%h` (HP), `%H` (max HP), `%b` (battery), `%B` (max battery), `%c` (credits), `%i` (free slots), `%s` (style short), `%S` (style long), `%m` (mob HP), `%M` (mob max HP), `%X_<skill>` (XP), `%x_<skill>` (XP to next).
-
-## Session States (net/server.go)
-
-| State | Purpose |
-|---|---|
-| `StateAccountName` | Entering account name |
-| `StatePassword` | Entering password |
-| `StateNewAccountPass` | Setting password for new account |
-| `StateNewAccountConfirm` | Confirming password |
-| `StateColorChoice` | New account: "Should I disable color?" |
-| `StateMenu` | Main menu |
-| `StateNewCharName` | Naming new character or selecting for connection |
-| `StateRenameAccount` | Renaming account |
-| `StateRenameChar` | Selecting character to rename |
-| `StateRenameCharName` | Entering new character name |
-| `StateDeleteChar` | Confirming character deletion |
-| `StatePurgeAccount` | Confirming account purge |
-| `StateGame` | In-game command mode |
-| `StateChangeDescription` | Setting player description |
-| `StateTalk` | NPC conversation input |
-| `StateDropAllConfirm` | Confirming `drop all` |
-| `StateRecipeChoice` | Recipe/product selection (by number or name) |
-| `StateHowMany` | "How many?" production count |
-| `StateSmithProduct` / `StateFletchProduct` / `StateCraftProduct` / `StateProductChoice` | Product selection table menus |
-| `StateShop` | Shop interface (buy, sell, browse, leave) |
-| `StateBank` | Bank interface (deposit, withdraw, browse, leave) |
-| `StateHacking` | Jacked into a terminal running a minigame |
-| `StateDangerConfirm` | Confirming `[Y/n]` entry into a hazardous room |
-
-## ItemDef Fields (item/item.go)
-
-| Field | Description |
-|---|---|
-| `id` | Unique identifier |
-| `name` | Display name |
-| `aliases` | Alternate name prefixes for matching |
-| `description` | Item description |
-| `value` | Credit value |
-| `stackable` | Can stack in inventory |
-| `equip_slot` | head/neck/torso/legs/hands/feet/back/ammo/main_hand/off_hand/ring |
-| `weapon_type` | melee / ranged / science |
-| `attack_type` | stab / slash / crush / ranged / science |
-| `stats` | Per-type attack/defense bonuses, strength, ranged_strength, science_damage, technology_bonus |
-| `speed` | Attack speed in ticks |
-| `requirements` | Skill level requirements to equip |
-| `tool_type` | Tool type string for gather requirements |
-| `tool_speed` | Speed bonus for gathering |
-| `burn_ticks` | Burn duration for firemaking |
-| `fire_level` | Required firemaking level |
-| `fire_xp` | XP for burning |
-| `quality` / `max_quality` | Default and maximum quality |
-| `search_table` / `search_misc_table` | Drop tables for searching this item |
-| `search_ticks` | Ticks per search action |
-| `search_message` | Custom search message |
-| `craft` | Craft block — type, skill, level, XP, station, tool, consume, output_qty, fail, message, success, steps |
-| `farming` | Farming-related fields (seed type, patch, yield, level, xp) |
-| `potion` | Potion properties (buff levels for attack/strength/defense/agility) |
+`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. A mob must have `behavior:` set on its YAML def to be interactable.
-- Exits changed from `map[ExitDir]int` to `map[ExitDir]ExitDef` — old `north: 2` still works via custom `UnmarshalYAML`. Same pattern for `RoomMob`.
-- `checkCondition` is the single condition evaluator — used by exits, talk options, on-enter scripts, and use_interactions.
-- Object `hidden: true` means the object doesn't appear in room listings but is still interactable.
-- Alias expansion happens before command dispatch and can shadow built-in commands.
+- `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`, and `quit` cancel the player's active action.
-- `findGroundMatches` and `findInventoryMatches` support bidirectional prefix matching — `"iron ax"` matches `"iron axe"`.
-- Mob wander config is per-instance in room YAML via `RoomMob`, not on `MobDef`.
+- `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 the highest-value item per slot; `remove all` unequips everything.
-- Cape of Agility makes movement instant (1 tick). Graceful pieces each reduce by 0.25 ticks; full set +0.5 bonus for 2 ticks total. Cape overrides Graceful.
-- During combat, movement is a flee attempt. Mob hits during countdown abort the flee.
-- Level-up messages output `*** You are now level N <skill>! ***` across all skills that gain XP.
-- Task mobs (`kind: task`) reuse the combat engine 1:1 — internally HP drains to 0 to complete; the bar is just displayed inverted. They never attack back; danger comes only from a room `hazard:`.
-- Hazard damage uses its own `applyHazardHit` path that deliberately omits the flee-abort (`MoveTicks`) logic, so hazards never interrupt walking. Mob hits still do.
-- A safespot blocks ALL hazard damage regardless of type (`safespotBlocksHazard`), but a melee work/attack step still forces you out of cover (so only ranged/science work keeps you sheltered).
-- `killPlayer` is the shared death path (extracted from `endCombat`); both combat death and lethal hazard hits go through it. `mob` may be nil for a hazard kill.
-- Science (deck) attacks go through `scienceAttack` which calls `combat.EffectiveRoll` for both attack and defense rolls — consistent with melee/ranged. The `techLevelBonus(p, "science")` is included in the attack roll (Neural Focus techs boost science accuracy). The `hpBar`/`progressBar` share a common `renderBar` helper; XP-drop suffixes use `formatXpDrop`/`formatXpDropSingle`.
-
-## Adding a New Skill
-
-1. Add `SkillName` constant + entry in `AllSkills` + `SkillAbbr` in `internal/player/player.go`
-2. Create a gather behavior YAML in `data/behaviors/`
-3. Create an object YAML referencing that behavior in `data/objects/`
-4. Create an item for the tool in `data/items/` (with `tool_type`)
-5. Place the object in a room YAML in `data/rooms/`
-6. Create any resource items in `data/items/`
-
-## Adding a New Production Skill
-
-Production skills share a unified system in `core_production.go`:
-
-1. Create the output item YAML in `data/items/` with a `craft:` block. See `building_guide/recipes.md`.
-2. Create a station object in `data/objects/` if needed
-3. Add a `cmd_<skill>.go` command handler following existing patterns
-4. Add the command to `commandRegistry` in `cmd_registry.go` with the appropriate class
-5. Wire up the `auto_start` option if desired
-
-The unified production cycle handles: HowMany prompt, start message, timed loops, success/fail rolls, XP, output quantity, stackable stacking, byproducts, multi-step messages, and end message. Tool requirements are checked in the command handler before calling `promptHowMany`.
-
-## CraftIndex
-
-`CraftIndex` (`internal/game/craft_index.go`) is built once at startup from all item YAMLs that have a `craft:` block. It provides `O(1)` lookups:
-
-- **`ByType("pharmacy")`** — all pharmacy-craftable items (used by `mix` command)
-- **`ByStation("anvil")`** — all items craftable at that station
-- **`ByInput("guam")`** — all items that consume that ingredient
-- **`FindByTwoInputs(a, b)`** — items consuming both inputs in different consume entries (used by `use` command)
-
-**Key properties:**
-- Built once at startup — `O(n)` one-pass scan over all items
-- All indexes are `O(1)` lookups with zero allocations
-- The CraftIndex stores `*ItemDef` pointers — no RecipeDef copies
-- No cache invalidation — craft data is static after startup
+- `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 a `cmd_*.go` file in `internal/game/` with the handler function `func (g *Game) executeXxx(sess *net.Session, args []string, rawInput string)`
-2. Add the command to `commandRegistry` in `cmd_registry.go` with the appropriate `CommandClass`
-3. Update `data/help/` YAML files
+1. Add `cmd_<name>.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/`