From 3102525d97aa6f1ad152c74b2ccfbd4b3a9b83f5 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 26 Jun 2026 02:52:31 -0400 Subject: feat: simplified conversation trees and output won't overwrite prompts now --- AGENTS.md | 577 ++++++++------------------------------------------------------ 1 file changed, 72 insertions(+), 505 deletions(-) (limited to 'AGENTS.md') 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 ` (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 ` (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/.yaml Room definitions (exits, objects, mobs, item_spawns, on_enter, description) -data/rooms/player_housing/ Player house rooms -data/items/.yaml Item definitions (stats, equip_slot, tool_type, craft, burn_ticks, search_table, etc.) -data/mobs/.yaml Mob definitions (combat stats, drops, behavior, steal config; kind: task for worksites) -data/objects/.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/.yaml Shared drop tables (weighted item lists, sub-table references) -data/hazards/.yaml Shared room hazard definitions (attack_type, attack, max_hit, speed, messages, required_item) -data/help/.yaml Help topics -data/modules/.yaml Science module definitions (level, max_hit, base_xp, junk_cost, category) -data/techs/.yaml Technology definitions (name, level, drain_rate, category, group, effects) -data/courses/.yaml Agility course definitions (name, required_level, start_room, obstacles) -data/players/accounts/ Account YAML (gitignored) -data/players/characters/ Character YAML (gitignored) +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 (`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: ` 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: `): 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 ` 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`. -**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: ` → `data/hazards/.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 ` — 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 ` — 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 ` 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 `. 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_` (XP), `%x_` (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 ! ***` 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_.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_.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/` -- cgit v1.2.3