# AGENTS.md Third Collapse — a Runescape-like sci-fi MUD written in Go. Data-driven via YAML files. No database, no ORM. Set on a terraformed asteroid where technology has regressed. "Science" replaces Prayer, "Technology" replaces Magic, Scavenging replaces Runecrafting. ## Build & Run ```bash make build # go build -o tc ./cmd/mud make run # build + run with DATA_DIR=./data make test # go test ./... -timeout 60s make vet # go vet ./... ``` Binary accepts `--config ` (defaults to `config.yaml`). If no config found, starts telnet on :4000. Dependencies: `gopkg.in/yaml.v3`, `github.com/gorilla/websocket`. Config: ```yaml game: tick_length: 600 # ms per game tick (default 600, min 50) ``` ## Package Map ``` cmd/mud/main.go Entry point — config loading, multi-listener, tick engine internal/ config/ YAML config (telnet, http, https listeners) net/ Conn interface (tcp + websocket), session states, hub, IAC echo game/ Login flow, command dispatch, input sanitization, all game logic action/ Behavior structs (GatherConfig, TalkConfig, etc.), Action, drop tables, SuccessChance player/ Player struct, skills, inventory, XP (RSC table), name/password validation, accounts world/ Room, MobDef, MobInstance, ObjState, ground items, wandering, drop tables combat/ Combat state tracking, combat lock, OSRS-style combat formulas object/ ItemDef, ObjectDef, item/object YAML loaders, EquipSlot, WeaponType, ItemStats engine/ Tick scheduler — configurable ms interval, subscriber pattern, ToTicks() color/ ANSI 16-color and xterm256-color escape code tags (text, , etc.) ``` `internal/game/` is the largest package. Key files: | File | Purpose | |---|---| | `game.go` | Game struct, HandleSession state machine, command dispatch, ProcessQueuedCommands | | `login_account.go` | Account auth, creation, password handling, main menu, account rename/purge | | `login_char.go` | Character creation, connection, rename, delete | | `cmd_*.go` | Command handlers for each command (see Commands section) | | `action.go` | StartAction, CancelAction, AdvanceActions, checkCondition, verbAliases, verbSkill | | `action_*.go` | Action lifecycle per type (gather, talk, use, toggle) | | `action_burn.go` | Burn/stoke firemaking (standalone, not behavior-YAML-driven) | | `action_search.go` | Search bird's nests for loot | | `action_room.go` | RunEnterSteps (on_enter scripts), BroadcastRespawns | | `action_state.go` | ActionType consts, ActionState struct, Description() for look display | | `action_default_target.go` | Smart auto-selection for gather/attack | | `tick.go` | DisconnectTick, RegenTick, WanderTick, SharedDepletionTick | | `map.go` | BFS graph builder, tiny map, full map, display utilities (strip, trim) | | `types.go` | EquipSlots, itemMatch, xpGain, deathDrop | | `utils.go` | Helper functions (plural, parseQty, parseChoiceIndex, formatPickupList, etc.) | | `help.go` | Help topic YAML loading and display | | `color.go` | colorMode(), colorize(), colorTag() helpers | | `table.go` | Unicode/ASCII table rendering for score/option/help | ## Key Conventions **YAML-driven.** Rooms, items, objects, mobs, behaviors, drops are YAML files under `data/`. Read from disk on every access — edit a room description and it takes effect immediately, no restart. **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. **Input sanitization.** `HandleSession` strips Unicode control characters from every input line via `player.StripControlCharacters` before dispatch. 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. **Tick-driven.** The world runs on a configurable tick (default 600ms). Combat, gathering, regen, wandering, respawns, shared depletion, fire ticks all advance per tick. **Fractional ticks.** All YAML timer fields (tool_speed, speed, base_wait, deplete_timer, etc.) 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. **Concurrency.** Every state-bearing package uses its own `sync.Mutex`: - `world.World` — groundItems, objStates, seeded, objMoves - `world.MobStore` — defs, instances - `action.Store` — cache - `engine.Engine` — subscribers - `Game` — charsMu (loggedInChars map) - `combat` — combatants, mobTargets The pattern is: lock, snapshot/unmarshal, unlock, then process. The engine creates a 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. ## Data Files ``` data/rooms/.yaml Room definitions (exits, objects, mobs, spawns, on_enter) data/items/.yaml Item definitions (stats, equip_slot, tool_type, burn_ticks, search_table, etc.) data/mobs/.yaml Mob definitions (combat stats, drops, behavior — NO wander config) data/objects/.yaml Object definitions (name, behavior, hidden, inroom_description, removal_item, props) data/behaviors/.yaml Behaviors (gather, talk, use, toggle) data/drops/.yaml Shared drop tables (weighted item lists) data/help/.yaml Help topics data/players/accounts/ Account YAML (gitignored) data/players/characters/ Character YAML (gitignored) ``` 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 list. See `WORLDBUILDING.md` for full YAML format reference with examples. ## Commands ### Instant (execute immediately, no queue) | Command | Description | |---------|-------------| | `say ` | Room chat (preserves case from raw input) | | `score` / `sc` | Character stats and skills | | `inventory` / `i` / `inv` | Inventory contents (28 slots) | | `equipment` / `eq` | Equipped items by slot | | `look` / `l` | Room view (mobs, objects, ground items, exits, players) | | `look ` / `l ` | Examine mobs, objects, items, players, directions | | `exits` | List available exits | | `help [topic]` | Help topics from data/help/ | | `map` | BFS ASCII map centered on player | | `option` / `options [name] [value]` | Character settings (see Option System) | | `alias ` | Account-wide command alias with arg passthrough | | `unalias ` | Remove an alias | | `description` / `desc` | Set custom player description | | `queued` | Show pending tick actions | ### Free (stackable, execute before active) | Command | Description | |---------|-------------| | `wear` / `wield ` | Equip item to correct slot. `wear all` auto-equips best per slot | | `wear all` | Auto-equips highest-value item per slot from inventory | | `remove` / `unwear` / `unwield ` | Unequip item back to inventory | | `style ` | Set combat style: accurate, aggressive, defensive, balanced (prefix match) | ### Active (replaces previous active, queued per tick) | Command | Description | |---------|-------------| | `n` / `s` / `e` / `w` / `u` / `d` | Movement (conditional exits, interrupts combat/actions) | | `get` / `take` / `grab` / `pick ` | Pick up item from ground (reservation-aware) | | `get all` / `get all ` | Pick up all (or matching) items | | `drop ` | Drop item to ground | | `drop all` / `drop all ` | Drop all (or matching) items | | `attack` / `kill ` | Combat with numbered targeting and smart default | | `mine` / `chop` / `cut` / `fish ` | Gathering with smart default (verbAliases map to "gather") | | `use ` | Crafting station interaction | | `talk` / `speak` / `ask ` | NPC conversations (objects or mobs with behavior) | | `pull` / `push ` | Toggle interactions (levers, gates) | | `search ` | Search bird's nests / searchable items (rolls on search_table) | | `burn [item]` | Start a fire (smart default: auto-select single log type) | | `stoke [item]` | Add logs to existing fire for XP | | `walk ` | BFS pathfinding to a room by number | | `walk ` | Direction sequence like `3n2e1u` (capped by agility level) | | `quit` | 10-tick rest sequence, then return to menu (blocked during combat) | ## Command Dispatch `handleGameCommand()` in `internal/game/game.go`: 1. Cancel rest timer 2. Resolve account aliases (alias expansion BEFORE command lookup — can shadow built-ins) 3. Classify command (Instant / Free / Active / Unknown) 4. Instant runs immediately; Free/Active queue for next tick ## 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 or get on same item — first to queue wins (timestamp-order). Shared depletion defers to award all successful gatherers on depletion tick. **ActionState** tracks player activity for `look` display. Timed actions (gather, combat, burn, stoke, use, rest, walk) persist until interrupted. One-tick actions (move, get, drop) clear after one tick. Queue feedback controlled by `queue_silently` option (default on). ## Action System Actions that interact with objects/mobs route through `StartAction()` in `action.go`: 1. Normalizes verb via `verbAliases` map (mine/chop/fish/cut → gather, talk/speak/ask → talk, pull/push → toggle) 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, startTalk, startUse, startToggle) The `verbSkill` map maps verb → skill name for default target resolution: - mine → mining, chop/cut → woodcutting, fish → fishing ### Action Types | ActionType | Trigger | Description | |---|---|---| | `gathering` | mine/chop/cut/fish | Resource gathering with skill checks, tool reqs, depletion | | `combating` | attack/kill | Combat with attack/defense rolls | | `using` | use | Crafting — consume items, produce output | | `talking` | talk/speak/ask | NPC conversation trees with choices/conditions/actions | | `toggling` | pull/push | Levers/switches — set world flags | | `burning` | burn | Firemaking startup (3 phases) | | `stoking` | stoke | Adding logs to existing fire | | `searching` | search | Search bird's nests / items with search_table | | `resting` | quit | 10-tick rest before disconnect | | `walking` | walk | Multi-step movement (one dir per tick) | | `moving` | n/s/e/w/u/d | One-tick movement state | | `picking_up` | get | One-tick pickup state | | `dropping` | drop | One-tick drop state | ### Behavior Types (YAML-driven) | Type | Verbs | Use | |---|---|---| | `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, actions | | `toggle` | pull, push | Levers, switches, gates — set world flags, conditional on existing values | ### Behaviors Not in YAML (hardcoded) | Action | File | Description | |---|---|---| | burn | `action_burn.go` | 3-phase firemaking: drop log → try burn → skill check → create fire object | | stoke | `action_burn.go` | Add logs to fire, XP per log, increments fire quality | | search | `action_search.go` | Search items with search_table, consumes one, rolls on drop table | ### 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, 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 `SuccessChance(cfg SuccessFormula, level int, requiredLevel int) float64`: ``` chance = cfg.Base + (level - requiredLevel) * cfg.PerLevel clamped to [0, cfg.Cap] ``` ### 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 | ### Condition System Used by exits, talk options, on-enter scripts, and toggle checks: | Field | Scope | Example | |---|---|---| | `flag` | World (shared by all players) | `flag: gate_open` | | `player_flag` | Per-character (quest progress) | `player_flag: finished_tutorial` | | `has_item` | Player inventory check | `has_item: bronze_key` | | `all_of` | All sub-conditions must pass | Nested list | | `any_of` | Any sub-condition passes | Nested list | | `not` | Invert the check | `not: true` | ### Firemaking (burn/stoke) See `internal/game/action_burn.go`. **Burn flow (3 phases):** - Phase 0 (inventory only): Drops 1 log to ground, outputs "You drop the X and begin to make a fire..." - Phase 1: "You try burning the X on the ground..." (waits tool_speed ticks) - Phase 2: Skill check (chance = 0.5 + (level - fireLevel) * 0.02, capped [0.05, 0.95]). On success creates `fire` object (quality = log's burn_ticks), gives XP, broadcasts. On fail retries Phase 1. **Stoke flow:** 10-tick loop → "You throw X onto the fire", gives XP, adds burn_ticks/2 to fire quality. Ends when out of item. **Fire objects:** Created via `World.AddObjInstance()`, tracked by `ObjState.Quality`. Each tick `FireTick()` decrements quality; at 0, `RemoveObjInstance()` cleans up and drops `removal_item` (e.g., ashes). **Fire tools:** `matches` (stackable, consumed), `firesteel` (non-consumable), `lighter` (quality-based, `MaxQuality: 100`, decreases per use). Tool search: main hand → inventory. **Item fields for firemaking:** `burn_ticks`, `fire_level`, `fire_xp`, `quality`, `max_quality`. ## 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: ` 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` in `tick.go`. ## 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 — each entry has independent despawn timer - Spawn items respawn after their respawn delay; nearby matching items are merged on respawn - Despawn timers visible via `despawn` option; reserve info via `reserve` option ## Combat System **Combat lock:** `LockedTicks: 15` — players cannot move for 15 ticks after entering combat. Decremented each tick. **Death mechanic:** Player drops credits to ground. If player has >3 items (inventory + equipment), the 3 most valuable are kept and rest are dropped. Player teleports to room 1 at full HP. **XP formula:** `baseXP = dmg * 4`, distributed by attack style: - Accurate: 75% Attack, 25% Hitpoints - Aggressive: 75% Strength, 25% Hitpoints - Defensive: 75% Defense, 25% Hitpoints - Balanced: 25% each Attack/Strength/Defense/Hitpoints **Mob combat level:** `0.25 * (attack + strength + defense + maxHP)` **Player combat level:** Ranged-or-melee dominant calculation. Includes Science (0.25) and Technology (0.125). **Mob respawn:** Default 30 ticks. Broadcasts spawn messages if `mob_spawn` option is on. **Mob DropTable:** `Remains` (guaranteed drop item), `Loot` (weighted DropEntry table). ## Option System Player options set via `option `: | 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 the room | | `mob_leave` | bool | true | Messages when mobs leave the room | | `mob_spawn` | bool | true | Messages when mobs spawn | | `reserve` | bool | true | Show full reserved item details | | `depletion` | bool | false | Show depletion and despawn timers | | `despawn` | bool | false | Show ground item despawn timers | | `color` | string | "none" | none/ansi/xterm256 — color output mode | | `mapwidth` | int | 30 | Map viewport width | | `mapheight` | int | 20 | Map viewport height | | `mappadding` | string | "none" | none/x/y/xy — blank-row stripping | | `automap` | bool | false | Show map automatically after moving | | `queue_silently` | bool | true | Suppress queue messages | | `room_desc_width` | int | 70 | Room desc line wrap width | | `unicode` | bool | true | Unicode box-drawing characters | | `prompt` | string | "> " | Custom command prompt | ## ItemDef Fields From `internal/object/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" / "technology" | | `stats` | attack/strength/defense/science/technology bonuses | | `speed` | Attack speed | | `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` | Default quality (e.g., lighter butane) | | `max_quality` | Maximum quality | | `search_table` | Drop table for searching this item | | `search_misc_table` | Secondary drop table | | `search_ticks` | Ticks per search action | | `search_message` | Custom search message | ## ObjectDef Fields From `internal/object/object.go`: | Field | Description | |---|---| | `id` | Unique identifier | | `name` | Display name | | `behavior` | Behavior ID for interaction | | `hidden` | Interactable but not shown in room listing | | `inroom_description` | Custom "A X is here." text | | `removal_item` | Item dropped when instance is removed (fire → ashes) | | `props` | Arbitrary key-value map (description, quality_description with `{quality}` placeholder) | ## World ObjState Fields From `internal/world/world.go`: | Field | Description | |---|---| | `DefID` | Object definition ID | | `Name` | Display name | | `Index` | Index for duplicate objects in same room | | `RoomID` | Current room | | `Depleted` | Resource is depleted | | `DepleteTimer` | Ticks until respawn | | `SharedMax` | Max shared depletion timer | | `SharedTimer` | Current shared timer value | | `WanderRooms` | Rooms for object wandering | | `WanderInterval` | Ticks between wanders | | `Quality` | Fire quality, resource health, etc. | | `JustRespawned` | Flag for respawn broadcast | ## Session States From `internal/net/server.go`: | State | Purpose | |---|---| | `StateAccountName` | Entering account name | | `StatePassword` | Entering password | | `StateNewAccountPass` | Setting password for new account | | `StateNewAccountConfirm` | Confirming password | | `StateMenu` | Main menu | | `StateNewCharName` | Naming new character | | `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` | ## 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/` Set `xp` on the behavior to award XP on successful gathers. Set `deplete_timer` for tree-style depletion, or use per-drop `depletes: true` for rock-style depletion. ## Adding a New Production Skill Same as gathering but use `type: use` behavior (see `UseConfig` in `internal/action/behavior.go`). Smithing is the reference example planned. ## Adding a New Command 1. Add a `cmd_*.go` file in `internal/game/` with the handler 2. Add a case in `handleGameCommand()` and entry in `classifyCommand()` in `internal/game/game.go` 3. Update `data/help/` YAML files ## Skills (23 total, 1-99, RSC XP table) | Category | Skills | |---|---| | Combat | Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology, Assassin | | Gathering | Fishing, Woodcutting, Mining, Hacking, Scavenging, Farming, Thieving | | Production | Cooking, Smithing, Crafting, Fletching, Alchemy, Construction, Firemaking | | Utility | Agility | Implemented: Mining, Fishing, Woodcutting, Firemaking. The rest are stubs (skill defined, XP tracked, but no behaviors/objects/items yet). ## 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 toggle checks. - 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. - `say` preserves case (alias expansion preserves case, dispatch extracts original input text). - `get`, `drop`, and `quit` cancel the player's active action (gathering, use, etc.). - `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`. - Walk distance capped by agility level (`len(dirs) > agilityLevel`). - `wear all` auto-equips the highest-value item per slot from inventory. - Combat lock prevents movement for 15 ticks after entering combat. - Level-up messages output `*** You are now level N ! ***` across all skills that gain XP.