1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
# AGENTS.md
The House of Icarus — a sci-fi MUD in Go. YAML-driven, no database. "Technology" replaces Prayer, "Science" replaces Magic, Scavenging replaces Runecrafting.
## Build & Run
```
make build # go build -o thoi ./cmd/mud
make run # build + run with DATA_DIR=./data
make test # DATA_DIR=$(PWD)/data go test ./... -timeout 60s
make vet # go vet ./...
```
Binary accepts `--config <path>` (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.
**Live state.** Player data persisted to YAML on every change. Ground items, mob instances, object states are in-memory only.
**No database.** Everything flat YAML. Passwords use sha256 + 16-byte random salt.
**Transport-agnostic.** `Conn` interface (`internal/net/conn.go`) — telnet and websocket. Game code works with `*Session`.
**Tick-driven.** 600ms default (min 50ms). All game advancement per tick. YAML timer fields accept `float64`; `engine.ToTicks(base)` probabilistically rounds.
**Concurrency.** Every state package uses its own `sync.Mutex`. Pattern: lock → snapshot/unmarshal → unlock → process. Engine creates subscriber snapshot each tick.
## Two State Systems
- **World flags** (`set_flags` / `flag`): Shared by all players.
- **Player flags** (`set_player_flags` / `player_flag`): Per-character, saved to YAML.
## Room Scripting
- **`on_enter` steps**: message + optional condition. A step may have `delay` and/or `set_flags`/`set_player_flags`. Timed steps run as scheduled enter sequences; otherwise messages print synchronously. Conditions snapshotted once at entry. Resumes on reconnect if interrupted.
- **Conditional descriptions** (`description: [{text, condition}]`): first passing variant wins. For objects, if no variant matches the object is absent for that player. Accepts plain string or list.
- **Exits** can `set_flags`/`set_player_flags` on successful traverse.
## Data Files
```
data/rooms/<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 per-instance in room YAML, not on MobDef. See `building_guide/` for full YAML format reference.
## 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.check_sources`).
## 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 suppressed by `queue_silently`.
## Action System
Actions route through `startAction()` in `act.go`: normalize verb → resolve target (objects then mobs) → load behavior → route to handler.
**Behavior types (YAML-driven):** `gather` (mine/chop/cut/fish), `use` (crafting stations), `talk` (NPC dialog). **Hardcoded actions:** burn/stoke, search, production (cook/smelt/smith/craft/mix/fletch/clean), steal, agility, farm, identify, finishing blow. Gather behaviors use SuccessChance = Base + (level-required)*PerLevel, clamped `[0, Cap]`. Action types and all YAML config fields are self-documenting in the source (`act_state.go`, `behavior.go`, `item/item.go`).
### Condition System
Used by exits, talk options, on-enter, use_interactions: `flag` (world), `player_flag` (per-char), `value` (exact match), `has_item`, `min_credits`, `all_of`, `any_of`, `not`. Bare flag/player_flag passes on truthy.
## Two Depletion Mechanics
- **Per-drop** (`depletes: true`): depletes on first successful gather of that drop.
- **Shared** (`deplete_timer: <ticks>`): 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.
## Combat System
`attack_type` (stab/slash/crush/ranged/science) selects attack/defense bonuses. Ranged consumes 1 ammo/attack. Aggressive mobs auto-attack if `playerLevel <= mobLevel * 2`. Movement during combat = flee attempt (mob hits abort). Death: drop credits + keep 3 most valuable items, respawn at room 1, deactivate techs. Equipment `requirements` checked before equipping. Formulas and XP splits in `combat/tracker.go`.
## Labor System (`sys_labor.go`, `sys_hazard.go`)
Non-violent reuse of combat engine for worksites. Task mobs (`kind: task`): HP drains to 0 to complete work (progress bar fills). Never attacks back. Commands `work`/`attack`/`kill` route to `doAttack`. Per-type defenses = method efficiency.
**Hazards:** Room references shared hazard by ID. `HazardTick()` rolls against everyone in room every `speed` ticks. `required_item` negates. Safespot blocks all hazard damage. Lethal hit calls `killPlayer`. Does NOT interrupt movement. Safe→dangerous prompts `[Y/n]` unless `danger_warning` off.
## Technology System (`tech.go`)
Togglable buffs that drain battery. 4 categories: Attack/Strength/etc. tiers (5/10/15% boost), Protection techs (40% reduction), Utility (regen/drain/retribution), Combo. Reserved IDs wired to code: `protect_melee`, `protect_ranged`, `protect_science`, `retribution`. Battery = Technology level. Each tech has drain rate. Equipment TechnologyBonus reduces drain (cap 50%). ActiveTechs resets on reconnect.
## Science System (`science.go`)
Mods from `data/modules/`: combat/utility/transport/enchant/processing. `trigger <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`.
## 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
20 states in `net/server.go` covering login flow, game, NPC talk, menus (bank/shop/recipe/confirm), and hacking.
## ItemDef Fields
Full field reference in `item/item.go`. Key fields: equip_slot, weapon_type, attack_type, stats, speed, requirements, tool_type, craft block, farming, potion, search_table.
## CraftIndex
`CraftIndex` built once at startup: O(1) lookups via `ByType`, `ByStation`, `ByInput`, `FindByTwoInputs`. No cache invalidation — static after startup.
## Places to Be Careful
- `startAction` searches objects first, then mobs. Mobs need `behavior:` on YAML def to be interactable.
- Exits: old `north: 2` still works via custom `UnmarshalYAML`. Same for `RoomMob`.
- `checkCondition` is the single condition evaluator — shared across exits, talk, on-enter, use_interactions.
- Object `hidden: true` = not in room listings but still interactable.
- Alias expansion happens before command dispatch and can shadow built-ins.
- `say` preserves case from raw input.
- `get`, `drop`, `quit` cancel active action.
- `findGroundMatches`/`findInventoryMatches` support bidirectional prefix matching.
- Mob wander config is per-instance in room YAML, not on MobDef.
- Walk distance capped by agility level.
- `wear all` equips highest-value per slot; `remove all` unequips everything.
- Cape of Agility: instant movement (1 tick). Graceful pieces reduce by 0.25 each; full set bonus. Cape overrides Graceful.
- During combat, movement = flee attempt. Mob hits abort flee.
- Task mobs (`kind: task`) reuse combat engine 1:1; HP drains to 0 to complete. Never attack back — only room hazard provides danger.
- Hazard damage omits flee-abort — never interrupts walking. Mob hits still do.
- Safespot blocks ALL hazard damage, but melee work/attack forces you out of cover.
- `killPlayer` is the shared death path — used by combat and lethal hazards. `mob` may be nil.
- Science (deck) attacks use `combat.EffectiveRoll` for both rolls. `techLevelBonus(p, "science")` included in attack roll (Neural Focus).
## Adding a New Command
1. Add `cmd_<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/`
## 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.
|