aboutsummaryrefslogtreecommitdiff
path: root/skill_plans/thieving.md
diff options
context:
space:
mode:
Diffstat (limited to 'skill_plans/thieving.md')
-rw-r--r--skill_plans/thieving.md1391
1 files changed, 0 insertions, 1391 deletions
diff --git a/skill_plans/thieving.md b/skill_plans/thieving.md
deleted file mode 100644
index 90398fc..0000000
--- a/skill_plans/thieving.md
+++ /dev/null
@@ -1,1391 +0,0 @@
-# Thieving Skill Implementation Plan
-
-## 1. Overview
-
-Thieving is a gathering-category skill that allows players to steal from mobs and objects. Unlike mining/fishing/woodcutting which use the behavior-YAML-driven `gather` system, thieving is a **hardcoded action type** (like `burn` and `search`) because it has unique mechanics:
-
-- Mob aggro on failure (mobs turn hostile)
-- Guard watching cycles on objects (tick-based state machine)
-- Guard spawning on watched-object failures
-- Sneak mode toggle with real-time guard awareness notifications
-
-The skill uses the existing `Thieving` constant already defined in `internal/player/player.go` (line 25, abbreviation `"thv"` at line 57).
-
-Commands: `steal` (active), `sneak` (instant toggle). Aliases: `thieve` maps to `steal`.
-
----
-
-## 2. Commands
-
-### `steal` / `thieve`
-
-- **Classification:** `ClassActive`
-- **Aliases:** `thieve` → `steal` (added to `verbAliases` in `action.go`)
-- **Syntax:**
- - `steal` — auto-resolves if only one stealable target (mob or object) in room
- - `steal <target>` — steal from a specific mob or object by name
- - `steal 2.man` — steal from the 2nd man (numbered targeting)
-- **Behavior:** Searches mobs first (reversed from `StartAction` which searches objects first), then objects. This is because stealing from mobs is the primary use case.
-
-### `sneak`
-
-- **Classification:** `ClassInstant`
-- **Syntax:** `sneak` — toggles sneak mode on/off
-- **Behavior:** Sets `p.Sneaking` (new bool field on Player, transient/not saved). While sneaking, `SneakTick()` sends guard-watching notifications each tick.
-
----
-
-## 3. New Files to Create
-
-### Go Files
-
-| File | Purpose |
-|---|---|
-| `internal/game/action_steal.go` | `doSteal()`, `startSteal()`, `advanceSteal()`, `resolveStealTarget()`, mob aggro logic, guard alert logic |
-| `internal/game/cmd_sneak.go` | `doSneak()` toggle handler, `SneakTick()` for guard-watching notifications |
-
-### YAML Data Files
-
-| File | Purpose |
-|---|---|
-| `data/items/credit_stick.yaml` | Credit stick item (searchable) |
-| `data/items/potato_seed.yaml` | Low-value seed |
-| `data/items/onion_seed.yaml` | Low-value seed |
-| `data/items/cabbage_seed.yaml` | Low-value seed |
-| `data/items/tomato_seed.yaml` | Low-mid value seed |
-| `data/items/sweetcorn_seed.yaml` | Mid value seed |
-| `data/items/strawberry_seed.yaml` | Mid value seed |
-| `data/items/watermelon_seed.yaml` | Mid-high value seed |
-| `data/items/ranarr_seed.yaml` | High value seed |
-| `data/items/snapdragon_seed.yaml` | High value seed |
-| `data/items/torstol_seed.yaml` | Very high value seed |
-| `data/items/bread.yaml` | Low-value food from stall |
-| `data/items/apple.yaml` | Low-value food from stall |
-| `data/items/cheese.yaml` | Low-value food from stall |
-| `data/objects/market_stall.yaml` | Market stall object (stealable) |
-| `data/mobs/farmer.yaml` | Farmer mob (stealable, low-mid seeds) |
-| `data/mobs/bioengineer.yaml` | Bioengineer mob (stealable, mid-high seeds) |
-| `data/drops/credit_stick_drop.yaml` | Drop table for credit stick search |
-| `data/drops/man_steal.yaml` | Drop table for stealing from man |
-| `data/drops/farmer_steal.yaml` | Drop table for stealing from farmer |
-| `data/drops/bioengineer_steal.yaml` | Drop table for stealing from bioengineer |
-| `data/drops/market_stall_steal.yaml` | Drop table for stealing from market stall |
-| `data/behaviors/stall_guard_talk.yaml` | Talk behavior for the guard who catches you |
-| `data/rooms/150.yaml` | Market Square (stall + guard + men) |
-| `data/rooms/151.yaml` | Farm Outpost (farmer + bioengineer) |
-| `data/rooms/152.yaml` | Detention Cell (jail room) |
-| `data/help/steal.yaml` | Help topic for steal |
-| `data/help/sneak.yaml` | Help topic for sneak |
-| `data/help/thieving.yaml` | Help topic for thieving skill |
-
----
-
-## 4. Code Changes to Existing Files
-
-### `internal/player/player.go`
-
-Add a transient `Sneaking` field to the `Player` struct:
-
-```go
-// In the Player struct, after the existing transient fields:
-Sneaking bool `yaml:"-"`
-```
-
-Add after line 174 (`VisualTickCurrent int`):
-```go
-Sneaking bool `yaml:"-"`
-```
-
-### `internal/game/action_state.go`
-
-Add the new `ActionType` constant. After `ActionEating` (line 24):
-
-```go
-ActionStealing ActionType = "stealing"
-```
-
-Add a `Description()` case inside the switch (after the `ActionEating` case, around line 75):
-
-```go
-case ActionStealing:
- return "stealing from " + a.TargetName
-```
-
-### `internal/game/action.go`
-
-Add `steal` aliases to `verbAliases` map (after `"push": "toggle"` on line 27):
-
-```go
-"steal": "steal",
-"thieve": "steal",
-```
-
-Add to `verbSkill` map (after `"shear": "crafting"` on line 36):
-
-```go
-"steal": "thieving",
-"thieve": "thieving",
-```
-
-Add `"steal"` case to `AdvanceActions()` switch (after `case "search":` block, around line 235):
-
-```go
-case "steal":
- g.advanceSteal(sess, p)
-```
-
-### `internal/game/game.go`
-
-#### `classifyCommand()` — line 136
-
-Add `"sneak"` to the `ClassInstant` list (line 138-142):
-
-```go
-case "say", "score", "sc", "inventory", "i", "inv",
- "look", "l", "exits", "help",
- "map", "option", "options", "alias", "unalias",
- "description", "desc", "queued", "color", "colors",
- "colortable", "prompt", "style", "sneak":
- return ClassInstant
-```
-
-Add `"steal", "thieve"` to the `ClassActive` list (line 146-151):
-
-```go
-case "get", "take", "grab", "pick", "drop",
- "attack", "kill",
- "north", "n", "south", "s", "east", "e",
- "west", "w", "up", "u", "down", "d",
- "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith", "craft",
- "steal", "thieve":
- return ClassActive
-```
-
-#### `executeCommand()` — line 249
-
-Add `"sneak"` case in the instant section (after the `"colortable"` case, around line 329):
-
-```go
-case "sneak":
- g.doSneak(sess)
-```
-
-Add `"steal", "thieve"` case in the active section (after the `"search"` block, around line 417):
-
-```go
-case "steal", "thieve":
- g.CancelAction(p)
- if len(args) == 0 {
- g.doSteal(sess, "")
- } else {
- g.doSteal(sess, strings.Join(args, " "))
- }
- return
-```
-
-#### `ProcessQueuedCommands()` — line 457
-
-Add `ActionStealing` to the list of persistent action states that don't get cleared (line 472):
-
-```go
-case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
- ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing,
- ActionStealing:
-```
-
-### `cmd/mud/main.go`
-
-Add `SneakTick()` to the tick subscriber (after `g.VisualTick()` on line 55):
-
-```go
-g.SneakTick()
-```
-
-### `internal/world/mob.go`
-
-Add steal-related fields to `MobDef` struct (after `Drops` field, line 36):
-
-```go
-StealTable string `yaml:"steal_table"`
-StealLevel int `yaml:"steal_level"`
-StealXP int `yaml:"steal_xp"`
-StealSpeed float64 `yaml:"steal_speed"`
-```
-
-Add corresponding runtime fields to `MobInstance` struct (after `regenerateTick` field, line 61):
-
-```go
-StealTable string
-StealLevel int
-StealXP int
-StealSpeed float64
-```
-
-In the mob instantiation function (wherever `MobInstance` is created from `MobDef`, in `mob.go`), copy the steal fields:
-
-```go
-inst.StealTable = def.StealTable
-inst.StealLevel = def.StealLevel
-inst.StealXP = def.StealXP
-inst.StealSpeed = def.StealSpeed
-```
-
-Search for `func (s *MobStore) spawnMob` or equivalent — it's the function that creates `MobInstance` from `MobDef`. The steal fields must be copied there. Around line 160-200 in `mob.go`, find where instances are built and add the four field copies.
-
-### `internal/object/object.go`
-
-Add steal-related fields to `ObjectDef` struct:
-
-```go
-StealTable string `yaml:"steal_table"`
-StealLevel int `yaml:"steal_level"`
-StealXP int `yaml:"steal_xp"`
-StealSpeed float64 `yaml:"steal_speed"`
-GuardMob string `yaml:"guard_mob"`
-```
-
-- `steal_table`: Drop table ID for loot when stealing from this object
-- `steal_level`: Required thieving level
-- `steal_xp`: XP awarded per successful steal
-- `steal_speed`: Ticks per steal attempt (base wait)
-- `guard_mob`: Mob def ID that guards this object (watches it)
-
----
-
-## 5. Items
-
-### `data/items/credit_stick.yaml`
-
-```yaml
-id: credit_stick
-name: credit stick
-color: "220"
-description: "A small electronic stick loaded with credits. You can search it to extract the credits."
-value: 10
-stackable: false
-search_table: credit_stick_drop
-search_ticks: 2
-search_message: "cracking open the credit stick"
-```
-
-### `data/items/bread.yaml`
-
-```yaml
-id: bread
-name: bread
-color: "179"
-description: "A crusty loaf of bread."
-value: 5
-stackable: false
-heal_value: 3
-eat_message: "You eat the bread. Not bad."
-```
-
-### `data/items/apple.yaml`
-
-```yaml
-id: apple
-name: apple
-color: "196"
-description: "A bright red apple."
-value: 3
-stackable: false
-heal_value: 2
-eat_message: "You eat the apple. Refreshing."
-```
-
-### `data/items/cheese.yaml`
-
-```yaml
-id: cheese
-name: cheese
-color: "226"
-description: "A wedge of sharp cheese."
-value: 4
-stackable: false
-heal_value: 2
-eat_message: "You eat the cheese. Tasty."
-```
-
-### `data/items/potato_seed.yaml`
-
-```yaml
-id: potato_seed
-name: potato seed
-color: "94"
-description: "A seed for growing potatoes."
-value: 2
-stackable: true
-```
-
-### `data/items/onion_seed.yaml`
-
-```yaml
-id: onion_seed
-name: onion seed
-color: "229"
-description: "A seed for growing onions."
-value: 3
-stackable: true
-```
-
-### `data/items/cabbage_seed.yaml`
-
-```yaml
-id: cabbage_seed
-name: cabbage seed
-color: "34"
-description: "A seed for growing cabbages."
-value: 4
-stackable: true
-```
-
-### `data/items/tomato_seed.yaml`
-
-```yaml
-id: tomato_seed
-name: tomato seed
-color: "196"
-description: "A seed for growing tomatoes."
-value: 8
-stackable: true
-```
-
-### `data/items/sweetcorn_seed.yaml`
-
-```yaml
-id: sweetcorn_seed
-name: sweetcorn seed
-color: "226"
-description: "A seed for growing sweetcorn."
-value: 25
-stackable: true
-```
-
-### `data/items/strawberry_seed.yaml`
-
-```yaml
-id: strawberry_seed
-name: strawberry seed
-color: "197"
-description: "A seed for growing strawberries."
-value: 40
-stackable: true
-```
-
-### `data/items/watermelon_seed.yaml`
-
-```yaml
-id: watermelon_seed
-name: watermelon seed
-color: "34"
-description: "A seed for growing watermelons."
-value: 80
-stackable: true
-```
-
-### `data/items/ranarr_seed.yaml`
-
-```yaml
-id: ranarr_seed
-name: ranarr seed
-color: "28"
-description: "A rare herb seed with potent alchemical properties."
-value: 500
-stackable: true
-```
-
-### `data/items/snapdragon_seed.yaml`
-
-```yaml
-id: snapdragon_seed
-name: snapdragon seed
-color: "92"
-description: "An extremely rare herb seed. Highly valued by alchemists."
-value: 1500
-stackable: true
-```
-
-### `data/items/torstol_seed.yaml`
-
-```yaml
-id: torstol_seed
-name: torstol seed
-color: "46"
-description: "The rarest of herb seeds. Worth a small fortune."
-value: 5000
-stackable: true
-```
-
----
-
-## 6. Mobs
-
-### `data/mobs/man.yaml` (UPDATE existing file)
-
-Add `steal_table`, `steal_level`, `steal_xp`, and `steal_speed` fields:
-
-```yaml
-id: man
-name: man
-description: "A shabby-looking man loitering in the town square."
-combat_descriptions:
- - "is engaged in a fight to the death with %s"
- - "is getting pummelled by %s"
- - "is locked in combat with %s"
- - "trades blows with %s"
- - "circles warily around %s"
-idle_descriptions:
- - "scribbles something in a small notebook"
- - "gazes skyward at the clouds"
- - "leans against a wall, looking bored"
- - "scratches his head thoughtfully"
- - "stares off into the distance"
- - "adjusts his tunic and stretches"
-attack: 1
-strength: 1
-defense: 1
-hp: 7
-speed: 5
-aggressive: false
-respawn_ticks: 30
-steal_table: man_steal
-steal_level: 1
-steal_xp: 8
-steal_speed: 4
-drops:
- remains: "bones"
- loot:
- - item_id: "credits"
- weight: 98
- quantity: 10
- - item_id: "credits"
- weight: 2
- quantity: 150
-```
-
-### `data/mobs/farmer.yaml`
-
-```yaml
-id: farmer
-name: Farmer
-description: "A weathered farmer in muddy overalls, pockets bulging with seeds."
-combat_descriptions:
- - "swings a shovel at %s"
- - "is getting beaten by %s"
- - "grapples with %s"
-idle_descriptions:
- - "examines a handful of seeds"
- - "wipes dirt from his hands"
- - "mutters about the growing season"
- - "adjusts his wide-brimmed hat"
-attack: 3
-strength: 3
-defense: 3
-hp: 15
-speed: 5
-aggressive: false
-respawn_ticks: 40
-steal_table: farmer_steal
-steal_level: 10
-steal_xp: 15
-steal_speed: 4
-drops:
- remains: "bones"
- loot:
- - item_id: "potato_seed"
- weight: 40
- quantity: 3
- - item_id: "onion_seed"
- weight: 30
- quantity: 2
- - item_id: "cabbage_seed"
- weight: 20
- quantity: 2
- - item_id: "tomato_seed"
- weight: 10
- quantity: 1
-```
-
-### `data/mobs/bioengineer.yaml`
-
-```yaml
-id: bioengineer
-name: Bioengineer
-description: "A lab-coated scientist carrying a satchel of genetically modified seeds. Her pockets are stuffed with rare specimens."
-combat_descriptions:
- - "jabs a syringe at %s"
- - "is being overpowered by %s"
- - "fights desperately against %s"
-idle_descriptions:
- - "scribbles notes on a clipboard"
- - "carefully inspects a vial of green liquid"
- - "adjusts her safety goggles"
- - "mutters about gene splicing yields"
-attack: 6
-strength: 4
-defense: 5
-hp: 25
-speed: 5
-aggressive: false
-respawn_ticks: 50
-steal_table: bioengineer_steal
-steal_level: 38
-steal_xp: 45
-steal_speed: 4
-drops:
- remains: "bones"
- loot:
- - item_id: "sweetcorn_seed"
- weight: 30
- quantity: 2
- - item_id: "strawberry_seed"
- weight: 25
- quantity: 1
- - item_id: "watermelon_seed"
- weight: 15
- quantity: 1
- - item_id: "ranarr_seed"
- weight: 5
- quantity: 1
-```
-
----
-
-## 7. Objects
-
-### `data/objects/market_stall.yaml`
-
-```yaml
-id: market_stall
-name: Market Stall
-description: "A wooden stall piled with food and sundries. The vendor doesn't seem particularly attentive."
-inroom_description: "A bustling {179}market stall{/} is set up here."
-hidden: false
-steal_table: market_stall_steal
-steal_level: 5
-steal_xp: 12
-steal_speed: 5
-guard_mob: guard
-```
-
-Note: `guard_mob: guard` means a mob with def ID `guard` in the same room is watching this stall. The `guard_mob` field is a reference — the actual mob must be placed in the room YAML via the `mobs:` list. If the guard mob is present in the room at the time of the steal, the watching mechanic activates.
-
----
-
-## 8. Rooms
-
-### `data/rooms/150.yaml` — Market Square
-
-```yaml
-id: 150
-name: "Market Square"
-description: "A noisy open-air market wedged between crumbling hab-blocks. Vendors hawk salvaged tech and reconstituted food from makeshift stalls. A {220 bold}Guard{/} watches over the area with a stern expression."
-map_symbol: "M"
-exits:
- south: 100
-objects:
- - id: market_stall
-mobs:
- - id: man
- wander_interval: 15
- - id: man
- wander_interval: 18
- - id: man
- wander_interval: 20
- - id: guard
-```
-
-### `data/rooms/151.yaml` — Farm Outpost
-
-```yaml
-id: 151
-name: "Farm Outpost"
-description: "A cluster of hydroponic grow-pods on the asteroid's surface, shielded by a flickering atmospheric dome. Rows of bio-luminescent crops stretch into the distance."
-map_symbol: "F"
-exits:
- west: 150
-mobs:
- - id: farmer
- wander_rooms: [151]
- - id: farmer
- wander_rooms: [151]
- - id: bioengineer
- wander_rooms: [151]
-```
-
-### `data/rooms/152.yaml` — Detention Cell
-
-```yaml
-id: 152
-name: "Detention Cell"
-description: "A small, grimy holding cell. The walls are scratched with tally marks from previous occupants. A heavy door bars the only exit."
-map_symbol: "J"
-exits:
- south: 100
-```
-
-### Room connectivity
-
-Add an exit from room 100 (Grand Concourse) to room 150:
-
-In `data/rooms/100.yaml`, add `north: 110` already exists. Add `south: 150`:
-
-```yaml
-id: 100
-name: "Grand Concourse"
-description: "The expansive white platform of Station X1's main thoroughfare. Neon strips pulse along the ceiling, reflecting off polished permacrete floors. Citizens and synthetics stream past in a constant dance of commerce and purpose."
-map_symbol: "+"
-exits:
- east: 101
- north: 110
- south: 150
-```
-
-Add an exit from room 150 to 151:
-
-Already handled: room 150 has `south: 100`, and room 151 has `west: 150`. Add `east: 151` to room 150's exits.
-
-Updated room 150 exits:
-```yaml
-exits:
- south: 100
- east: 151
-```
-
----
-
-## 9. Mechanics
-
-### Steal Success Formula
-
-Uses the existing `SuccessChance` pattern from `internal/action/store.go`:
-
-```
-chance = base + (thievingLevel - requiredLevel) * perLevel
-clamped to [0, cap]
-```
-
-Constants (hardcoded in `action_steal.go`):
-
-```go
-var stealSuccess = action.SuccessFormula{
- Base: 0.5,
- PerLevel: 0.03,
- Cap: 0.95,
-}
-```
-
-When a guard mob is watching (see Guard Watching Cycle), the chance is halved:
-
-```go
-if guardWatching {
- chance *= 0.5
-}
-```
-
-Minimum chance is 0.05 (5%) even when halved.
-
-### Steal from Mob — Failure Consequence
-
-On a failed steal against a mob:
-1. The mob turns aggressive **toward the player only** — initiates combat.
-2. Message: `"The <mob name> notices you! They attack!"`
-3. Combat starts via the existing `g.startCombat(sess, p, mob)` call.
-4. The mob must not already be in combat (`combat.IsMobInCombat`). If it is, output `"The <mob name> is busy."` and cancel.
-
-### Steal from Object — Failure Consequence
-
-On a failed steal against a guarded object:
-- **If the guard mob is watching:** The guard calls for backup. A `stall_guard` mob instance is spawned in the room (or the existing guard initiates a talk dialog). The player enters `StateTalk` with the `stall_guard_talk` behavior offering: bribe, jail, or fight.
-- **If the guard mob is NOT watching:** Simple failure message. `"You fail to steal anything."` No consequences.
-- **If no guard mob exists in the room:** Simple failure, no consequences.
-
-### Sneak Mode Toggle
-
-- `sneak` toggles `p.Sneaking` bool.
-- When enabled: `"You begin sneaking."` — the player is now in sneak mode.
-- When disabled: `"You stop sneaking."` — normal mode.
-- Sneak mode is **transient** (not saved to YAML). Lost on disconnect.
-- Sneak mode does NOT affect movement or other actions — it only enables guard-watching notifications and improves steal chance on guarded objects.
-
-### Guard Watching Cycle
-
-Each tick, `SneakTick()` runs for all sneaking players. For each sneaking player:
-
-1. Find all objects in the room with a `guard_mob` field.
-2. For each such object, check if a living mob with that def ID is in the room.
-3. If a guard is present, use a tick-based watching cycle:
- - The guard watches the object for `watchDuration` ticks (8 ticks), then looks away for `lookAwayDuration` ticks (4 ticks), cycling.
- - Tracked via world-level state: `guardWatchTimers map[string]int` on the `Game` struct, keyed by `"roomID:objDefID"`.
- - Each tick the counter increments. If `counter % (watchDuration + lookAwayDuration) < watchDuration`, the guard is watching.
-4. Send a message to the sneaking player:
- - Watching: `"The Guard is watching the Market Stall."`
- - Not watching: `"The Guard looks away from the Market Stall."`
- - Only send on state **transitions** (watching→not watching, not watching→watching), not every tick. Track last-known state per player per object.
-
-To track per-player notification state, add a transient field to Player:
-
-```go
-SneakNotified map[string]bool `yaml:"-"` // key: "roomID:objDefID", value: last known watching state
-```
-
-### Guard Watching — Implementation on Game struct
-
-Add to `Game` struct:
-
-```go
-guardWatchTimers map[string]int // key: "roomID:objDefID", value: tick counter
-```
-
-Initialize in `New()`:
-
-```go
-guardWatchTimers: make(map[string]int),
-```
-
----
-
-## 10. Action Lifecycle
-
-### File: `internal/game/action_steal.go`
-
-```go
-package game
-
-import (
- "fmt"
- "math/rand"
- "sort"
- "strconv"
- "strings"
-
- "thehouseoficarus/internal/action"
- "thehouseoficarus/internal/combat"
- "thehouseoficarus/internal/engine"
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/object"
- "thehouseoficarus/internal/player"
- "thehouseoficarus/internal/world"
-)
-
-var stealSuccess = action.SuccessFormula{
- Base: 0.5,
- PerLevel: 0.03,
- Cap: 0.95,
-}
-```
-
-### `doSteal(sess *net.Session, input string)`
-
-Entry point called from `executeCommand`. Signature:
-
-```go
-func (g *Game) doSteal(sess *net.Session, input string)
-```
-
-Logic:
-1. `p := sess.Player.(*player.Player)`
-2. Check `combat.GetCombat(p.Name) != nil` → `"You can't do that during combat!"`
-3. `g.CancelAction(p)`
-4. Call `g.resolveStealTarget(sess, p, input)` → returns `(targetType string, mob *world.MobInstance, obj *object.ObjectDef, err string)`
-5. If err != "" → `sess.WriteLine(err); return`
-6. Call `g.startSteal(sess, p, targetType, mob, obj)`
-
-### `resolveStealTarget(sess *net.Session, p *player.Player, input string) (targetType string, mob *world.MobInstance, obj *object.ObjectDef, errMsg string)`
-
-Logic:
-1. Parse `input` for numbered targeting (`N.name` → `instanceIdx`, `name`).
-2. If `input == ""` (no target specified):
- a. Collect all stealable mobs in room (those with `StealTable != ""`).
- b. Collect all stealable objects in room (those with `StealTable != ""`).
- c. Combined count: if 0 → return `"", nil, nil, "There's nothing here to steal from."`
- d. If all stealable targets are the same mob def → auto-select first mob. Return `"mob", mob, nil, ""`.
- e. If exactly 1 stealable object and 0 stealable mobs → auto-select object. Return `"object", nil, obj, ""`.
- f. If multiple different types → return `"", nil, nil, "Steal from what?"`.
-3. If `input != ""`:
- a. Search mobs in room with `StealTable != ""` matching input (using `mob.MatchQuality(name)`).
- b. If multiple mobs found with different def IDs → `"Which one?"`.
- c. If mobs found → apply `instanceIdx`, return `"mob", selectedMob, nil, ""`.
- d. If no mob found, search objects in room with `StealTable != ""` matching input (using `world.WordPrefixMatch`).
- e. If object found → return `"object", nil, objDef, ""`.
- f. If nothing → return `"", nil, nil, "There's nothing here to steal from."`.
-
-### `startSteal(sess *net.Session, p *player.Player, targetType string, mob *world.MobInstance, obj *object.ObjectDef)`
-
-Logic:
-1. Determine `stealTable`, `stealLevel`, `stealXP`, `stealSpeed`, `targetName`, `targetID`:
- - If `targetType == "mob"`: from `mob.StealTable`, `mob.StealLevel`, `mob.StealXP`, `mob.StealSpeed`, `mob.Name`, `mob.InstanceID`
- - If `targetType == "object"`: from `obj.StealTable`, `obj.StealLevel`, `obj.StealXP`, `obj.StealSpeed`, `obj.Name`, `obj.ID`
-2. Check `stealTable == ""` → `"You can't steal from the <name>."`
-3. Check level requirement: `p.Level(player.Thieving) < stealLevel` → `"You need level <N> thieving to steal from the <name>."`
-4. Check inventory space: `p.FirstFreeSlot() == -1` → `"Your inventory is too full!"`
-5. If mob target, check `mob.HP <= 0` → `"That is already dead."`. Check `combat.IsMobInCombat(mob.InstanceID)` → `"The <name> is busy."`
-6. Determine `guardWatching` (only for object targets with `obj.GuardMob != ""`):
- - Check if a mob with DefID == `obj.GuardMob` exists in room and is alive.
- - If so, check the guard watch timer cycle to determine if watching.
-7. Create action:
-
-```go
-sess.WriteLine(fmt.Sprintf("You attempt to steal from the %s...", targetName))
-
-p.ActionState = &ActionState{Type: ActionStealing, TargetName: targetName}
-
-p.Action = &action.Action{
- Type: "steal",
- TargetID: targetID,
- TargetName: targetName,
- WaitLeft: engine.ToTicks(stealSpeed),
- Data: map[string]any{
- "target_type": targetType,
- "steal_table": stealTable,
- "steal_level": stealLevel,
- "steal_xp": stealXP,
- "target_name": targetName,
- "mob_instance_id": mobInstanceID, // "" if object
- "obj_def_id": objDefID, // "" if mob
- "guard_mob": guardMob, // "" if no guard
- "guard_watching": guardWatching,
- },
-}
-```
-
-### `advanceSteal(sess *net.Session, p *player.Player)`
-
-Called from `AdvanceActions()` when `p.Action.Type == "steal"` and timer reaches 0.
-
-Logic:
-1. Extract all data fields from `p.Action.Data`.
-2. Validate target still exists:
- - If mob: check `g.MobStore.GetInstance(mobInstanceID)` is non-nil, still alive, still in same room.
- - If object: check object still exists in room (via `g.World.FindObjInstances`).
- - If gone: `"Your target is gone."` → `g.CancelAction(p); return`
-3. Calculate success chance:
- ```go
- level := p.Level(player.Thieving)
- chance := action.SuccessChance(stealSuccess, level, stealLevel)
- if guardWatching {
- chance *= 0.5
- if chance < 0.05 {
- chance = 0.05
- }
- }
- ```
-4. Roll: `rand.Float64() < chance`
-5. **On success:**
- a. Load drop table: `g.BehaviorStore.LoadDropTable(stealTable)`
- b. Resolve drop: `g.BehaviorStore.ResolveDrop(dt.Drops)`
- c. If drop is nil or empty → `"You steal nothing of value."` (edge case)
- d. Give item to player using same pattern as `giveSearchLoot` (check free slot, handle credits specially, drop to ground if full).
- e. Award XP:
- ```go
- if stealXP > 0 {
- if newLevel := p.AddSkillXP(player.Thieving, stealXP); newLevel > 0 {
- sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d thieving! ***", newLevel)))
- }
- }
- ```
- f. XP drop message (if `xp_drops` option on):
- ```go
- if stealXP > 0 && p.OptionBool("xp_drops") {
- msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", stealXP, player.SkillAbbr[player.Thieving]))
- }
- ```
- g. Save character: `g.AccountStore.SaveCharacter(p)`
- h. Restart action for continuous stealing (like gather loops):
- ```go
- p.Action.WaitLeft = engine.ToTicks(stealSpeed)
- ```
- Check inventory space before restarting. If full → cancel action.
-6. **On failure (mob target):**
- a. `sess.WriteLine(fmt.Sprintf("The %s notices you! They attack!", targetName))`
- b. Initiate combat: `g.startCombat(sess, p, mob)` — reuse existing combat start.
- c. Cancel steal action: `g.CancelAction(p)`
-7. **On failure (object target):**
- a. If `guardMob != ""` and guard is alive in room and `guardWatching`:
- - `sess.WriteLine("You fumble and the Guard spots you!")`
- - Start the guard talk interaction:
- ```go
- guardMob := g.findGuardInRoom(p.RoomID, data["guard_mob"].(string))
- if guardMob != nil {
- g.CancelAction(p)
- g.startMobTalk(sess, p, guardMob)
- return
- }
- ```
- - This requires the guard mob to have `behavior: stall_guard_talk` set in its YAML def. BUT: the existing `guard` mob already has `behavior: guard_talk`. We need a **separate** behavior for the caught-stealing scenario. Options:
- - Create a new talk behavior `stall_guard_talk` and **temporarily** override the guard's behavior when the steal fails. Since `startMobTalk` uses `mob.BehaviorID`, we can set `guardMob.BehaviorID = "stall_guard_talk"` before calling it, then restore after. This is hacky.
- - Better: Use `g.startTalkFromBehavior(sess, p, "stall_guard_talk", guardMob.Name)` — create a small helper that starts a talk without requiring the mob's own behavior field. This is cleaner.
- - Cleanest approach: Add a helper `startStealGuardTalk(sess, p, guardMob)` that loads `stall_guard_talk` behavior directly and initiates the talk state, bypassing the mob's own behavior ID.
- b. If guard not watching or no guard:
- - `sess.WriteLine("You fail to steal anything.")`
- - Restart action for retry: `p.Action.WaitLeft = engine.ToTicks(stealSpeed)`
-
-### Helper: `findGuardInRoom(roomID int, guardDefID string) *world.MobInstance`
-
-```go
-func (g *Game) findGuardInRoom(roomID int, guardDefID string) *world.MobInstance {
- mobs := g.MobStore.MobsInRoom(roomID)
- for _, m := range mobs {
- if m.DefID == guardDefID && m.HP > 0 {
- return m
- }
- }
- return nil
-}
-```
-
-### Helper: `isGuardWatching(roomID int, objDefID string) bool`
-
-```go
-const guardWatchDuration = 8
-const guardLookAwayDuration = 4
-const guardCycleLength = guardWatchDuration + guardLookAwayDuration // 12
-
-func (g *Game) isGuardWatching(roomID int, objDefID string) bool {
- key := fmt.Sprintf("%d:%s", roomID, objDefID)
- counter := g.guardWatchTimers[key]
- return counter % guardCycleLength < guardWatchDuration
-}
-```
-
-### Helper: `startStealGuardTalk(sess *net.Session, p *player.Player, guardMob *world.MobInstance)`
-
-```go
-func (g *Game) startStealGuardTalk(sess *net.Session, p *player.Player, guardMob *world.MobInstance) {
- cfg, err := g.BehaviorStore.LoadTalk("stall_guard_talk")
- if err != nil {
- sess.WriteLine("The Guard glares at you but says nothing.")
- return
- }
-
- p.ActionState = &ActionState{Type: ActionTalking, TargetName: guardMob.Name}
-
- startNode := cfg.Nodes["start"]
- sess.WriteLine(fmt.Sprintf("\n%s says: \"%s\"", g.colorize(sess, "mob_name", guardMob.Name), startNode.Message))
-
- // Show options (reuse existing talk option display pattern)
- g.showTalkOptions(sess, p, cfg, "start")
-
- sess.State = net.StateTalk
- sess.TalkData = &net.TalkData{
- BehaviorID: "stall_guard_talk",
- NodeID: "start",
- TargetName: guardMob.Name,
- }
-}
-```
-
-Note: The above uses `sess.TalkData` — check how the existing talk system stores state. Look at `internal/net/server.go` for `TalkData`. The existing talk system stores talk state in the session. The `startStealGuardTalk` function must follow the exact same pattern as `startMobTalk` / `startTalk` in `action_talk.go` — read that file to match precisely. The key point is that the talk behavior `stall_guard_talk` is loaded by ID rather than from the mob's own `BehaviorID`.
-
----
-
-## 11. Sneak Mode
-
-### File: `internal/game/cmd_sneak.go`
-
-```go
-package game
-
-import (
- "fmt"
-
- "thehouseoficarus/internal/net"
- "thehouseoficarus/internal/player"
-)
-
-func (g *Game) doSneak(sess *net.Session) {
- p := sess.Player.(*player.Player)
- p.Sneaking = !p.Sneaking
- if p.Sneaking {
- p.SneakNotified = make(map[string]bool)
- sess.WriteLine("You begin sneaking.")
- } else {
- p.SneakNotified = nil
- sess.WriteLine("You stop sneaking.")
- }
-}
-
-func (g *Game) SneakTick() {
- if g.Hub == nil {
- return
- }
-
- // Advance all guard watch timers
- for key := range g.guardWatchTimers {
- g.guardWatchTimers[key]++
- }
-
- for _, sess := range g.Hub.AllSessions() {
- p, ok := sess.Player.(*player.Player)
- if !ok || p == nil || !p.Sneaking {
- continue
- }
-
- objs := g.World.AllObjInstances(p.RoomID)
- for _, st := range objs {
- objDef, err := g.ObjectStore.Load(st.DefID)
- if err != nil || objDef.GuardMob == "" {
- continue
- }
-
- guard := g.findGuardInRoom(p.RoomID, objDef.GuardMob)
- if guard == nil {
- continue
- }
-
- // Ensure timer exists
- timerKey := fmt.Sprintf("%d:%s", p.RoomID, st.DefID)
- if _, exists := g.guardWatchTimers[timerKey]; !exists {
- g.guardWatchTimers[timerKey] = 0
- }
-
- watching := g.isGuardWatching(p.RoomID, st.DefID)
- notifyKey := timerKey
-
- if p.SneakNotified == nil {
- p.SneakNotified = make(map[string]bool)
- }
-
- lastState, known := p.SneakNotified[notifyKey]
- if !known || lastState != watching {
- if watching {
- sess.WriteLine(g.colorize(sess, "warning", fmt.Sprintf("The %s is watching the %s.", guard.Name, objDef.Name)))
- } else {
- sess.WriteLine(g.colorize(sess, "success", fmt.Sprintf("The %s looks away from the %s.", guard.Name, objDef.Name)))
- }
- p.SneakNotified[notifyKey] = watching
- }
- }
- }
-}
-```
-
-### Player struct additions (in `internal/player/player.go`)
-
-After `Sneaking bool`:
-
-```go
-SneakNotified map[string]bool `yaml:"-"`
-```
-
----
-
-## 12. Guard Interaction
-
-### Talk Behavior: `data/behaviors/stall_guard_talk.yaml`
-
-```yaml
-id: stall_guard_talk
-type: talk
-nodes:
- start:
- message: "Caught you red-handed! You have three options, thief."
- options:
- - text: "I'll pay a fine. (500 credits)"
- goto: bribe
- condition:
- min_credits: 500
- - text: "Take me to jail."
- goto: jail
- - text: "You'll have to catch me first!"
- goto: fight
- - text: "I can't afford that..."
- goto: jail
- condition:
- min_credits: 500
- not: true
- bribe:
- message: "Smart choice. Hand over 500 credits and we'll forget this happened."
- action:
- cost: 500
- set_player_flags:
- bribed_guard: true
- options:
- - text: "Fine, take it."
- end: true
- jail:
- message: "Off to the detention cell with you!"
- action:
- teleport: 152
- set_player_flags:
- been_to_jail: true
- options:
- - text: "(You are dragged away)"
- end: true
- fight:
- message: "Then defend yourself!"
- action:
- set_flags:
- guard_hostile: true
- options:
- - text: "(The guard attacks!)"
- end: true
-```
-
-**Fight option handling:** When the `fight` node ends and `guard_hostile` flag is set, the guard should attack the player. This is handled in a post-talk hook. After the talk ends (when the player selects the end option for the `fight` node), check the world flag `guard_hostile`. If set:
-
-1. Clear the flag immediately: `delete(g.WorldFlags, "guard_hostile")`
-2. Find the guard mob in the room
-3. If guard exists and is not protected for combat purposes: temporarily set `guard.Protected = false`, start combat via `g.startCombat(sess, p, guardMob)`, then restore `guard.Protected = true` afterward (or just leave it false during this combat).
-
-**Implementation note:** The existing talk system processes `NodeAction` fields automatically via `executeTalkAction`. The `teleport` action already works. The `cost` action deducts credits. The only custom behavior needed is the "fight" trigger. Since the talk system already handles `set_flags`, we need a post-talk check in `handleTalkInput` (in `internal/game/action_talk.go`) or in the talk end handler:
-
-Add to the end of talk processing (where `end: true` is handled), after executing the node action:
-
-```go
-if g.WorldFlags["guard_hostile"] != nil {
- delete(g.WorldFlags, "guard_hostile")
- guardMob := g.findGuardInRoom(p.RoomID, "guard")
- if guardMob != nil {
- guardMob.Protected = false
- g.startCombat(sess, p, guardMob)
- }
-}
-```
-
-This check goes in the talk-end code path in `action_talk.go` (or `game.go` where `handleTalkInput` processes choices).
-
----
-
-## 13. XP Table
-
-| Target | Thieving Level Required | XP per Steal | Steal Speed (ticks) |
-|---|---|---|---|
-| Man | 1 | 8 | 4 |
-| Market Stall | 5 | 12 | 5 |
-| Farmer | 10 | 15 | 4 |
-| Bioengineer | 38 | 45 | 4 |
-
-These values are set in the mob/object YAML files via `steal_level`, `steal_xp`, and `steal_speed` fields.
-
-**XP progression reference (RSC table):**
-- Level 1: 0 XP
-- Level 10: 1,154 XP (~144 man steals)
-- Level 38: 31,191 XP (~668 farmer steals from level 10)
-- Level 50: 101,333 XP (~1,559 bioengineer steals from level 38)
-- Level 99: 13,034,431 XP
-
----
-
-## 14. Drop Tables
-
-### `data/drops/credit_stick_drop.yaml`
-
-```yaml
-id: credit_stick_drop
-drops:
- - item_id: credits
- weight: 40
- quantity: 15
- - item_id: credits
- weight: 30
- quantity: 30
- - item_id: credits
- weight: 20
- quantity: 50
- - item_id: credits
- weight: 8
- quantity: 100
- - item_id: credits
- weight: 2
- quantity: 250
-```
-
-### `data/drops/man_steal.yaml`
-
-```yaml
-id: man_steal
-drops:
- - item_id: credit_stick
- weight: 80
- quantity: 1
- - item_id: credits
- weight: 20
- quantity: 5
-```
-
-### `data/drops/farmer_steal.yaml`
-
-```yaml
-id: farmer_steal
-drops:
- - item_id: potato_seed
- weight: 30
- quantity: 1
- - item_id: onion_seed
- weight: 25
- quantity: 1
- - item_id: cabbage_seed
- weight: 20
- quantity: 1
- - item_id: tomato_seed
- weight: 15
- quantity: 1
- - item_id: sweetcorn_seed
- weight: 8
- quantity: 1
- - item_id: strawberry_seed
- weight: 2
- quantity: 1
-```
-
-### `data/drops/bioengineer_steal.yaml`
-
-```yaml
-id: bioengineer_steal
-drops:
- - item_id: sweetcorn_seed
- weight: 25
- quantity: 1
- - item_id: strawberry_seed
- weight: 20
- quantity: 1
- - item_id: watermelon_seed
- weight: 20
- quantity: 1
- - item_id: ranarr_seed
- weight: 15
- quantity: 1
- - item_id: snapdragon_seed
- weight: 12
- quantity: 1
- - item_id: torstol_seed
- weight: 8
- quantity: 1
-```
-
-### `data/drops/market_stall_steal.yaml`
-
-```yaml
-id: market_stall_steal
-drops:
- - item_id: bread
- weight: 40
- quantity: 1
- - item_id: apple
- weight: 35
- quantity: 1
- - item_id: cheese
- weight: 25
- quantity: 1
-```
-
----
-
-## 15. Help Files
-
-### `data/help/steal.yaml`
-
-```yaml
-name: "steal"
-category: "Skills"
-description: |
- Steal from mobs or objects.
-
- Usage: steal [target]
-
- Attempts to pickpocket a mob or shoplift from an object. Requires
- a minimum thieving level depending on the target.
-
- If there is only one stealable target in the room, you can type
- just "steal". If there are multiple different targets, you must
- specify: "steal man", "steal stall", "steal 2.man".
-
- On success, you receive a random item from the target's loot table
- and gain thieving XP. The action repeats automatically until you
- run out of inventory space or are interrupted.
-
- On failure against a mob, the mob turns hostile and attacks you.
-
- On failure against a guarded object (while the guard is watching),
- the guard confronts you with options to pay a bribe, go to jail,
- or fight.
-
- Use "sneak" to see when guards are watching or looking away.
-
- Aliases: thieve
-
- See also: help sneak, help thieving
-```
-
-### `data/help/sneak.yaml`
-
-```yaml
-name: "sneak"
-category: "Skills"
-description: |
- Toggle sneak mode on and off.
-
- Usage: sneak
-
- While sneaking, you receive messages telling you when guards are
- watching or looking away from objects they protect. Use this
- information to time your steals for when the guard is distracted.
-
- Stealing from a guarded object while the guard is looking away
- has no penalty on failure. Stealing while the guard is watching
- halves your success chance, and a failure causes the guard to
- confront you.
-
- Sneak mode is lost when you disconnect.
-
- See also: help steal, help thieving
-```
-
-### `data/help/thieving.yaml`
-
-```yaml
-name: "thieving"
-category: "Skills"
-description: |
- Thieving lets you steal from mobs and objects for loot and XP.
-
- Targets:
- Man - Level 1, 8 XP - Credit sticks
- Market Stall - Level 5, 12 XP - Food items (guarded)
- Farmer - Level 10, 15 XP - Low/mid seeds
- Bioengineer - Level 38, 45 XP - Mid/high seeds
-
- Success chance increases with your thieving level relative to
- the target's requirement. Failing against a mob starts combat.
- Failing against a guarded object while the guard watches triggers
- a confrontation (bribe, jail, or fight).
-
- Credit sticks obtained from stealing can be searched for credits.
-
- Commands: steal, sneak
-
- See also: help steal, help sneak
-```
-
----
-
-## Summary of All Changes
-
-### New Go files (2):
-1. `internal/game/action_steal.go` — `doSteal`, `resolveStealTarget`, `startSteal`, `advanceSteal`, `findGuardInRoom`, `isGuardWatching`, `startStealGuardTalk`, helpers
-2. `internal/game/cmd_sneak.go` — `doSneak`, `SneakTick`
-
-### Modified Go files (6):
-1. `internal/player/player.go` — Add `Sneaking bool` and `SneakNotified map[string]bool` to Player struct
-2. `internal/game/action_state.go` — Add `ActionStealing` constant and `Description()` case
-3. `internal/game/action.go` — Add `"steal"/"thieve"` to `verbAliases` and `verbSkill`, add `"steal"` case to `AdvanceActions()`
-4. `internal/game/game.go` — Add `"sneak"` to ClassInstant, `"steal"/"thieve"` to ClassActive, add cases to `executeCommand()`, add `ActionStealing` to persistent states in `ProcessQueuedCommands()`, add `guardWatchTimers` to Game struct
-5. `internal/world/mob.go` — Add `StealTable`, `StealLevel`, `StealXP`, `StealSpeed` to `MobDef` and `MobInstance`, copy in spawn function
-6. `internal/object/object.go` — Add `StealTable`, `StealLevel`, `StealXP`, `StealSpeed`, `GuardMob` to `ObjectDef`
-7. `cmd/mud/main.go` — Add `g.SneakTick()` to tick subscriber
-8. `internal/game/action_talk.go` — Add post-talk `guard_hostile` flag check for fight option
-
-### Modified YAML files (2):
-1. `data/mobs/man.yaml` — Add steal fields
-2. `data/rooms/100.yaml` — Add `south: 150` exit
-
-### New YAML files (24):
-- 13 items: `credit_stick`, `bread`, `apple`, `cheese`, `potato_seed`, `onion_seed`, `cabbage_seed`, `tomato_seed`, `sweetcorn_seed`, `strawberry_seed`, `watermelon_seed`, `ranarr_seed`, `snapdragon_seed`, `torstol_seed`
-- 2 mobs: `farmer`, `bioengineer`
-- 1 object: `market_stall`
-- 1 behavior: `stall_guard_talk`
-- 4 drop tables: `credit_stick_drop`, `man_steal`, `farmer_steal`, `bioengineer_steal`, `market_stall_steal`
-- 3 rooms: `150`, `151`, `152`
-- 3 help files: `steal`, `sneak`, `thieving`