From b8c90886ef1f3afb8d908aac89028bd177836cae Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 9 Jul 2026 16:09:35 -0400 Subject: feat(admin): multi-select with shift+drag for common bulk operations --- AGENTS.md | 200 -------------------------------------------------------------- 1 file changed, 200 deletions(-) delete mode 100644 AGENTS.md (limited to 'AGENTS.md') diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 3da4260..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,200 +0,0 @@ -# AGENTS.md - -The House of Icarus — a sci-fi MUD in Go. YAML-driven, no database. "Technology" replaces Prayer, "Science" replaces Magic, Scavenging replaces Runecrafting. - -## Build & Run - -``` -make build # go build -o thoi ./cmd/thoi -make run # build + run with DATA_DIR=./data -make test # DATA_DIR=$(PWD)/data go test ./... -timeout 60s -make vet # go vet ./... -``` - -Binary accepts `--config ` (defaults to `config.yaml`). Dependencies: `gopkg.in/yaml.v3`, `github.com/gorilla/websocket`. - -## Prefixes - -File prefixes in `internal/game/`: `game.go/cmd_registry.go/tick.go` (core), `core_*` (infra), `act_*` (actions), `cmd_*` (commands), `sys_*` (subsystems), `render_*` (output), `production_*` (crafting engine), `combat_*` (hits), `look_*` (display). - -## Key Conventions - -**YAML-driven.** Rooms, items, objects, mobs, drops, modules, courses under `data/`. Read from disk on every access — edit and it takes effect immediately. **The filename is the ID** for every data type: lookup by filename stem. **Do not put a top-level `id:` field** (ignored). A nested `- id:` under a room's `objects:`/`mobs:` is a *reference* and is still required. Subdirectories are organizational only. **Local objects:** a room `objects:` entry that carries content fields (`name`/`description`/`aliases`/`inroom_description`/`color`/`hidden`/`on_look`) is a fully local, room-scoped object def instead of a reference — passive subset only (no gather/talk/use/safespot/steal/guard_mob/on_use). Local identity derives from the normalized `name` (lowercased, whitespace-collapsed, spaces kept); `id:` is reserved for references and ignored+warned on local objects. Each object in a room must have a unique name (startup error on collision, incl. local-name vs referenced file id); names may repeat across different rooms. - -**Live state.** Player data persisted to YAML on every change. Ground items, mob instances, object states are in-memory only. - -**No database.** Everything flat YAML. Passwords use sha256 + 16-byte random salt. - -**Transport-agnostic.** `Conn` interface (`internal/net/conn.go`) — telnet and websocket. Game code works with `*Session`. - -**Tick-driven.** 600ms default (min 50ms). All game advancement per tick. YAML timer fields accept `float64`; `engine.ToTicks(base)` probabilistically rounds. - -**Concurrency.** Every state package uses its own `sync.Mutex`. Pattern: lock → snapshot/unmarshal → unlock → process. Engine creates subscriber snapshot each tick. - -## Two State Systems - -- **Global flags** (`set_global_flags` / `global_flag`): Shared by all players. -- **Player flags** (`set_player_flags` / `player_flag`): Per-character, saved to YAML. - -## Room Scripting - -- **`on_enter` steps** (`[]behavior.StepAction`): message + optional `condition` + `delay` + any of the shared `StepAction` effects (`set_global_flags`/`set_player_flags`, `give_item`/`take_item`, `teleport`, `heal`, `credits`, `aps_node`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`). Timed steps (any non-zero field triggers the timed path) run as scheduled enter sequences; otherwise messages print synchronously. Conditions snapshotted once at entry. Resumes on reconnect if interrupted. -- **Conditional descriptions** (`description: [{text, condition}]`): first passing variant wins. For objects, if no variant matches the object is absent for that player. -- **Exits** carry an `on_traverse:` list of `behavior.Interaction` (same shape as on_use) — first passing entry fires AFTER `p.RoomID` is updated to the destination, so `aps_node`/`teleport` resolve against the new room. `ExitDef.Condition` still gates whether the exit is passable at all; each `on_traverse` entry has its own additional `Condition`. - -## Data Files - -``` -data/rooms/.yaml data/items/.yaml data/mobs/.yaml -data/objects/.yaml data/objects/unique/ data/drops/.yaml -data/hazards/.yaml data/help/.yaml data/modules/.yaml -data/techs/.yaml data/courses/.yaml data/players/accounts|characters/ -``` - -Wander config per-instance in room YAML, not on MobDef. See `building_guide/` for full YAML format reference. - -## Map & Colors - -**Color specs are hex** (`00`–`FF`, case-insensitive) — the notation for the xterm-256 palette index (internally still `0–255`; only the string form is hex). Applies everywhere `color.Parse` runs: `color:` fields (rooms/items/objects), the `color`/`symbol` commands, `bg:`/`g:` tokens, and inline `{spec}text{/}` tags. Raw `ColorSpec{Fg:int}` literals stay decimal indices. `color` command targets live in `colorCategoryOrder` (`cmd_color.go`) + `config.DefaultColors()`; `map_at` (the `@`) and `map_blocked` (the blocked `X`) are themeable. - -**Map rendering** (`render_map.go`): BFS lays rooms on a 2D grid (NSEW move, up/down don't). Rooms carry an optional `color:` field (player `symbol` color overrides it). Links between two grid-adjacent rooms render per-direction: two-way → bar (`-`/`│`), one-way → arrow (`← ↑ → ↓` / `< ^ > v` pointing along travel), no traversable direction → blocked `X` (`map_blocked`). Bars/arrows blend the two endpoints' colors (`color.Average`); only `X` uses the blocked color. A player's custom symbol also shows in dim brackets on the look title. - -## Startup Validation - -Comprehensive integrity checks on all data files (errors logged, server still starts). Duplicate ID checks across all directories. Referential integrity: exits→rooms, mobs/objects/spawns→defs, drops/steal→items/tables, craft blocks→items/stations/tools, etc. Configurable orphan-room reachability check (`startup_validation.root_rooms`). Grid-overlap check: starting from `root_rooms`, lays each horizontal plane (NSEW only; up/down seed new planes) on a 2D grid and errors when two rooms collide on one cell (overlap) or a room maps to two cells (twist). - -## Command Dispatch - -`handleGameCommand()` in `game.go`: expands account aliases → split input → `classifyCommand()` → `ClassInstant` (executes now + prompt), `ClassFree` (next tick queue), `ClassActive` (sorted timestamp queue), `ClassUnknown` (verbAliases/obstacleVerbs, else error). - -## Tick-Based Action Queue - -`ProcessQueuedCommands()`: clear stale one-tick actions → execute free commands → execute active commands (sorted, first-queued wins conflicts) → advance walks → flush shared depletions. `ActionState` tracks player activity for `look`. Queue feedback shown by `show_queued_cmds`. - -## Action System - -Actions route through `startAction()` in `act.go`: normalize verb → resolve target (objects then mobs) → load behavior → route to handler. - -**Behavior types (YAML-driven):** `gather` (mine/chop/cut/fish), `use` (crafting stations), `talk` (NPC dialog). **Hardcoded actions:** burn/stoke, search, production (cook/smelt/smith/craft/mix/fletch/clean), steal, agility, farm, identify, finishing blow. Gather behaviors use SuccessChance = Base + (level-required)*PerLevel, clamped `[0, Cap]`. Action types and all YAML config fields are self-documenting in the source (`act_state.go`, `behavior.go`, `item/item.go`). - -### Interactions (unified conditional-action model) - -`behavior.Interaction` (struct in `behavior/behavior.go`) is the shared per-entry shape for every "when X happens, do Y with optional condition" feature in the game: - -- **On Use** (`on_use:` on objects) — fires on `use ` (bare, no `item_id`) or `use on ` (item-keyed). Triggered in `cmd_use.go`; queued as a one-tick action; tick executor in `act_use_interaction.go`. -- **On Look** (`on_look:` on objects) — fires after the object's description is shown on `look `. Triggered in `look_target.go`; runs the first passing entry inline (no queue). An entry with an `item_id` only fires if the player currently carries that item in their inventory. -- **On Kill** (`on_kill:` on mobs) — fires (additively over standard loot) when a combat kill of the mob completes OR when a `kind: task` mob's work drains its HP to zero (the "completion" of the task). Triggered from `endCombat` in `combat_mob.go`. An entry with an `item_id` only fires if the player is wielding that item in a weapon-hand slot (main_hand or off_hand) at the moment of the kill. -- **Exit On Traverse** (`on_traverse:` on exits) — fires AFTER `p.RoomID` is set to the destination, so `aps_node`/`teleport` resolve against the new room. Stashed in `Player.MovePendingInteraction` at move-classify time, applied in `completeMove` (`cmd_move.go`). -- **On Enter steps** (`on_enter:` on rooms) — list of `behavior.StepAction`; each step has its own `Condition` + `Message`/`Delay` + full effect set. Reconnect-resume. Driven by `enterSeq`/`EnterSeqTick` in `act_room.go`. -- **Trigger steps** (`steps:` on room/global `TriggerDef`) — same `StepAction` per step, newly supports per-step `Condition`. Trigger watcher + async playback in `sys_triggers.go` / `trigger_store.go`. The event-listener / cascade / re-entry-guard machinery is separate from interactions' inline dispatch. -- **Talk** — `TalkNode.Action` and `TalkOption.Action` are `*NodeAction` (inline subset). Wrap via `handleNodeAction` → `applyNodeAction`. - -Each list of Interactions is walked top-to-bottom; the **first** entry whose item filter and `Condition` pass wins and fires (one entry per triggering event). `applyInteraction` (`act_effects.go`) is the convenience wrapper for that walk + fire; individual callers can loop directly. The item filter is interaction-kind-specific: on_use matches the held item, on_look requires `p.HasItem`, on_kill requires `p.IsWielding`; nil disables the filter (exit traversal, on_traverse). - -**Effect superset — `behavior.StepAction`.** The universal effect container embedded by all interaction `action` fields. Holds the inline-only `NodeAction` fields (`set_global_flags`, `set_player_flags`, `give_item`, `take_item`, `teleport`, `heal`, `credits`, `aps_node`) PLUS the sequence-only fields (`message`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, `delay`) and a per-step `Condition`. Inline callers (use/look/kill/talk) ignore the sequence-only fields; on_enter / trigger steps use them. - -**One executor — `applyStepAction(sess, p, step, roomID, scope, flagValue)` in `act_effects.go`.** Scopes: `scopePlayer` (inline: flags + items + heal + credits + teleport + aps_node + broadcast), `scopePlayerSeq` (on_enter / trigger player seq: above + broadcast_global + spawn_mob + despawn_mob), `scopeGlobal` (trigger global seq: broadcasts + global flags + world-owned spawn/despawn only). `applyNodeAction` (`act_talk.go`) is a thin wrapper passing a NodeAction-wrapped StepAction at scopePlayer; `fireEnterStep` / `executeTriggerStep` / `executeGlobalTriggerStep` are thin wrappers at scopePlayerSeq / scopeGlobal. The old duplicate executors (`applyNodeAction` body, `fireEnterStep` body, `executeTriggerStep`, `executeGlobalTriggerStep`) collapsed into one in `act_effects.go`. - -`aps_node` retains its dedicated bool field — it stamps `aps_node_: true` on the player, which the `aps` command (`cmd_aps.go`) reads to build the datapad. (The room-id is dynamic, so it can't be expressed as a static `set_player_flags` entry without template substitution.) `reputation_cost` was removed (assassin reputation shop is not yet wired up). - -### Condition System - -Used by exits (exit gate + `on_traverse`), talk options/nodes, on-enter steps, on_use / on_look (objects), on_kill (mobs), trigger steps: `global_flag` (global), `player_flag` (per-char), `value` (exact match), `has_item`, `min_credits`, `all_of`, `any_of`, `not`. Bare global_flag/player_flag passes on truthy. - -## Two Depletion Mechanics - -- **Per-drop** (`depletes: true`): depletes on first successful gather of that drop. -- **Shared** (`deplete_timer: `): timer counts down while anyone gathers; at 0, next gather depletes for all. Regenerates when idle. - -## Ground Item System - -`DropDespawnTicks = 1000`. `ReserveTicks = 100` (mob loot reserved for killer). Same-ID items NOT merged. Spawn items respawn with merge. Despawn/reserve visible via options. - -## Drop Resolution (`behavior/store.go`) - -Drop tables are skill-independent weighted lists of items. `ResolveDrop` does one weighted pick; if the picked entry is a `table:` ref, its `quantity` yields that many copies of a single sub-roll (used by gather/search/steal). `ResolveDropList` (mob loot only, `combat_mob.go`) instead treats a picked table entry's `quantity` as the number of **independent rolls** against the sub-table (each may differ), returning one entry per roll; a plain item entry yields one result. Mob loot is still a single weighted pick per kill — a table entry just expands into N sub-rolls. - -## Shop System (`cmd_shop.go`) - -Root-level `shop:` on a MobDef (`behavior.ShopConfig`) — NOT part of the talk tree. Traded via plain `StateGame` commands: `list`, `buy `, `sell `, with `list ` / `buy from ` / `sell to ` to disambiguate when a room has multiple shop mobs (else "Which shop?"). No bespoke shop CLI/session state. **Prices derive from item `value:`** — shops sell at 100% of value; buy back via OSRS formula `value×max(10, buy_percentage−stock×change_percentage)/100` (defaults `buy_percentage: 40`, `change_percentage: 3`, floor 10%). Per-item `stock` (target+starting); buying depletes (out of stock at 0), selling raises it (lowering next sell price). Restock moves current stock toward target one step per item `restock_ticks` (default `behavior.ShopDefaultRestock`, set from `game_constants.shop_default_restock` in config.yaml = 1000) in **both** directions; dynamic (unlisted) items decay to 0. `buys_anything` (default true) lets specialty shops refuse unlisted items. **Live stock is per-MobInstance, in-memory only** (`ShopStock`/`ShopRestock` maps), mutated only under `MobStore.mu` via `ShopTake`/`ShopAdd`/`ShopStockOf`; restock runs in `MobStore.Tick()`. - -## Combat System - -`attack_type` (stab/slash/crush/ranged/science) selects attack/defense bonuses. **Weapons** carry a single `attack_type` string. **Mobs** carry an `attack_type` list (`[]string`, validated): exactly one melee type (stab/slash/crush) plus optionally `ranged`/`science`. A mob uses its melee type in normal combat; when a player is safespotted from melee (`safespotBlocksMelee`), the mob switches to its strongest ranged/science type (`mobStrongestRangedScience`, by max hit) via `effectiveMobAttackType`. A melee-only mob whose melee is blocked is fully blocked (`safespotBlocksMob`). Tech protection (`applyTechProtection`) uses the *resolved* attack type of the landed hit. Ranged consumes 1 ammo/attack. Aggressive mobs auto-attack if `playerLevel <= mobLevel * 2`. Movement during combat = flee attempt (mob hits abort). Death: drop credits + keep 3 most valuable items, respawn at room 1, deactivate techs. Equipment `requirements` checked before equipping. Formulas and XP splits in `combat/tracker.go`. - -## Labor System (`sys_labor.go`, `sys_hazard.go`) - -Non-violent reuse of combat engine for worksites. Task mobs (`kind: task`): HP drains to 0 to complete work (progress bar fills). Never attacks back. Commands `work`/`attack`/`kill` route to `doAttack`. Per-type defenses = method efficiency. - -**Hazards:** Room references shared hazard by ID. `HazardTick()` rolls against everyone in room every `speed` ticks. `required_item` negates. Safespot blocks all hazard damage. Lethal hit calls `killPlayer`. Does NOT interrupt movement. Safe→dangerous prompts `[Y/n]` unless `danger_warning` off. - -## Technology System (`tech.go`) - -Togglable buffs that drain battery. 4 categories: Attack/Strength/etc. tiers (5/10/15% boost), Protection techs (40% reduction), Utility (regen/drain/retribution), Combo. Reserved IDs wired to code: `protect_melee`, `protect_ranged`, `protect_science`, `retribution`. Battery = Technology level. Each tech has drain rate. Equipment TechnologyBonus reduces drain (cap 50%). ActiveTechs resets on reconnect. - -## Science System (`science.go`) - -Mods from `data/modules/`: combat/utility/transport/enchant/processing. `trigger ` fires once. `autotrigger ` auto-fires each round when deck equipped. Deck combat uses weapon speed; mod fires instead if autotrigger set. No deck = manual queuing. View with `mods`/`modules`/`module`. - -## Hacking System - -Minigame interface: 3 games at terminals (levels 1/20/40). Interface in `game/hacking/`. - -## Skills (23 total, 1-99, classic XP table) - -| Category | Skills | -| ---------- | --------------------------------------------------------------------------- | -| Combat | Attack, Strength, Defense, Hitpoints, Ranged, Science, Technology, Assassin | -| Gathering | Fishing, Woodcutting, Mining, Hacking, Scavenging, Thieving | -| Production | Cooking, Smithing, Crafting, Fletching, Pharmacy, Construction, Farming | -| Utility | Agility, Firemaking | - -## Session States - -States in `net/server.go` covering login flow, game, NPC talk, menus (bank/recipe/confirm), and hacking. (Shops are command-driven in `StateGame` — no dedicated shop state.) - -## ItemDef Fields - -Full field reference in `item/item.go`. Key fields: equip_slot, weapon_type, attack_type, stats, speed, requirements, tool_type, craft block, farming, potion, search_table. - -## CraftIndex - -`CraftIndex` built once at startup: O(1) lookups via `ByType`, `ByStation`, `ByInput`, `FindByTwoInputs`. No cache invalidation — static after startup. - -## Places to Be Careful - -- `startAction` searches objects first, then mobs. Mobs need `behavior:` on YAML def to be interactable. -- Exits use a strict `{room: }` map format. -- `checkCondition` is the single condition evaluator — shared across exits, talk, on-enter, on_use/on_look (objects), on_kill (mobs), trigger steps. Every interaction type and every timed step shares one condition language. -- Object `hidden: true` = not in room listings but still interactable. -- Alias expansion happens before command dispatch and can shadow built-ins. -- `say` preserves case from raw input. -- `get`, `drop`, `quit` cancel active action. -- `findGroundMatches`/`findInventoryMatches` support bidirectional prefix matching. -- Mob wander config is per-instance in room YAML, not on MobDef. -- Walk distance capped by agility level. -- `wear all` equips highest-value per slot; `remove all` unequips everything. -- Cape of Agility: instant movement (1 tick). Graceful pieces reduce by 0.25 each; full set bonus. Cape overrides Graceful. -- During combat, movement = flee attempt. Mob hits abort flee. -- Task mobs (`kind: task`) reuse combat engine 1:1; HP drains to 0 to complete. Never attack back — only room hazard provides danger. -- Hazard damage omits flee-abort — never interrupts walking. Mob hits still do. -- Safespot blocks ALL hazard damage, but melee work/attack forces you out of cover. -- `killPlayer` is the shared death path — used by combat and lethal hazards. `mob` may be nil. -- Science (deck) attacks use `combat.EffectiveRoll` for both rolls. `techLevelBonus(p, "science")` included in attack roll (Neural Focus). - -## Adding a New Command - -1. Add `cmd_.go` in `internal/game/` with `func (g *Game) executeXxx(sess *net.Session, args []string, rawInput string)` -2. Register in `commandRegistry` in `cmd_registry.go` -3. Add help YAML in `data/help/` - -## Known Gaps - -- **Tick subscriber ordering**: `AdvanceActions`, combat subscriptions, mob attack subscriptions, - and hazard ticks are independent subscribers without priority guarantees. A trigger completing - and a mob landing a hit on the same tick have non-deterministic ordering. -- **3D map BFS cost**: `buildGraph()` does a full 3D BFS across the entire reachable world on every - map render (`map`/`look`/move). For large worlds (10k+ rooms) this could become expensive. Options: - (a) cache the graph keyed by player room and invalidate on world edits (`dig`/`room insert`/`undig`), - (b) prune by visited rooms (revert to horizontal-only expansion from unvisited), or (c) compute z-level - assignments once at startup and use precomputed plane membership. -- cgit v1.2.3