aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-16 04:17:43 -0400
committerhistoria <[not public]>2026-06-16 04:17:43 -0400
commit085e728d22a3369bd110b77409914cfbe9ebe611 (patch)
treeeeb88cb1ceb91cc9ea2b5a1877c3c66328fab503
parent43f21aabae8924f35c12c8485e690aeefe0089fb (diff)
downloadthehouseoficarus-085e728d22a3369bd110b77409914cfbe9ebe611.tar.gz
feat: recipe system, combining items, cooking skill
-rw-r--r--AGENTS.md8
-rw-r--r--TODO.md2
-rw-r--r--cmd/mud/main.go1
-rw-r--r--data/help/color.yaml6
-rw-r--r--data/help/cook.yaml29
-rw-r--r--data/help/eat.yaml25
-rw-r--r--data/help/firemaking.yaml12
-rw-r--r--data/help/fishing.yaml6
-rw-r--r--data/help/gathering.yaml9
-rw-r--r--data/help/recipe.yaml31
-rw-r--r--data/help/use.yaml32
-rw-r--r--data/items/adamant_axe.yaml1
-rw-r--r--data/items/adamant_pickaxe.yaml1
-rw-r--r--data/items/adamantite_ore.yaml1
-rw-r--r--data/items/anchovies.yaml8
-rw-r--r--data/items/ashes.yaml1
-rw-r--r--data/items/big_fishing_net.yaml1
-rw-r--r--data/items/birds_nest.yaml1
-rw-r--r--data/items/black_axe.yaml1
-rw-r--r--data/items/black_pickaxe.yaml1
-rw-r--r--data/items/bones.yaml5
-rw-r--r--data/items/bread.yaml7
-rw-r--r--data/items/bread_dough.yaml11
-rw-r--r--data/items/bronze_axe.yaml1
-rw-r--r--data/items/bronze_pickaxe.yaml1
-rw-r--r--data/items/bronze_sword.yaml5
-rw-r--r--data/items/bucket_of_water.yaml6
-rw-r--r--data/items/burnt_fish.yaml6
-rw-r--r--data/items/burnt_meat.yaml6
-rw-r--r--data/items/christmas_cracker.yaml1
-rw-r--r--data/items/clay.yaml1
-rw-r--r--data/items/coal.yaml1
-rw-r--r--data/items/copper_ore.yaml1
-rw-r--r--data/items/credits.yaml5
-rw-r--r--data/items/dragon_axe.yaml1
-rw-r--r--data/items/dragon_pickaxe.yaml1
-rw-r--r--data/items/feather.yaml1
-rw-r--r--data/items/firesteel.yaml1
-rw-r--r--data/items/fishing_bait.yaml1
-rw-r--r--data/items/fishing_rod.yaml1
-rw-r--r--data/items/fly_fishing_rod.yaml1
-rw-r--r--data/items/gold_ore.yaml1
-rw-r--r--data/items/harpoon.yaml1
-rw-r--r--data/items/herring.yaml8
-rw-r--r--data/items/iron_axe.yaml1
-rw-r--r--data/items/iron_ore.yaml1
-rw-r--r--data/items/iron_pickaxe.yaml1
-rw-r--r--data/items/lighter.yaml1
-rw-r--r--data/items/lobster_pot.yaml1
-rw-r--r--data/items/logs.yaml1
-rw-r--r--data/items/magic_logs.yaml1
-rw-r--r--data/items/mahogany_logs.yaml1
-rw-r--r--data/items/maple_logs.yaml1
-rw-r--r--data/items/matches.yaml1
-rw-r--r--data/items/mithril_axe.yaml1
-rw-r--r--data/items/mithril_ore.yaml1
-rw-r--r--data/items/mithril_pickaxe.yaml1
-rw-r--r--data/items/oak_logs.yaml1
-rw-r--r--data/items/party_hat_blue.yaml1
-rw-r--r--data/items/party_hat_green.yaml1
-rw-r--r--data/items/party_hat_pink.yaml1
-rw-r--r--data/items/party_hat_red.yaml1
-rw-r--r--data/items/party_hat_white.yaml1
-rw-r--r--data/items/party_hat_yellow.yaml1
-rw-r--r--data/items/pastry_dough.yaml11
-rw-r--r--data/items/pizza_dough.yaml11
-rw-r--r--data/items/pot_of_flour.yaml6
-rw-r--r--data/items/raw_anchovies.yaml1
-rw-r--r--data/items/raw_anglerfish.yaml1
-rw-r--r--data/items/raw_bass.yaml1
-rw-r--r--data/items/raw_cave_eel.yaml1
-rw-r--r--data/items/raw_cod.yaml1
-rw-r--r--data/items/raw_herring.yaml1
-rw-r--r--data/items/raw_lobster.yaml1
-rw-r--r--data/items/raw_mackerel.yaml1
-rw-r--r--data/items/raw_pike.yaml1
-rw-r--r--data/items/raw_rainbow_fish.yaml1
-rw-r--r--data/items/raw_salmon.yaml1
-rw-r--r--data/items/raw_sardine.yaml1
-rw-r--r--data/items/raw_shark.yaml1
-rw-r--r--data/items/raw_shrimps.yaml1
-rw-r--r--data/items/raw_slimy_eel.yaml1
-rw-r--r--data/items/raw_swordfish.yaml1
-rw-r--r--data/items/raw_trout.yaml1
-rw-r--r--data/items/raw_tuna.yaml1
-rw-r--r--data/items/redwood_logs.yaml1
-rw-r--r--data/items/rune_axe.yaml1
-rw-r--r--data/items/rune_pickaxe.yaml1
-rw-r--r--data/items/runite_ore.yaml1
-rw-r--r--data/items/sacred_eel.yaml1
-rw-r--r--data/items/salmon.yaml8
-rw-r--r--data/items/sardine.yaml8
-rw-r--r--data/items/scrap_metal.yaml1
-rw-r--r--data/items/shrimps.yaml8
-rw-r--r--data/items/silver_ore.yaml1
-rw-r--r--data/items/small_fishing_net.yaml1
-rw-r--r--data/items/steel_axe.yaml1
-rw-r--r--data/items/steel_pickaxe.yaml1
-rw-r--r--data/items/teak_logs.yaml1
-rw-r--r--data/items/tin_ore.yaml1
-rw-r--r--data/items/trout.yaml8
-rw-r--r--data/items/uncut_diamond.yaml1
-rw-r--r--data/items/uncut_emerald.yaml1
-rw-r--r--data/items/uncut_ruby.yaml1
-rw-r--r--data/items/uncut_sapphire.yaml1
-rw-r--r--data/items/willow_logs.yaml1
-rw-r--r--data/items/yew_logs.yaml1
-rw-r--r--data/mobs/guard.yaml4
-rw-r--r--data/mobs/man.yaml4
-rw-r--r--data/mobs/newbie_trainer.yaml5
-rw-r--r--data/objects/cooking_range.yaml5
-rw-r--r--data/objects/lumby_fountain.yaml1
-rw-r--r--data/recipes/cook_anchovies.yaml14
-rw-r--r--data/recipes/cook_bread.yaml14
-rw-r--r--data/recipes/cook_herring.yaml14
-rw-r--r--data/recipes/cook_salmon.yaml14
-rw-r--r--data/recipes/cook_sardine.yaml14
-rw-r--r--data/recipes/cook_shrimps.yaml14
-rw-r--r--data/recipes/cook_trout.yaml14
-rw-r--r--data/rooms/11.yaml11
-rw-r--r--internal/action/recipe.go213
-rw-r--r--internal/game/action.go2
-rw-r--r--internal/game/action_cook.go153
-rw-r--r--internal/game/action_state.go6
-rw-r--r--internal/game/cmd_attack.go5
-rw-r--r--internal/game/cmd_cook.go246
-rw-r--r--internal/game/cmd_eat.go132
-rw-r--r--internal/game/cmd_use.go200
-rw-r--r--internal/game/game.go26
-rw-r--r--internal/game/help.go2
-rw-r--r--internal/game/tick.go15
-rw-r--r--internal/net/server.go19
-rw-r--r--internal/object/item.go24
-rw-r--r--internal/object/item_store.go22
-rw-r--r--internal/player/player.go1
-rw-r--r--worldbuilding_guide/README.md25
-rw-r--r--worldbuilding_guide/behaviors.md264
-rw-r--r--worldbuilding_guide/conditions.md66
-rw-r--r--worldbuilding_guide/doors.md102
-rw-r--r--worldbuilding_guide/drops.md52
-rw-r--r--worldbuilding_guide/hidden_objects.md21
-rw-r--r--worldbuilding_guide/index.md10
-rw-r--r--worldbuilding_guide/items.md68
-rw-r--r--worldbuilding_guide/mobs.md54
-rw-r--r--worldbuilding_guide/objects.md37
-rw-r--r--worldbuilding_guide/quests.md107
-rw-r--r--worldbuilding_guide/recipes.md112
-rw-r--r--worldbuilding_guide/rooms.md122
-rw-r--r--worldbuilding_guide/search.md15
-rw-r--r--worldbuilding_guide/state.md8
-rw-r--r--worldbuilding_guide/tips.md21
-rw-r--r--worldbuilding_guide/toggles.md18
-rw-r--r--worldbuilding_guide/wandering_mobs.md20
-rw-r--r--worldbuilding_guide/wandering_objects.md23
154 files changed, 2551 insertions, 155 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 8c1feea..6dc559b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -104,6 +104,7 @@ data/objects/<id>.yaml Object definitions (name, behavior, hidden, inroom_des
data/behaviors/<id>.yaml Behaviors (gather, talk, use, toggle)
data/drops/<id>.yaml Shared drop tables (weighted item lists)
data/help/<id>.yaml Help topics
+data/recipes/<id>.yaml Recipe definitions (cooking, crafting, smithing, etc.)
data/players/accounts/ Account YAML (gitignored)
data/players/characters/ Character YAML (gitignored)
```
@@ -131,6 +132,9 @@ See `WORLDBUILDING.md` for full YAML format reference with examples.
| `unalias <name>` | Remove an alias |
| `description` / `desc` | Set custom player description |
| `queued` | Show pending tick actions |
+| `color` / `colors [target] [value]` | Customize display colors (account-wide) |
+| `color reset <target>` | Reset a color target to server default |
+| `color <target> off` | Disable color for a specific target |
### Free (stackable, execute before active)
| Command | Description |
@@ -139,6 +143,7 @@ See `WORLDBUILDING.md` for full YAML format reference with examples.
| `wear all` | Auto-equips highest-value item per slot from inventory |
| `remove` / `unwear` / `unwield <item>` | Unequip item back to inventory |
| `style <name>` | Set combat style: accurate, aggressive, defensive, balanced (prefix match) |
+| `eat <item>` | Eat food to heal (3-tick cooldown, can eat during combat) |
### Active (replaces previous active, queued per tick)
| Command | Description |
@@ -150,7 +155,8 @@ See `WORLDBUILDING.md` for full YAML format reference with examples.
| `drop all` / `drop all <name>` | Drop all (or matching) items |
| `attack` / `kill <mob>` | Combat with numbered targeting and smart default |
| `mine` / `chop` / `cut` / `fish <object>` | Gathering with smart default (verbAliases map to "gather") |
-| `use <object>` | Crafting station interaction |
+| `use <item> [on/with] <target>` | Universal item interaction: recipes, crafting, gathering shortcuts |
+| `cook <item>` | Cook raw food on a fire or cooking range (Cooking skill) |
| `talk` / `speak` / `ask <target>` | NPC conversations (objects or mobs with behavior) |
| `pull` / `push <object>` | Toggle interactions (levers, gates) |
| `search <item>` | Search bird's nests / searchable items (rolls on search_table) |
diff --git a/TODO.md b/TODO.md
index e4ef170..6ae968f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -4,7 +4,7 @@ Upcoming work for Third Collapse. See `AGENTS.md` for architecture, commands, an
## Phase 1: Core Skill Chains (breadth over depth)
-- [ ] Cooking — fire/range object (`use` type), raw fish/meat → cooked food
+- [x] Cooking — fire/range object, raw fish → cooked food, bread dough → bread, eat command
- [ ] Smithing — furnace + anvil objects (`use` type), ore → bars → weapons/armor
- [ ] Fletching — fletching table object (`use` type), logs → arrow shafts/bows
- [ ] Equipment: bronze/iron weapons, axes, cooked trout, arrows, bows
diff --git a/cmd/mud/main.go b/cmd/mud/main.go
index 2d19608..67ba93d 100644
--- a/cmd/mud/main.go
+++ b/cmd/mud/main.go
@@ -50,6 +50,7 @@ func main() {
g.SharedDepletionTick()
g.FireTick()
g.AdvanceActions()
+ g.ConsumeTick()
g.BroadcastRespawns()
return true
})
diff --git a/data/help/color.yaml b/data/help/color.yaml
index 1d5ba54..777aa07 100644
--- a/data/help/color.yaml
+++ b/data/help/color.yaml
@@ -57,10 +57,8 @@ description: |
3. Built-in fallback
Auto-colored elements (not configurable):
- - Object names: colors are set per-object in their YAML definition
- (fishing spots are blue, rocks are earth-toned, trees are green, etc.)
- - Item names: colors are set per-item in their YAML definition
- (ores, logs, fish, tools, etc.)
+ - Objects: fishing spots are blue, rocks are earth-toned, trees green
+ - Items: ores, logs, fish, gems, and tools have their own colors
- Mob/player levels: displayed relative to your combat level
(red if higher, orange if <5 higher, white if equal,
yellow if <5 lower, green if much lower)
diff --git a/data/help/cook.yaml b/data/help/cook.yaml
new file mode 100644
index 0000000..20ee72f
--- /dev/null
+++ b/data/help/cook.yaml
@@ -0,0 +1,29 @@
+name: "cook"
+category: "Skills"
+description: |
+ Cook raw food on a fire or cooking range.
+
+ Usage: cook <item> Cook a specific item
+ cook Show all items you can cook right now
+
+ Cooking requires a heat source — either a lit fire or a cooking
+ range. Some recipes (like baking bread) require a cooking range
+ and cannot be done on an open fire. The fire does not need to
+ stay lit throughout — cooking continues regardless.
+
+ Each item takes 6 ticks to cook. A Cooking skill check determines
+ whether you produce a cooked item or a burnt one. On failure, you
+ get a burnt variant instead of the cooked food.
+
+ Typing "cook" with no arguments shows a menu of everything you
+ can cook with your current inventory and the available station.
+ If only one thing can be cooked, it starts immediately.
+
+ If multiple recipes exist for the same ingredient, a menu appears
+ letting you choose which to make.
+
+ You can also cook by using items on a cooking station:
+ use trout on fire
+ use raw shrimps on range
+
+ See also: eat, use, recipe
diff --git a/data/help/eat.yaml b/data/help/eat.yaml
new file mode 100644
index 0000000..1facf7d
--- /dev/null
+++ b/data/help/eat.yaml
@@ -0,0 +1,25 @@
+name: "eat"
+category: "Skills"
+description: |
+ Eat food to restore (or lose) hitpoints.
+
+ Usage: eat <item>
+
+ Each food item heals a specific amount of HP (shown when you
+ examine it with "look <item>"). Some items may damage you
+ instead — read the description carefully.
+
+ You cannot exceed your maximum HP when eating.
+ If you reach 0 HP, you will die as normal.
+
+ Eating has a 3-tick cooldown between uses. During this time you
+ cannot eat again — you'll see "You're still digesting your last meal."
+
+ Eating during combat replaces your next attack — instead of hitting
+ the mob, you eat. You remain in combat after eating and resume
+ attacking on the following tick.
+
+ Eating outside combat cancels any current action (except combat)
+ and takes effect on the next tick.
+
+ See also: cook, recipe
diff --git a/data/help/firemaking.yaml b/data/help/firemaking.yaml
index 081e10e..61ec072 100644
--- a/data/help/firemaking.yaml
+++ b/data/help/firemaking.yaml
@@ -15,9 +15,8 @@ description: |
----------------------
The burn command checks for burnable logs on the ground first, then
in your inventory. If the logs are in your inventory, you drop one
- to the ground and begin making a fire. After a wait (reduced by
- tool_speed), the game checks your firemaking level against the
- log's difficulty:
+ to the ground and begin making a fire. After a wait (reduced by your fire starter's speed), the game
+ checks your firemaking level against the log's difficulty:
Success → You start a fire! The fire object appears in the room
with a burn time based on the log used. You gain XP.
@@ -31,10 +30,9 @@ description: |
STOKE — Adding Fuel
-------------------
Stoke adds more logs from your inventory to an existing fire. Each
- log extends the fire's burn time by half its burn_ticks and awards
- firemaking XP. No skill check is needed to stoke — the XP is
- guaranteed. Stoking continues automatically until you're out of
- the item or the fire dies out.
+ log extends the fire's burn time and awards firemaking XP. No skill
+ check is needed to stoke — the XP is guaranteed. Stoking continues
+ automatically until you're out of the item or the fire dies out.
SMART DEFAULTS
--------------
diff --git a/data/help/fishing.yaml b/data/help/fishing.yaml
index cf2ea95..61d7309 100644
--- a/data/help/fishing.yaml
+++ b/data/help/fishing.yaml
@@ -10,9 +10,9 @@ description: |
Some spots also require bait (fishing bait or feathers), which is
consumed each cast whether you catch anything or not.
- If you run out of bait, fishing stops. Better tools have higher
- tool_speed, reducing wait time. A wielded tool takes priority over
- inventory tools.
+ If you run out of bait, fishing stops. Better tools have a higher
+ speed rating, reducing wait time per cast. A wielded tool takes
+ priority over inventory tools.
Each spot type can yield different fish depending on your fishing
level. Higher level unlocks more valuable catches.
diff --git a/data/help/gathering.yaml b/data/help/gathering.yaml
index 876f92f..960519d 100644
--- a/data/help/gathering.yaml
+++ b/data/help/gathering.yaml
@@ -10,12 +10,13 @@ description: |
Gathering skills use a tick-based action system:
1. You begin the action (e.g. "You swing your pickaxe...")
- 2. After a wait (reduced by tool speed), the game rolls for success
+ 2. After a wait (reduced by your tool's speed), the game rolls for success
3. Success picks a weighted random drop from the object's drop table
- Tools: Items with tool_type and tool_speed. A wielded tool always takes
- priority. Otherwise the first matching tool in your inventory is used.
- Higher tool_speed means faster gathering.
+ Tools: Items with a tool type and speed (e.g., pickaxes, axes, nets).
+ A wielded tool always takes priority. Otherwise the first matching
+ tool in your inventory is used. Better tools have higher speed,
+ meaning faster gathering.
Bait: Some fishing spots consume bait each attempt. If you run out,
the action stops.
diff --git a/data/help/recipe.yaml b/data/help/recipe.yaml
new file mode 100644
index 0000000..9df0a5b
--- /dev/null
+++ b/data/help/recipe.yaml
@@ -0,0 +1,31 @@
+name: "recipe"
+category: "Skills"
+description: |
+ The recipe system lets you combine items and use cooking stations
+ to create new items. Recipes are the core of cooking, crafting,
+ smithing, and other production skills.
+
+ There are two ways to make things:
+
+ Station recipes — use a cooking station (fire or range):
+ cook <item> Cook on a fire or range
+ use <item> on <object> Use an item on a cooking station
+
+ Item combinations — combine items directly from inventory:
+ use <item> with <item> Combine two items (e.g., flour + water)
+
+ When a recipe has a skill requirement, the game rolls your skill
+ level against the recipe's difficulty. Success gives XP and the
+ finished product. Failure may produce a burnt or ruined item.
+
+ Typing "cook" with no arguments shows a menu of everything you
+ can cook on the available station with your current inventory.
+ If only one thing can be cooked, it starts immediately.
+
+ If multiple recipes match your ingredients, a numbered menu
+ appears for you to choose.
+
+ The menu shows item names in their display color — ores, fish,
+ and cooked food each have distinct colors for easy scanning.
+
+ See also: cook, eat, use
diff --git a/data/help/use.yaml b/data/help/use.yaml
index 41ceb34..a6efafb 100644
--- a/data/help/use.yaml
+++ b/data/help/use.yaml
@@ -1,13 +1,29 @@
name: "use"
-category: "Skills"
+category: "General"
description: |
- Use a crafting station or interactive object.
+ Use an item on a target — the universal interaction command.
- Usage: use <object>
+ Usage:
+ use <item> Use an item on a room object
+ use <item> on <target> Use item on a specific target
+ use <item> with <item> Combine two inventory items (shorthand)
- Some objects can be used for crafting (furnaces, anvils, spinning wheels,
- ranges, etc.). Using an object typically consumes items from your
- inventory and produces something new, with an optional skill check.
+ The "use" command routes to different systems based on the target:
- You need the required materials in your inventory. The action continues
- until you run out of materials or inventory space.
+ Object target (room object)
+ If a recipe matches: opens the recipe menu for crafting/cooking
+ Otherwise: dispatches to the object's behavior (gather, talk, toggle)
+
+ Item target (inventory or ground)
+ If a recipe matches both items: combines them (e.g., flour + water)
+
+ Mob target
+ Future: give items to NPCs for quests
+
+ Examples:
+ use trout on fire Cook trout on a campfire
+ use water on flour Mix dough (opens recipe menu)
+ use bronze pickaxe on rock Mine copper/tin rocks
+ use iron gate Push/pull a gate (falls through to toggle)
+
+ See also: cook, recipe, gather, talk
diff --git a/data/items/adamant_axe.yaml b/data/items/adamant_axe.yaml
index bd52982..6c36a2c 100644
--- a/data/items/adamant_axe.yaml
+++ b/data/items/adamant_axe.yaml
@@ -1,7 +1,6 @@
id: adamant_axe
name: adamant axe
color: "fg=green"
-aliases: ["axe", "hatchet"]
description: "A powerful adamant axe."
value: 1250
stackable: false
diff --git a/data/items/adamant_pickaxe.yaml b/data/items/adamant_pickaxe.yaml
index 4aa0bb0..ead660c 100644
--- a/data/items/adamant_pickaxe.yaml
+++ b/data/items/adamant_pickaxe.yaml
@@ -1,7 +1,6 @@
id: adamant_pickaxe
name: adamant pickaxe
color: "fg=green"
-aliases: ["pick", "pickaxe"]
description: "A powerful adamant pickaxe."
value: 1250
stackable: false
diff --git a/data/items/adamantite_ore.yaml b/data/items/adamantite_ore.yaml
index 578ece5..2afde1c 100644
--- a/data/items/adamantite_ore.yaml
+++ b/data/items/adamantite_ore.yaml
@@ -1,7 +1,6 @@
id: adamantite_ore
name: adamantite ore
color: "fg=green"
-aliases: ["adamantite", "adamant", "ore"]
description: "A chunk of dark green adamantite ore."
value: 100
stackable: false
diff --git a/data/items/anchovies.yaml b/data/items/anchovies.yaml
new file mode 100644
index 0000000..54b1a5a
--- /dev/null
+++ b/data/items/anchovies.yaml
@@ -0,0 +1,8 @@
+id: anchovies
+name: anchovies
+color: "fg=bright_red"
+description: "Some nicely cooked anchovies."
+value: 5
+stackable: false
+heal_value: 3
+eat_message: "You eat the anchovies."
diff --git a/data/items/ashes.yaml b/data/items/ashes.yaml
index 40998d7..6835057 100644
--- a/data/items/ashes.yaml
+++ b/data/items/ashes.yaml
@@ -1,7 +1,6 @@
id: ashes
name: ashes
color: "dim"
-aliases: ["ash"]
description: "A pile of cold ashes. What was once ablaze is now mere dust."
value: 1
stackable: false
diff --git a/data/items/big_fishing_net.yaml b/data/items/big_fishing_net.yaml
index 79eff43..4b2b69e 100644
--- a/data/items/big_fishing_net.yaml
+++ b/data/items/big_fishing_net.yaml
@@ -1,7 +1,6 @@
id: big_fishing_net
name: big fishing net
color: "fg=white"
-aliases: ["net", "big net"]
description: "A larger fishing net for catching bigger fish."
value: 20
stackable: false
diff --git a/data/items/birds_nest.yaml b/data/items/birds_nest.yaml
index 8a9e907..18fbab5 100644
--- a/data/items/birds_nest.yaml
+++ b/data/items/birds_nest.yaml
@@ -1,7 +1,6 @@
id: birds_nest
name: bird's nest
color: "fg=bright_yellow"
-aliases: ["nest", "birds", "bird"]
description: "A bird's nest. You can search it for treasure."
value: 5
stackable: false
diff --git a/data/items/black_axe.yaml b/data/items/black_axe.yaml
index b47fbed..b0ee271 100644
--- a/data/items/black_axe.yaml
+++ b/data/items/black_axe.yaml
@@ -1,7 +1,6 @@
id: black_axe
name: black axe
color: "dim"
-aliases: ["axe", "hatchet"]
description: "A sleek black axe, as fast as steel."
value: 500
stackable: false
diff --git a/data/items/black_pickaxe.yaml b/data/items/black_pickaxe.yaml
index 1de8fb6..2896612 100644
--- a/data/items/black_pickaxe.yaml
+++ b/data/items/black_pickaxe.yaml
@@ -1,7 +1,6 @@
id: black_pickaxe
name: black pickaxe
color: "dim"
-aliases: ["pick", "pickaxe"]
description: "A sleek black pickaxe."
value: 500
stackable: false
diff --git a/data/items/bones.yaml b/data/items/bones.yaml
index 6e9e801..6883a16 100644
--- a/data/items/bones.yaml
+++ b/data/items/bones.yaml
@@ -1,7 +1,6 @@
-id: "bones"
-name: "bones"
+id: bones
+name: bones
color: "fg=white"
-aliases: ["bone"]
description: "A pile of bones."
value: 2
stackable: false
diff --git a/data/items/bread.yaml b/data/items/bread.yaml
index 87c9067..2e0a1ff 100644
--- a/data/items/bread.yaml
+++ b/data/items/bread.yaml
@@ -1,7 +1,8 @@
id: bread
name: bread
color: "fg=bright_yellow"
-aliases: ["loaf", "bread"]
-description: "A fresh loaf of bread. Smells wonderful."
-value: 2
+description: "A fresh loaf of bread, still warm from the oven."
+value: 5
stackable: false
+heal_value: 5
+eat_message: "You eat the bread. Warm and satisfying."
diff --git a/data/items/bread_dough.yaml b/data/items/bread_dough.yaml
new file mode 100644
index 0000000..d40391c
--- /dev/null
+++ b/data/items/bread_dough.yaml
@@ -0,0 +1,11 @@
+id: bread_dough
+name: bread dough
+color: "fg=bright_yellow"
+description: "A ball of uncooked bread dough. It needs to be baked."
+value: 3
+stackable: false
+made_from:
+ - items: [pot_of_flour]
+ qty: 1
+ - items: [bucket_of_water]
+ qty: 1
diff --git a/data/items/bronze_axe.yaml b/data/items/bronze_axe.yaml
index 9a024c8..5db0929 100644
--- a/data/items/bronze_axe.yaml
+++ b/data/items/bronze_axe.yaml
@@ -1,7 +1,6 @@
id: bronze_axe
name: bronze axe
color: "fg=bright_yellow"
-aliases: ["axe", "hatchet"]
description: "A sturdy bronze axe, good for woodcutting."
value: 10
stackable: false
diff --git a/data/items/bronze_pickaxe.yaml b/data/items/bronze_pickaxe.yaml
index 3d87f77..d81d2bb 100644
--- a/data/items/bronze_pickaxe.yaml
+++ b/data/items/bronze_pickaxe.yaml
@@ -1,7 +1,6 @@
id: bronze_pickaxe
name: bronze pickaxe
color: "fg=bright_yellow"
-aliases: ["pick", "pickaxe"]
description: "A sturdy bronze pickaxe, good for mining."
value: 10
stackable: false
diff --git a/data/items/bronze_sword.yaml b/data/items/bronze_sword.yaml
index e001d26..ca47743 100644
--- a/data/items/bronze_sword.yaml
+++ b/data/items/bronze_sword.yaml
@@ -1,7 +1,6 @@
-id: "bronze_sword"
-name: "bronze sword"
+id: bronze_sword
+name: bronze sword
color: "fg=bright_yellow"
-aliases: ["sword", "bronze"]
description: "A basic bronze short sword."
value: 12
stackable: false
diff --git a/data/items/bucket_of_water.yaml b/data/items/bucket_of_water.yaml
new file mode 100644
index 0000000..e3d0af8
--- /dev/null
+++ b/data/items/bucket_of_water.yaml
@@ -0,0 +1,6 @@
+id: bucket_of_water
+name: bucket of water
+color: "fg=cyan"
+description: "A wooden bucket filled with fresh water."
+value: 5
+stackable: false
diff --git a/data/items/burnt_fish.yaml b/data/items/burnt_fish.yaml
new file mode 100644
index 0000000..e8a41b4
--- /dev/null
+++ b/data/items/burnt_fish.yaml
@@ -0,0 +1,6 @@
+id: burnt_fish
+name: burnt fish
+color: "dim"
+description: "A badly burnt fish. Completely inedible."
+value: 0
+stackable: true
diff --git a/data/items/burnt_meat.yaml b/data/items/burnt_meat.yaml
new file mode 100644
index 0000000..8d960c3
--- /dev/null
+++ b/data/items/burnt_meat.yaml
@@ -0,0 +1,6 @@
+id: burnt_meat
+name: burnt meat
+color: "dim"
+description: "A piece of badly burnt meat. Completely inedible."
+value: 0
+stackable: true
diff --git a/data/items/christmas_cracker.yaml b/data/items/christmas_cracker.yaml
index 352730c..16cd9b3 100644
--- a/data/items/christmas_cracker.yaml
+++ b/data/items/christmas_cracker.yaml
@@ -1,7 +1,6 @@
id: christmas_cracker
name: christmas cracker
color: "fg=bright_red"
-aliases: ["cracker", "christmas"]
description: "A festive party cracker. Pull it apart to see what's inside!"
value: 10
stackable: true
diff --git a/data/items/clay.yaml b/data/items/clay.yaml
index 1b67233..f8575a3 100644
--- a/data/items/clay.yaml
+++ b/data/items/clay.yaml
@@ -1,7 +1,6 @@
id: clay
name: clay
color: "fg=bright_red"
-aliases: ["clay"]
description: "Some soft clay, useful for crafting and pottery."
value: 1
stackable: false
diff --git a/data/items/coal.yaml b/data/items/coal.yaml
index 97befb5..23a42a2 100644
--- a/data/items/coal.yaml
+++ b/data/items/coal.yaml
@@ -1,7 +1,6 @@
id: coal
name: coal
color: "dim"
-aliases: ["coal"]
description: "A lump of coal, useful for smelting."
value: 5
stackable: false
diff --git a/data/items/copper_ore.yaml b/data/items/copper_ore.yaml
index 30e4053..8e53691 100644
--- a/data/items/copper_ore.yaml
+++ b/data/items/copper_ore.yaml
@@ -1,7 +1,6 @@
id: copper_ore
name: copper ore
color: "fg=bright_yellow"
-aliases: ["ore", "copper"]
description: "A chunk of copper-bearing rock. Can be smelted into a bronze bar."
value: 1
stackable: false
diff --git a/data/items/credits.yaml b/data/items/credits.yaml
index b434edf..4d2c9a8 100644
--- a/data/items/credits.yaml
+++ b/data/items/credits.yaml
@@ -1,7 +1,6 @@
-id: "credits"
-name: "credits"
+id: credits
+name: credits
color: "fg=bright_yellow"
-aliases: ["credit", "creds"]
description: "A handful of universal credits."
value: 1
stackable: true
diff --git a/data/items/dragon_axe.yaml b/data/items/dragon_axe.yaml
index 57fd85d..53c1f82 100644
--- a/data/items/dragon_axe.yaml
+++ b/data/items/dragon_axe.yaml
@@ -1,7 +1,6 @@
id: dragon_axe
name: dragon axe
color: "fg=bright_red"
-aliases: ["axe", "hatchet"]
description: "A legendary dragon axe that bites through wood like butter."
value: 100000
stackable: false
diff --git a/data/items/dragon_pickaxe.yaml b/data/items/dragon_pickaxe.yaml
index 581b318..623a390 100644
--- a/data/items/dragon_pickaxe.yaml
+++ b/data/items/dragon_pickaxe.yaml
@@ -1,7 +1,6 @@
id: dragon_pickaxe
name: dragon pickaxe
color: "fg=bright_red"
-aliases: ["pick", "pickaxe"]
description: "A legendary dragon pickaxe that pulverizes rock."
value: 100000
stackable: false
diff --git a/data/items/feather.yaml b/data/items/feather.yaml
index b8a89fe..2d2e7db 100644
--- a/data/items/feather.yaml
+++ b/data/items/feather.yaml
@@ -1,7 +1,6 @@
id: feather
name: feather
color: "fg=white"
-aliases: ["feathers"]
description: "A colorful feather. Used as fly fishing bait."
value: 1
stackable: true
diff --git a/data/items/firesteel.yaml b/data/items/firesteel.yaml
index 4323daf..8d65d26 100644
--- a/data/items/firesteel.yaml
+++ b/data/items/firesteel.yaml
@@ -1,7 +1,6 @@
id: firesteel
name: firesteel
color: "dim"
-aliases: ["steel", "flint"]
description: "A rugged firesteel that never wears out."
value: 20
stackable: false
diff --git a/data/items/fishing_bait.yaml b/data/items/fishing_bait.yaml
index 81f2f2b..7690a1c 100644
--- a/data/items/fishing_bait.yaml
+++ b/data/items/fishing_bait.yaml
@@ -1,7 +1,6 @@
id: fishing_bait
name: fishing bait
color: "fg=yellow"
-aliases: ["bait"]
description: "A handful of wriggling bait. Used with a fishing rod."
value: 1
stackable: true
diff --git a/data/items/fishing_rod.yaml b/data/items/fishing_rod.yaml
index 85a3622..77b207c 100644
--- a/data/items/fishing_rod.yaml
+++ b/data/items/fishing_rod.yaml
@@ -1,7 +1,6 @@
id: fishing_rod
name: fishing rod
color: "fg=green"
-aliases: ["rod", "fishing"]
description: "A simple fishing rod. Use with fishing bait."
value: 5
stackable: false
diff --git a/data/items/fly_fishing_rod.yaml b/data/items/fly_fishing_rod.yaml
index 04745fd..1531ea1 100644
--- a/data/items/fly_fishing_rod.yaml
+++ b/data/items/fly_fishing_rod.yaml
@@ -1,7 +1,6 @@
id: fly_fishing_rod
name: fly fishing rod
color: "fg=green"
-aliases: ["rod", "fly rod", "fly fishing"]
description: "A flexible rod for fly fishing with artificial flies."
value: 20
stackable: false
diff --git a/data/items/gold_ore.yaml b/data/items/gold_ore.yaml
index 91c32dc..1cbf4b2 100644
--- a/data/items/gold_ore.yaml
+++ b/data/items/gold_ore.yaml
@@ -1,7 +1,6 @@
id: gold_ore
name: gold ore
color: "fg=bright_yellow"
-aliases: ["gold", "ore"]
description: "A glittering chunk of gold ore."
value: 25
stackable: false
diff --git a/data/items/harpoon.yaml b/data/items/harpoon.yaml
index c7cfbc2..e318469 100644
--- a/data/items/harpoon.yaml
+++ b/data/items/harpoon.yaml
@@ -1,7 +1,6 @@
id: harpoon
name: harpoon
color: "dim"
-aliases: ["harpoon", "spear"]
description: "A barbed harpoon for spearing large fish."
value: 30
stackable: false
diff --git a/data/items/herring.yaml b/data/items/herring.yaml
new file mode 100644
index 0000000..5a72528
--- /dev/null
+++ b/data/items/herring.yaml
@@ -0,0 +1,8 @@
+id: herring
+name: herring
+color: "fg=bright_red"
+description: "A nicely cooked herring."
+value: 7
+stackable: false
+heal_value: 5
+eat_message: "You eat the herring."
diff --git a/data/items/iron_axe.yaml b/data/items/iron_axe.yaml
index 6e05bd6..17cf6a2 100644
--- a/data/items/iron_axe.yaml
+++ b/data/items/iron_axe.yaml
@@ -1,7 +1,6 @@
id: iron_axe
name: iron axe
color: "dim"
-aliases: ["axe", "hatchet"]
description: "A solid iron axe, better than bronze."
value: 50
stackable: false
diff --git a/data/items/iron_ore.yaml b/data/items/iron_ore.yaml
index f3e4183..90cddc7 100644
--- a/data/items/iron_ore.yaml
+++ b/data/items/iron_ore.yaml
@@ -1,7 +1,6 @@
id: iron_ore
name: iron ore
color: "dim"
-aliases: ["iron", "ore"]
description: "A chunk of iron ore, ready for smelting."
value: 5
stackable: false
diff --git a/data/items/iron_pickaxe.yaml b/data/items/iron_pickaxe.yaml
index 7deeafe..4eb9a0e 100644
--- a/data/items/iron_pickaxe.yaml
+++ b/data/items/iron_pickaxe.yaml
@@ -1,7 +1,6 @@
id: iron_pickaxe
name: iron pickaxe
color: "dim"
-aliases: ["pick", "pickaxe"]
description: "A solid iron pickaxe, better than bronze."
value: 50
stackable: false
diff --git a/data/items/lighter.yaml b/data/items/lighter.yaml
index e672993..2f3b321 100644
--- a/data/items/lighter.yaml
+++ b/data/items/lighter.yaml
@@ -1,7 +1,6 @@
id: lighter
name: lighter
color: "fg=cyan"
-aliases: ["light"]
description: "A reliable butane lighter."
value: 15
stackable: false
diff --git a/data/items/lobster_pot.yaml b/data/items/lobster_pot.yaml
index caa96ea..a91e2b5 100644
--- a/data/items/lobster_pot.yaml
+++ b/data/items/lobster_pot.yaml
@@ -1,7 +1,6 @@
id: lobster_pot
name: lobster pot
color: "dim"
-aliases: ["pot", "lobster", "cage"]
description: "A wire cage for trapping lobsters."
value: 20
stackable: false
diff --git a/data/items/logs.yaml b/data/items/logs.yaml
index a21dd00..55d89e9 100644
--- a/data/items/logs.yaml
+++ b/data/items/logs.yaml
@@ -1,7 +1,6 @@
id: logs
name: logs
color: "fg=green"
-aliases: ["log", "wood"]
description: "Some regular logs, good for firemaking and fletching."
value: 2
stackable: false
diff --git a/data/items/magic_logs.yaml b/data/items/magic_logs.yaml
index c17e234..5c1536b 100644
--- a/data/items/magic_logs.yaml
+++ b/data/items/magic_logs.yaml
@@ -1,7 +1,6 @@
id: magic_logs
name: magic logs
color: "fg=magenta"
-aliases: ["logs", "magic", "log"]
description: "Some mystical magic logs."
value: 80
stackable: false
diff --git a/data/items/mahogany_logs.yaml b/data/items/mahogany_logs.yaml
index 2c68ebc..9b8d5a3 100644
--- a/data/items/mahogany_logs.yaml
+++ b/data/items/mahogany_logs.yaml
@@ -1,7 +1,6 @@
id: mahogany_logs
name: mahogany logs
color: "fg=bright_red"
-aliases: ["logs", "mahogany", "log"]
description: "Some rich mahogany logs."
value: 20
stackable: false
diff --git a/data/items/maple_logs.yaml b/data/items/maple_logs.yaml
index ac5c2c9..3bc4947 100644
--- a/data/items/maple_logs.yaml
+++ b/data/items/maple_logs.yaml
@@ -1,7 +1,6 @@
id: maple_logs
name: maple logs
color: "fg=bright_red"
-aliases: ["logs", "maple", "log"]
description: "Some smooth maple logs."
value: 16
stackable: false
diff --git a/data/items/matches.yaml b/data/items/matches.yaml
index e3b9284..495ea19 100644
--- a/data/items/matches.yaml
+++ b/data/items/matches.yaml
@@ -1,7 +1,6 @@
id: matches
name: matches
color: "fg=bright_red"
-aliases: ["match"]
description: "A small box of matches for starting fires."
value: 5
stackable: true
diff --git a/data/items/mithril_axe.yaml b/data/items/mithril_axe.yaml
index 2c3bfd4..4977957 100644
--- a/data/items/mithril_axe.yaml
+++ b/data/items/mithril_axe.yaml
@@ -1,7 +1,6 @@
id: mithril_axe
name: mithril axe
color: "fg=blue"
-aliases: ["axe", "hatchet"]
description: "A lightweight mithril axe."
value: 500
stackable: false
diff --git a/data/items/mithril_ore.yaml b/data/items/mithril_ore.yaml
index 23a5d81..42339af 100644
--- a/data/items/mithril_ore.yaml
+++ b/data/items/mithril_ore.yaml
@@ -1,7 +1,6 @@
id: mithril_ore
name: mithril ore
color: "fg=blue"
-aliases: ["mithril", "ore"]
description: "A piece of light blue mithril ore."
value: 50
stackable: false
diff --git a/data/items/mithril_pickaxe.yaml b/data/items/mithril_pickaxe.yaml
index 585cec6..a72614d 100644
--- a/data/items/mithril_pickaxe.yaml
+++ b/data/items/mithril_pickaxe.yaml
@@ -1,7 +1,6 @@
id: mithril_pickaxe
name: mithril pickaxe
color: "fg=blue"
-aliases: ["pick", "pickaxe"]
description: "A lightweight mithril pickaxe."
value: 500
stackable: false
diff --git a/data/items/oak_logs.yaml b/data/items/oak_logs.yaml
index 12ee703..1176f85 100644
--- a/data/items/oak_logs.yaml
+++ b/data/items/oak_logs.yaml
@@ -1,7 +1,6 @@
id: oak_logs
name: oak logs
color: "fg=green"
-aliases: ["logs", "oak", "log"]
description: "Some sturdy oak logs."
value: 4
stackable: false
diff --git a/data/items/party_hat_blue.yaml b/data/items/party_hat_blue.yaml
index 5ccfb26..5aa5a8b 100644
--- a/data/items/party_hat_blue.yaml
+++ b/data/items/party_hat_blue.yaml
@@ -1,7 +1,6 @@
id: party_hat_blue
name: blue party hat
color: "fg=blue"
-aliases: ["party hat", "blue hat"]
description: "A festive blue party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_green.yaml b/data/items/party_hat_green.yaml
index edf590e..03273ac 100644
--- a/data/items/party_hat_green.yaml
+++ b/data/items/party_hat_green.yaml
@@ -1,7 +1,6 @@
id: party_hat_green
name: green party hat
color: "fg=green"
-aliases: ["party hat", "green hat"]
description: "A festive green party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_pink.yaml b/data/items/party_hat_pink.yaml
index d0c8abe..b4e350a 100644
--- a/data/items/party_hat_pink.yaml
+++ b/data/items/party_hat_pink.yaml
@@ -1,7 +1,6 @@
id: party_hat_pink
name: pink party hat
color: "fg=magenta"
-aliases: ["party hat", "pink hat"]
description: "A festive pink party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_red.yaml b/data/items/party_hat_red.yaml
index f85396f..48a503a 100644
--- a/data/items/party_hat_red.yaml
+++ b/data/items/party_hat_red.yaml
@@ -1,7 +1,6 @@
id: party_hat_red
name: red party hat
color: "fg=red"
-aliases: ["party hat", "red hat"]
description: "A festive red party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_white.yaml b/data/items/party_hat_white.yaml
index 29b978a..785fac3 100644
--- a/data/items/party_hat_white.yaml
+++ b/data/items/party_hat_white.yaml
@@ -1,7 +1,6 @@
id: party_hat_white
name: white party hat
color: "fg=white"
-aliases: ["party hat", "white hat"]
description: "A festive white party hat."
value: 2
equip_slot: head
diff --git a/data/items/party_hat_yellow.yaml b/data/items/party_hat_yellow.yaml
index 561156a..8b7c20e 100644
--- a/data/items/party_hat_yellow.yaml
+++ b/data/items/party_hat_yellow.yaml
@@ -1,7 +1,6 @@
id: party_hat_yellow
name: yellow party hat
color: "fg=yellow"
-aliases: ["party hat", "yellow hat"]
description: "A festive yellow party hat."
value: 2
equip_slot: head
diff --git a/data/items/pastry_dough.yaml b/data/items/pastry_dough.yaml
new file mode 100644
index 0000000..200d048
--- /dev/null
+++ b/data/items/pastry_dough.yaml
@@ -0,0 +1,11 @@
+id: pastry_dough
+name: pastry dough
+color: "fg=bright_yellow"
+description: "A ball of uncooked pastry dough. It needs to be baked."
+value: 3
+stackable: false
+made_from:
+ - items: [pot_of_flour]
+ qty: 1
+ - items: [bucket_of_water]
+ qty: 1
diff --git a/data/items/pizza_dough.yaml b/data/items/pizza_dough.yaml
new file mode 100644
index 0000000..59043ca
--- /dev/null
+++ b/data/items/pizza_dough.yaml
@@ -0,0 +1,11 @@
+id: pizza_dough
+name: pizza dough
+color: "fg=bright_yellow"
+description: "A ball of uncooked pizza dough. It needs to be baked."
+value: 3
+stackable: false
+made_from:
+ - items: [pot_of_flour]
+ qty: 1
+ - items: [bucket_of_water]
+ qty: 1
diff --git a/data/items/pot_of_flour.yaml b/data/items/pot_of_flour.yaml
new file mode 100644
index 0000000..100114b
--- /dev/null
+++ b/data/items/pot_of_flour.yaml
@@ -0,0 +1,6 @@
+id: pot_of_flour
+name: pot of flour
+color: "fg=white"
+description: "A clay pot filled with finely ground flour."
+value: 5
+stackable: false
diff --git a/data/items/raw_anchovies.yaml b/data/items/raw_anchovies.yaml
index ae6234b..74d8ae5 100644
--- a/data/items/raw_anchovies.yaml
+++ b/data/items/raw_anchovies.yaml
@@ -1,7 +1,6 @@
id: raw_anchovies
name: raw anchovies
color: "fg=blue"
-aliases: ["anchovies", "anchovy"]
description: "Some tiny raw anchovies."
value: 2
stackable: false
diff --git a/data/items/raw_anglerfish.yaml b/data/items/raw_anglerfish.yaml
index b38105e..53a771e 100644
--- a/data/items/raw_anglerfish.yaml
+++ b/data/items/raw_anglerfish.yaml
@@ -1,7 +1,6 @@
id: raw_anglerfish
name: raw anglerfish
color: "fg=cyan"
-aliases: ["anglerfish", "angler"]
description: "An ugly but delicious raw anglerfish."
value: 20
stackable: false
diff --git a/data/items/raw_bass.yaml b/data/items/raw_bass.yaml
index 6f80a2c..766f141 100644
--- a/data/items/raw_bass.yaml
+++ b/data/items/raw_bass.yaml
@@ -1,7 +1,6 @@
id: raw_bass
name: raw bass
color: "fg=blue"
-aliases: ["bass"]
description: "A freshly caught raw bass."
value: 10
stackable: false
diff --git a/data/items/raw_cave_eel.yaml b/data/items/raw_cave_eel.yaml
index 884fe1e..81c2e1a 100644
--- a/data/items/raw_cave_eel.yaml
+++ b/data/items/raw_cave_eel.yaml
@@ -1,7 +1,6 @@
id: raw_cave_eel
name: raw cave eel
color: "fg=cyan"
-aliases: ["cave eel", "eel"]
description: "A pale eel from deep underground waters."
value: 12
stackable: false
diff --git a/data/items/raw_cod.yaml b/data/items/raw_cod.yaml
index aa98c30..3124df3 100644
--- a/data/items/raw_cod.yaml
+++ b/data/items/raw_cod.yaml
@@ -1,7 +1,6 @@
id: raw_cod
name: raw cod
color: "fg=blue"
-aliases: ["cod"]
description: "A freshly caught raw cod."
value: 6
stackable: false
diff --git a/data/items/raw_herring.yaml b/data/items/raw_herring.yaml
index ad439d9..fd8f9c0 100644
--- a/data/items/raw_herring.yaml
+++ b/data/items/raw_herring.yaml
@@ -1,7 +1,6 @@
id: raw_herring
name: raw herring
color: "fg=blue"
-aliases: ["herring"]
description: "A freshly caught raw herring."
value: 5
stackable: false
diff --git a/data/items/raw_lobster.yaml b/data/items/raw_lobster.yaml
index ac97b82..71983f8 100644
--- a/data/items/raw_lobster.yaml
+++ b/data/items/raw_lobster.yaml
@@ -1,7 +1,6 @@
id: raw_lobster
name: raw lobster
color: "fg=blue"
-aliases: ["lobster"]
description: "A freshly caught raw lobster."
value: 15
stackable: false
diff --git a/data/items/raw_mackerel.yaml b/data/items/raw_mackerel.yaml
index 0f1eb5d..71f7bac 100644
--- a/data/items/raw_mackerel.yaml
+++ b/data/items/raw_mackerel.yaml
@@ -1,7 +1,6 @@
id: raw_mackerel
name: raw mackerel
color: "fg=blue"
-aliases: ["mackerel"]
description: "A freshly caught raw mackerel."
value: 4
stackable: false
diff --git a/data/items/raw_pike.yaml b/data/items/raw_pike.yaml
index fad2599..f4eaf51 100644
--- a/data/items/raw_pike.yaml
+++ b/data/items/raw_pike.yaml
@@ -1,7 +1,6 @@
id: raw_pike
name: raw pike
color: "fg=blue"
-aliases: ["pike"]
description: "A freshly caught raw pike."
value: 8
stackable: false
diff --git a/data/items/raw_rainbow_fish.yaml b/data/items/raw_rainbow_fish.yaml
index f27f52b..c86c4e7 100644
--- a/data/items/raw_rainbow_fish.yaml
+++ b/data/items/raw_rainbow_fish.yaml
@@ -1,7 +1,6 @@
id: raw_rainbow_fish
name: raw rainbow fish
color: "fg=magenta"
-aliases: ["rainbow fish", "rainbow"]
description: "A shimmering raw rainbow fish."
value: 10
stackable: false
diff --git a/data/items/raw_salmon.yaml b/data/items/raw_salmon.yaml
index 457e74d..abf00a3 100644
--- a/data/items/raw_salmon.yaml
+++ b/data/items/raw_salmon.yaml
@@ -1,7 +1,6 @@
id: raw_salmon
name: raw salmon
color: "fg=blue"
-aliases: ["salmon"]
description: "A freshly caught raw salmon."
value: 8
stackable: false
diff --git a/data/items/raw_sardine.yaml b/data/items/raw_sardine.yaml
index f42d19d..c46bebb 100644
--- a/data/items/raw_sardine.yaml
+++ b/data/items/raw_sardine.yaml
@@ -1,7 +1,6 @@
id: raw_sardine
name: raw sardine
color: "fg=blue"
-aliases: ["sardine"]
description: "A freshly caught raw sardine."
value: 3
stackable: false
diff --git a/data/items/raw_shark.yaml b/data/items/raw_shark.yaml
index b5a0ebc..90a3436 100644
--- a/data/items/raw_shark.yaml
+++ b/data/items/raw_shark.yaml
@@ -1,7 +1,6 @@
id: raw_shark
name: raw shark
color: "fg=blue"
-aliases: ["shark"]
description: "A freshly caught raw shark."
value: 30
stackable: false
diff --git a/data/items/raw_shrimps.yaml b/data/items/raw_shrimps.yaml
index a343011..0eb919b 100644
--- a/data/items/raw_shrimps.yaml
+++ b/data/items/raw_shrimps.yaml
@@ -1,7 +1,6 @@
id: raw_shrimps
name: raw shrimps
color: "fg=blue"
-aliases: ["shrimps", "shrimp"]
description: "Some small raw shrimps."
value: 2
stackable: false
diff --git a/data/items/raw_slimy_eel.yaml b/data/items/raw_slimy_eel.yaml
index 68c0efe..8010db1 100644
--- a/data/items/raw_slimy_eel.yaml
+++ b/data/items/raw_slimy_eel.yaml
@@ -1,7 +1,6 @@
id: raw_slimy_eel
name: raw slimy eel
color: "fg=green"
-aliases: ["slimy eel", "eel"]
description: "A slippery raw eel covered in slime."
value: 10
stackable: false
diff --git a/data/items/raw_swordfish.yaml b/data/items/raw_swordfish.yaml
index a09e3f4..a0a5a73 100644
--- a/data/items/raw_swordfish.yaml
+++ b/data/items/raw_swordfish.yaml
@@ -1,7 +1,6 @@
id: raw_swordfish
name: raw swordfish
color: "fg=blue"
-aliases: ["swordfish", "sword"]
description: "A freshly caught raw swordfish."
value: 18
stackable: false
diff --git a/data/items/raw_trout.yaml b/data/items/raw_trout.yaml
index 299f8fd..248f6c1 100644
--- a/data/items/raw_trout.yaml
+++ b/data/items/raw_trout.yaml
@@ -1,7 +1,6 @@
id: raw_trout
name: raw trout
color: "fg=blue"
-aliases: ["trout", "fish", "raw"]
description: "A freshly caught trout. Needs cooking."
value: 3
stackable: false
diff --git a/data/items/raw_tuna.yaml b/data/items/raw_tuna.yaml
index 0b707c3..1fdfcb8 100644
--- a/data/items/raw_tuna.yaml
+++ b/data/items/raw_tuna.yaml
@@ -1,7 +1,6 @@
id: raw_tuna
name: raw tuna
color: "fg=blue"
-aliases: ["tuna"]
description: "A freshly caught raw tuna."
value: 12
stackable: false
diff --git a/data/items/redwood_logs.yaml b/data/items/redwood_logs.yaml
index c3cd8e6..25bc3d1 100644
--- a/data/items/redwood_logs.yaml
+++ b/data/items/redwood_logs.yaml
@@ -1,7 +1,6 @@
id: redwood_logs
name: redwood logs
color: "fg=bright_red"
-aliases: ["logs", "redwood", "log"]
description: "Some rare redwood logs."
value: 100
stackable: false
diff --git a/data/items/rune_axe.yaml b/data/items/rune_axe.yaml
index 13158fc..8040e0b 100644
--- a/data/items/rune_axe.yaml
+++ b/data/items/rune_axe.yaml
@@ -1,7 +1,6 @@
id: rune_axe
name: rune axe
color: "fg=cyan"
-aliases: ["axe", "hatchet"]
description: "An extremely sharp rune axe."
value: 40000
stackable: false
diff --git a/data/items/rune_pickaxe.yaml b/data/items/rune_pickaxe.yaml
index 0086d9d..4d8c967 100644
--- a/data/items/rune_pickaxe.yaml
+++ b/data/items/rune_pickaxe.yaml
@@ -1,7 +1,6 @@
id: rune_pickaxe
name: rune pickaxe
color: "fg=cyan"
-aliases: ["pick", "pickaxe"]
description: "An extremely durable rune pickaxe."
value: 40000
stackable: false
diff --git a/data/items/runite_ore.yaml b/data/items/runite_ore.yaml
index ca8e417..8b9d288 100644
--- a/data/items/runite_ore.yaml
+++ b/data/items/runite_ore.yaml
@@ -1,7 +1,6 @@
id: runite_ore
name: runite ore
color: "fg=cyan"
-aliases: ["runite", "rune", "ore"]
description: "A rare piece of light blue runite ore."
value: 500
stackable: false
diff --git a/data/items/sacred_eel.yaml b/data/items/sacred_eel.yaml
index 2697f1b..e0ec18e 100644
--- a/data/items/sacred_eel.yaml
+++ b/data/items/sacred_eel.yaml
@@ -1,7 +1,6 @@
id: sacred_eel
name: sacred eel
color: "fg=magenta"
-aliases: ["sacred eel", "eel"]
description: "A glowing eel imbued with sacred energy."
value: 25
stackable: false
diff --git a/data/items/salmon.yaml b/data/items/salmon.yaml
new file mode 100644
index 0000000..307d4ae
--- /dev/null
+++ b/data/items/salmon.yaml
@@ -0,0 +1,8 @@
+id: salmon
+name: salmon
+color: "fg=bright_red"
+description: "A nicely cooked salmon."
+value: 10
+stackable: false
+heal_value: 9
+eat_message: "You eat the salmon. Savory and satisfying!"
diff --git a/data/items/sardine.yaml b/data/items/sardine.yaml
new file mode 100644
index 0000000..8414553
--- /dev/null
+++ b/data/items/sardine.yaml
@@ -0,0 +1,8 @@
+id: sardine
+name: sardine
+color: "fg=bright_red"
+description: "A nicely cooked sardine."
+value: 5
+stackable: false
+heal_value: 4
+eat_message: "You eat the sardine."
diff --git a/data/items/scrap_metal.yaml b/data/items/scrap_metal.yaml
index 6d96e89..81e068d 100644
--- a/data/items/scrap_metal.yaml
+++ b/data/items/scrap_metal.yaml
@@ -1,7 +1,6 @@
id: scrap_metal
name: scrap metal
color: "dim"
-aliases: ["scrap", "metal"]
description: "A chunk of salvaged scrap metal."
value: 2
stackable: false
diff --git a/data/items/shrimps.yaml b/data/items/shrimps.yaml
new file mode 100644
index 0000000..c7b47b4
--- /dev/null
+++ b/data/items/shrimps.yaml
@@ -0,0 +1,8 @@
+id: shrimps
+name: shrimps
+color: "fg=bright_red"
+description: "Some nicely cooked shrimps."
+value: 5
+stackable: false
+heal_value: 3
+eat_message: "You eat the shrimps. Tasty!"
diff --git a/data/items/silver_ore.yaml b/data/items/silver_ore.yaml
index b6554d5..c8a6b73 100644
--- a/data/items/silver_ore.yaml
+++ b/data/items/silver_ore.yaml
@@ -1,7 +1,6 @@
id: silver_ore
name: silver ore
color: "fg=white"
-aliases: ["silver", "ore"]
description: "A piece of silver ore with a faint metallic sheen."
value: 10
stackable: false
diff --git a/data/items/small_fishing_net.yaml b/data/items/small_fishing_net.yaml
index e5fab71..f497a55 100644
--- a/data/items/small_fishing_net.yaml
+++ b/data/items/small_fishing_net.yaml
@@ -1,7 +1,6 @@
id: small_fishing_net
name: small fishing net
color: "fg=white"
-aliases: ["net", "small net"]
description: "A small hand-held fishing net."
value: 5
stackable: false
diff --git a/data/items/steel_axe.yaml b/data/items/steel_axe.yaml
index 976a5af..240c921 100644
--- a/data/items/steel_axe.yaml
+++ b/data/items/steel_axe.yaml
@@ -1,7 +1,6 @@
id: steel_axe
name: steel axe
color: "fg=white"
-aliases: ["axe", "hatchet"]
description: "A sharp steel axe for serious woodcutting."
value: 200
stackable: false
diff --git a/data/items/steel_pickaxe.yaml b/data/items/steel_pickaxe.yaml
index 282e0e5..bff14ed 100644
--- a/data/items/steel_pickaxe.yaml
+++ b/data/items/steel_pickaxe.yaml
@@ -1,7 +1,6 @@
id: steel_pickaxe
name: steel pickaxe
color: "fg=white"
-aliases: ["pick", "pickaxe"]
description: "A sharp steel pickaxe."
value: 200
stackable: false
diff --git a/data/items/teak_logs.yaml b/data/items/teak_logs.yaml
index b33a86a..1f49678 100644
--- a/data/items/teak_logs.yaml
+++ b/data/items/teak_logs.yaml
@@ -1,7 +1,6 @@
id: teak_logs
name: teak logs
color: "fg=bright_yellow"
-aliases: ["logs", "teak", "log"]
description: "Some solid teak logs."
value: 12
stackable: false
diff --git a/data/items/tin_ore.yaml b/data/items/tin_ore.yaml
index b2227aa..7e31d8c 100644
--- a/data/items/tin_ore.yaml
+++ b/data/items/tin_ore.yaml
@@ -1,7 +1,6 @@
id: tin_ore
name: tin ore
color: "fg=white"
-aliases: ["tin", "ore"]
description: "A lump of tin ore, ready for smelting."
value: 3
stackable: false
diff --git a/data/items/trout.yaml b/data/items/trout.yaml
new file mode 100644
index 0000000..65486cb
--- /dev/null
+++ b/data/items/trout.yaml
@@ -0,0 +1,8 @@
+id: trout
+name: trout
+color: "fg=bright_red"
+description: "A nicely cooked trout."
+value: 8
+stackable: false
+heal_value: 7
+eat_message: "You eat the trout. Delicious!"
diff --git a/data/items/uncut_diamond.yaml b/data/items/uncut_diamond.yaml
index 1c3fffe..1bad26c 100644
--- a/data/items/uncut_diamond.yaml
+++ b/data/items/uncut_diamond.yaml
@@ -1,7 +1,6 @@
id: uncut_diamond
name: uncut diamond
color: "fg=white"
-aliases: ["diamond", "gem", "uncut"]
description: "An uncut diamond. Can be cut with crafting."
value: 200
stackable: false
diff --git a/data/items/uncut_emerald.yaml b/data/items/uncut_emerald.yaml
index 2ec0d5c..e97d180 100644
--- a/data/items/uncut_emerald.yaml
+++ b/data/items/uncut_emerald.yaml
@@ -1,7 +1,6 @@
id: uncut_emerald
name: uncut emerald
color: "fg=green"
-aliases: ["emerald", "gem", "uncut"]
description: "An uncut emerald. Can be cut with crafting."
value: 50
stackable: false
diff --git a/data/items/uncut_ruby.yaml b/data/items/uncut_ruby.yaml
index 491bbe0..3d45d5f 100644
--- a/data/items/uncut_ruby.yaml
+++ b/data/items/uncut_ruby.yaml
@@ -1,7 +1,6 @@
id: uncut_ruby
name: uncut ruby
color: "fg=bright_red"
-aliases: ["ruby", "gem", "uncut"]
description: "An uncut ruby. Can be cut with crafting."
value: 100
stackable: false
diff --git a/data/items/uncut_sapphire.yaml b/data/items/uncut_sapphire.yaml
index 9e61020..4d50cb3 100644
--- a/data/items/uncut_sapphire.yaml
+++ b/data/items/uncut_sapphire.yaml
@@ -1,7 +1,6 @@
id: uncut_sapphire
name: uncut sapphire
color: "fg=blue"
-aliases: ["sapphire", "gem", "uncut"]
description: "An uncut sapphire. Can be cut with crafting."
value: 25
stackable: false
diff --git a/data/items/willow_logs.yaml b/data/items/willow_logs.yaml
index fa87857..1461ff2 100644
--- a/data/items/willow_logs.yaml
+++ b/data/items/willow_logs.yaml
@@ -1,7 +1,6 @@
id: willow_logs
name: willow logs
color: "fg=green"
-aliases: ["logs", "willow", "log"]
description: "Some flexible willow logs."
value: 8
stackable: false
diff --git a/data/items/yew_logs.yaml b/data/items/yew_logs.yaml
index 6ca8c88..557ca1d 100644
--- a/data/items/yew_logs.yaml
+++ b/data/items/yew_logs.yaml
@@ -1,7 +1,6 @@
id: yew_logs
name: yew logs
color: "fg=green"
-aliases: ["logs", "yew", "log"]
description: "Some high-quality yew logs."
value: 40
stackable: false
diff --git a/data/mobs/guard.yaml b/data/mobs/guard.yaml
index 3203ec1..4b5576c 100644
--- a/data/mobs/guard.yaml
+++ b/data/mobs/guard.yaml
@@ -1,5 +1,5 @@
-id: "guard"
-name: "Guard"
+id: guard
+name: Guard
description: "A stern-looking guard in weathered armor."
behavior: guard_talk
unique: true
diff --git a/data/mobs/man.yaml b/data/mobs/man.yaml
index 5fa9625..a4b0729 100644
--- a/data/mobs/man.yaml
+++ b/data/mobs/man.yaml
@@ -1,5 +1,5 @@
-id: "man"
-name: "man"
+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"
diff --git a/data/mobs/newbie_trainer.yaml b/data/mobs/newbie_trainer.yaml
index c518d42..162dfa2 100644
--- a/data/mobs/newbie_trainer.yaml
+++ b/data/mobs/newbie_trainer.yaml
@@ -1,6 +1,5 @@
-id: "newbie_trainer"
-name: "Newbie Trainer"
-aliases: ["trainer", "newbie", "instructor"]
+id: newbie_trainer
+name: Newbie Trainer
description: "A friendly-looking instructor ready to help new adventurers."
unique: true
protected: true
diff --git a/data/objects/cooking_range.yaml b/data/objects/cooking_range.yaml
new file mode 100644
index 0000000..caa52c5
--- /dev/null
+++ b/data/objects/cooking_range.yaml
@@ -0,0 +1,5 @@
+id: cooking_range
+name: cooking range
+color: "fg=white"
+props:
+ description: "A sturdy iron range with multiple burners. Perfect for cooking."
diff --git a/data/objects/lumby_fountain.yaml b/data/objects/lumby_fountain.yaml
index e46d0f5..29c79ea 100644
--- a/data/objects/lumby_fountain.yaml
+++ b/data/objects/lumby_fountain.yaml
@@ -1,6 +1,5 @@
id: lumby_fountain
name: town fountain
color: "fg=cyan"
-behavior: ""
props:
description: "Clear water sparkles in the sunlight."
diff --git a/data/recipes/cook_anchovies.yaml b/data/recipes/cook_anchovies.yaml
new file mode 100644
index 0000000..df04d01
--- /dev/null
+++ b/data/recipes/cook_anchovies.yaml
@@ -0,0 +1,14 @@
+id: cook_anchovies
+type: cooking
+skill: cooking
+level: 1
+xp: 30
+wait: 6
+station: [fire, cooking_range]
+consume:
+ - items: [raw_anchovies]
+ qty: 1
+output: anchovies
+fail: burnt_fish
+message: "Cooked to perfection. It looks great!"
+fail_message: "You accidentally burn the anchovies."
diff --git a/data/recipes/cook_bread.yaml b/data/recipes/cook_bread.yaml
new file mode 100644
index 0000000..650525f
--- /dev/null
+++ b/data/recipes/cook_bread.yaml
@@ -0,0 +1,14 @@
+id: cook_bread
+type: cooking
+skill: cooking
+level: 1
+xp: 40
+wait: 6
+station: [cooking_range]
+consume:
+ - items: [bread_dough]
+ qty: 1
+output: bread
+fail: burnt_meat
+message: "You bake the dough into a fresh loaf of bread."
+fail_message: "You burn the bread to a crisp."
diff --git a/data/recipes/cook_herring.yaml b/data/recipes/cook_herring.yaml
new file mode 100644
index 0000000..7779ef0
--- /dev/null
+++ b/data/recipes/cook_herring.yaml
@@ -0,0 +1,14 @@
+id: cook_herring
+type: cooking
+skill: cooking
+level: 5
+xp: 50
+wait: 6
+station: [fire, cooking_range]
+consume:
+ - items: [raw_herring]
+ qty: 1
+output: herring
+fail: burnt_fish
+message: "Cooked to perfection. It looks great!"
+fail_message: "You accidentally burn the herring."
diff --git a/data/recipes/cook_salmon.yaml b/data/recipes/cook_salmon.yaml
new file mode 100644
index 0000000..04ee40c
--- /dev/null
+++ b/data/recipes/cook_salmon.yaml
@@ -0,0 +1,14 @@
+id: cook_salmon
+type: cooking
+skill: cooking
+level: 25
+xp: 90
+wait: 6
+station: [fire, cooking_range]
+consume:
+ - items: [raw_salmon]
+ qty: 1
+output: salmon
+fail: burnt_fish
+message: "Cooked to perfection. It looks great!"
+fail_message: "You accidentally burn the salmon."
diff --git a/data/recipes/cook_sardine.yaml b/data/recipes/cook_sardine.yaml
new file mode 100644
index 0000000..0ad6901
--- /dev/null
+++ b/data/recipes/cook_sardine.yaml
@@ -0,0 +1,14 @@
+id: cook_sardine
+type: cooking
+skill: cooking
+level: 1
+xp: 40
+wait: 6
+station: [fire, cooking_range]
+consume:
+ - items: [raw_sardine]
+ qty: 1
+output: sardine
+fail: burnt_fish
+message: "Cooked to perfection. It looks great!"
+fail_message: "You accidentally burn the sardine."
diff --git a/data/recipes/cook_shrimps.yaml b/data/recipes/cook_shrimps.yaml
new file mode 100644
index 0000000..f38c1cd
--- /dev/null
+++ b/data/recipes/cook_shrimps.yaml
@@ -0,0 +1,14 @@
+id: cook_shrimps
+type: cooking
+skill: cooking
+level: 1
+xp: 30
+wait: 6
+station: [fire, cooking_range]
+consume:
+ - items: [raw_shrimps]
+ qty: 1
+output: shrimps
+fail: burnt_fish
+message: "Cooked to perfection. It looks great!"
+fail_message: "You accidentally burn the shrimps."
diff --git a/data/recipes/cook_trout.yaml b/data/recipes/cook_trout.yaml
new file mode 100644
index 0000000..79df193
--- /dev/null
+++ b/data/recipes/cook_trout.yaml
@@ -0,0 +1,14 @@
+id: cook_trout
+type: cooking
+skill: cooking
+level: 15
+xp: 70
+wait: 6
+station: [fire, cooking_range]
+consume:
+ - items: [raw_trout]
+ qty: 1
+output: trout
+fail: burnt_fish
+message: "Cooked to perfection. It looks great!"
+fail_message: "You accidentally burn the trout."
diff --git a/data/rooms/11.yaml b/data/rooms/11.yaml
index bf26577..cc2a220 100644
--- a/data/rooms/11.yaml
+++ b/data/rooms/11.yaml
@@ -1,6 +1,15 @@
id: 11
name: "Kitchen"
-description: "A warm kitchen with a stone hearth and wooden counters. This area is not yet accessible."
+description: "A warm kitchen with a stone hearth and wooden counters. A large iron cooking range dominates one wall, and shelves are stocked with ingredients."
exits:
east: 12
west: 10
+objects:
+ - id: cooking_range
+spawns:
+ - item_id: bucket_of_water
+ quantity: 1
+ respawn_ticks: 200
+ - item_id: pot_of_flour
+ quantity: 1
+ respawn_ticks: 200
diff --git a/internal/action/recipe.go b/internal/action/recipe.go
new file mode 100644
index 0000000..7627047
--- /dev/null
+++ b/internal/action/recipe.go
@@ -0,0 +1,213 @@
+package action
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/yaml.v3"
+)
+
+type ConsumeEntry struct {
+ Items []string `yaml:"items"`
+ Qty int `yaml:"qty"`
+}
+
+type RecipeDef struct {
+ ID string `yaml:"id"`
+ Type string `yaml:"type"`
+ Skill string `yaml:"skill"`
+ Level int `yaml:"level"`
+ XP int `yaml:"xp"`
+ Station []string `yaml:"station"`
+ Consume []ConsumeEntry `yaml:"consume"`
+ Output string `yaml:"output"`
+ Fail string `yaml:"fail"`
+ Message string `yaml:"message"`
+ FailMessage string `yaml:"fail_message"`
+ Wait float64 `yaml:"wait"`
+ Success *SuccessFormula `yaml:"success"`
+}
+
+type RecipeStore struct {
+ dataDir string
+}
+
+func NewRecipeStore(dataDir string) *RecipeStore {
+ return &RecipeStore{dataDir: dataDir}
+}
+
+func (s *RecipeStore) LoadAll() ([]RecipeDef, error) {
+ dir := filepath.Join(s.dataDir, "recipes")
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+ var recipes []RecipeDef
+ for _, e := range entries {
+ if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" {
+ continue
+ }
+ data, err := os.ReadFile(filepath.Join(dir, e.Name()))
+ if err != nil {
+ continue
+ }
+ var r RecipeDef
+ if err := yaml.Unmarshal(data, &r); err != nil {
+ continue
+ }
+ recipes = append(recipes, r)
+ }
+ return recipes, nil
+}
+
+func (r *RecipeDef) MatchesEntry(itemID string) bool {
+ for _, e := range r.Consume {
+ for _, id := range e.Items {
+ if id == itemID {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (s *RecipeStore) FindByItem(itemID, recipeType string) ([]RecipeDef, error) {
+ all, err := s.LoadAll()
+ if err != nil {
+ return nil, err
+ }
+ var matches []RecipeDef
+ for _, r := range all {
+ if recipeType != "" && r.Type != recipeType {
+ continue
+ }
+ if r.MatchesEntry(itemID) {
+ matches = append(matches, r)
+ }
+ }
+ return matches, nil
+}
+
+func (s *RecipeStore) FindByItems(a, b string) ([]RecipeDef, error) {
+ all, err := s.LoadAll()
+ if err != nil {
+ return nil, err
+ }
+ var matches []RecipeDef
+ for _, r := range all {
+ if len(r.Consume) < 2 {
+ continue
+ }
+ aEntry := -1
+ bEntry := -1
+ for ei, e := range r.Consume {
+ for _, id := range e.Items {
+ if id == a && aEntry < 0 {
+ aEntry = ei
+ }
+ if id == b && bEntry < 0 {
+ bEntry = ei
+ }
+ }
+ }
+ if aEntry >= 0 && bEntry >= 0 && aEntry != bEntry {
+ matches = append(matches, r)
+ }
+ }
+ return matches, nil
+}
+
+func (s *RecipeStore) FindByStation(itemID string, stationIDs []string, recipeType string) ([]RecipeDef, error) {
+ all, err := s.LoadAll()
+ if err != nil {
+ return nil, err
+ }
+ var matches []RecipeDef
+ for _, r := range all {
+ if recipeType != "" && r.Type != recipeType {
+ continue
+ }
+ if !r.MatchesEntry(itemID) {
+ continue
+ }
+ if len(r.Station) == 0 {
+ continue
+ }
+ for _, station := range stationIDs {
+ for _, rs := range r.Station {
+ if rs == station {
+ matches = append(matches, r)
+ goto next
+ }
+ }
+ }
+ next:
+ }
+ return matches, nil
+}
+
+func (r *RecipeDef) DisplayName() string {
+ return r.Output
+}
+
+func (s *RecipeStore) FindByNameOrID(query string, recipeType string) (*RecipeDef, error) {
+ all, err := s.LoadAll()
+ if err != nil {
+ return nil, err
+ }
+ for _, r := range all {
+ if recipeType != "" && r.Type != recipeType {
+ continue
+ }
+ if r.ID == query || r.Output == query {
+ return &r, nil
+ }
+ }
+ return nil, fmt.Errorf("recipe not found: %s", query)
+}
+
+func (r *RecipeDef) FirstItemName(load func(string) (string, bool)) string {
+ for _, e := range r.Consume {
+ for _, id := range e.Items {
+ if name, ok := load(id); ok {
+ return name
+ }
+ return id
+ }
+ }
+ return ""
+}
+
+func (r *RecipeDef) ConsumeAll(hasItem func(string) bool, removeItem func(string, int) bool) bool {
+ for _, e := range r.Consume {
+ found := false
+ for _, id := range e.Items {
+ if hasItem(id) {
+ removeItem(id, e.Qty)
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+ return true
+}
+
+func (r *RecipeDef) HasAllItems(hasItem func(string) bool) bool {
+ for _, e := range r.Consume {
+ found := false
+ for _, id := range e.Items {
+ if hasItem(id) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/game/action.go b/internal/game/action.go
index 26484e0..3857030 100644
--- a/internal/game/action.go
+++ b/internal/game/action.go
@@ -224,6 +224,8 @@ func (g *Game) AdvanceActions() {
g.advanceStoke(sess, p)
case "search":
g.advanceSearch(sess, p)
+ case "cook":
+ g.advanceCook(sess, p)
}
if p.Action == nil {
g.writePrompt(sess)
diff --git a/internal/game/action_cook.go b/internal/game/action_cook.go
new file mode 100644
index 0000000..e4d2597
--- /dev/null
+++ b/internal/game/action_cook.go
@@ -0,0 +1,153 @@
+package game
+
+import (
+ "fmt"
+ "math/rand"
+
+ "thirdcollapse/internal/action"
+ "thirdcollapse/internal/engine"
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/player"
+)
+
+func (g *Game) startCook(sess *net.Session, p *player.Player, recipe *action.RecipeDef, stationName string) {
+ g.CancelAction(p)
+
+ if recipe.Skill != "" {
+ skillLevel := p.Level(player.SkillName(recipe.Skill))
+ if skillLevel < recipe.Level {
+ sess.WriteLine(fmt.Sprintf("You need level %d %s to cook that.", recipe.Level, recipe.Skill))
+ return
+ }
+ }
+
+ if !recipe.HasAllItems(p.HasItem) {
+ sess.WriteLine("You don't have the required ingredients.")
+ return
+ }
+
+ wait := recipe.Wait
+ if wait <= 0 {
+ wait = 6
+ }
+
+ p.Action = &action.Action{
+ Type: "cook",
+ TargetID: recipe.ID,
+ TargetName: recipe.DisplayName(),
+ Data: map[string]any{
+ "recipe_id": recipe.ID,
+ "station_name": stationName,
+ "phase": 0,
+ "wait": wait,
+ },
+ WaitLeft: engine.ToTicks(1),
+ }
+
+ p.ActionState = &ActionState{Type: ActionCooking, TargetName: recipe.DisplayName()}
+}
+
+func (g *Game) advanceCook(sess *net.Session, p *player.Player) bool {
+ recipeID := p.Action.Data["recipe_id"].(string)
+ stationName := p.Action.Data["station_name"].(string)
+ phase := p.Action.Data["phase"].(int)
+ wait := p.Action.Data["wait"].(float64)
+
+ all, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ g.CancelAction(p)
+ return false
+ }
+ var recipe *action.RecipeDef
+ for _, r := range all {
+ if r.ID == recipeID {
+ recipe = &r
+ break
+ }
+ }
+ if recipe == nil {
+ g.CancelAction(p)
+ return false
+ }
+
+ if phase == 0 {
+ firstItem := recipe.FirstItemName(func(id string) (string, bool) {
+ def, err := g.ItemStore.Load(id)
+ if err != nil {
+ return id, false
+ }
+ return def.Name, true
+ })
+ p.ActionState = &ActionState{Type: ActionCooking, TargetName: recipe.DisplayName()}
+ if stationName != "" && stationName != "inventory" && stationName != "ground" {
+ sess.WriteLine(fmt.Sprintf("\nYou start cooking %s on the %s.", firstItem, stationName))
+ } else {
+ sess.WriteLine(fmt.Sprintf("\nYou start making %s.", recipe.DisplayName()))
+ }
+ p.Action.Data["phase"] = 1
+ p.Action.WaitLeft = engine.ToTicks(wait)
+ return true
+ }
+
+ skillLevel := p.Level(player.SkillName(recipe.Skill))
+ chance := 1.0
+ if recipe.Success != nil {
+ chance = action.SuccessChance(*recipe.Success, skillLevel, recipe.Level)
+ }
+
+ if rand.Float64() < chance {
+ outputID := recipe.Output
+
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("Your inventory is too full!")
+ g.CancelAction(p)
+ return false
+ }
+
+ recipe.ConsumeAll(p.HasItem, p.RemoveItem)
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: outputID, Quantity: 1})
+
+ if recipe.XP > 0 {
+ if newLevel := p.AddSkillXP(player.SkillName(recipe.Skill), recipe.XP); newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d %s! ***", newLevel, recipe.Skill)))
+ }
+ }
+ g.AccountStore.SaveCharacter(p)
+
+ msg := recipe.Message
+ if msg == "" {
+ msg = fmt.Sprintf("Cooked to perfection. It looks great!")
+ }
+ sess.WriteLine(msg)
+ if p.OptionBool("xp_drops") && recipe.XP > 0 {
+ abbr := player.SkillAbbr[player.SkillName(recipe.Skill)]
+ sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)))
+ }
+ } else {
+ recipe.ConsumeAll(p.HasItem, p.RemoveItem)
+
+ if recipe.Fail != "" {
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot >= 0 {
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: recipe.Fail, Quantity: 1})
+ }
+ }
+ g.AccountStore.SaveCharacter(p)
+
+ msg := recipe.FailMessage
+ if msg == "" {
+ msg = "You badly overcook it."
+ }
+ sess.WriteLine(msg)
+ }
+
+ if recipe.HasAllItems(p.HasItem) && p.FirstFreeSlot() >= 0 {
+ p.Action.Data["phase"] = 1
+ p.Action.WaitLeft = engine.ToTicks(wait)
+ return true
+ }
+
+ g.CancelAction(p)
+ return false
+}
diff --git a/internal/game/action_state.go b/internal/game/action_state.go
index 280dd56..c5d086e 100644
--- a/internal/game/action_state.go
+++ b/internal/game/action_state.go
@@ -19,6 +19,8 @@ const (
ActionSearching ActionType = "searching"
ActionResting ActionType = "resting"
ActionWalking ActionType = "walking"
+ ActionCooking ActionType = "cooking"
+ ActionEating ActionType = "eating"
)
type ActionState struct {
@@ -64,6 +66,10 @@ func (a *ActionState) Description() string {
return "resting"
case ActionWalking:
return "walking somewhere with a purpose!"
+ case ActionCooking:
+ return "cooking some " + a.TargetName
+ case ActionEating:
+ return "eating " + a.TargetName
}
return ""
}
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index ba4a2e1..2e15419 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -171,6 +171,11 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
}
func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ if g.processConsumeQueue(p, sess) {
+ p.ActionState = &ActionState{Type: ActionEating, TargetName: "food"}
+ return
+ }
+
attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
equipAtt := 0
diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go
new file mode 100644
index 0000000..be5fa3a
--- /dev/null
+++ b/internal/game/cmd_cook.go
@@ -0,0 +1,246 @@
+package game
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "thirdcollapse/internal/action"
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/player"
+)
+
+type cookableEntry struct {
+ ItemName string
+ Recipe action.RecipeDef
+}
+
+func (g *Game) doCook(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ g.CancelAction(p)
+
+ stationName := g.findCookingStation(p.RoomID)
+ if stationName == "" {
+ sess.WriteLine("You need a fire or cooking range to cook.")
+ return
+ }
+
+ allRecipes, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ sess.WriteLine("Error loading recipes.")
+ return
+ }
+
+ if input == "" {
+ g.showCookMenu(sess, p, allRecipes, stationName)
+ return
+ }
+
+ qty, itemName := parseQty(input)
+ _ = qty
+
+ matches := g.findInventoryMatches(itemName, p)
+ if len(matches) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemName))
+ return
+ }
+
+ unique := uniqueItemNames(matches)
+ if len(unique) > 1 {
+ sess.WriteLine("Which one?")
+ for _, name := range unique {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
+ itemID := matches[0].ID
+
+ var recipes []action.RecipeDef
+ for _, r := range allRecipes {
+ if r.Type != "cooking" {
+ continue
+ }
+ if stationOK(stationName, r.Station) && r.MatchesEntry(itemID) && r.HasAllItems(p.HasItem) {
+ recipes = append(recipes, r)
+ }
+ }
+
+ if len(recipes) == 0 {
+ sess.WriteLine("You can't cook that here.")
+ return
+ }
+
+ if len(recipes) == 1 {
+ g.startCook(sess, p, &recipes[0], stationName)
+ return
+ }
+
+ sess.PendingRecipeItem = itemID
+ sess.PendingCookMenu = recipesToMenuData(recipes)
+ sess.State = net.StateCookRecipe
+ sess.WriteLine(fmt.Sprintf("\nWhat would you like to cook on the %s?", stationName))
+ for i, r := range recipes {
+ sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r)))
+ }
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(recipes)+1))
+}
+
+func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationName string) {
+ seen := make(map[string]bool)
+ var cookables []cookableEntry
+
+ for _, r := range allRecipes {
+ if r.Type != "cooking" {
+ continue
+ }
+ if !stationOK(stationName, r.Station) {
+ continue
+ }
+ if seen[r.ID] {
+ continue
+ }
+ if !r.HasAllItems(p.HasItem) {
+ continue
+ }
+ seen[r.ID] = true
+ cookables = append(cookables, cookableEntry{g.recipeName(sess, &r), r})
+ }
+
+ if len(cookables) == 0 {
+ sess.WriteLine("You don't have anything you can cook.")
+ return
+ }
+
+ if len(cookables) == 1 {
+ g.startCook(sess, p, &cookables[0].Recipe, stationName)
+ return
+ }
+
+ sess.PendingRecipeItem = ""
+ sess.State = net.StateCookRecipe
+ sess.WriteLine(fmt.Sprintf("\nWhat would you like to cook on the %s?", stationName))
+ for i, c := range cookables {
+ sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, c.ItemName))
+ }
+ sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(cookables)+1))
+
+ sess.PendingCookMenu = makeCookMenuData(cookables)
+}
+
+func (g *Game) recipeName(sess *net.Session, r *action.RecipeDef) string {
+ if r.Output != "" {
+ if def, err := g.ItemStore.Load(r.Output); err == nil {
+ return g.itemColorize(sess, def, def.Name)
+ }
+ }
+ return r.DisplayName()
+}
+
+func makeCookMenuData(cookables []cookableEntry) []map[string]string {
+ out := make([]map[string]string, len(cookables))
+ for i, c := range cookables {
+ out[i] = map[string]string{"recipe_id": c.Recipe.ID}
+ }
+ return out
+}
+
+func recipesToMenuData(recipes []action.RecipeDef) []map[string]string {
+ out := make([]map[string]string, len(recipes))
+ for i, r := range recipes {
+ out[i] = map[string]string{"recipe_id": r.ID}
+ }
+ return out
+}
+
+func stationOK(stationName string, recipeStations []string) bool {
+ if len(recipeStations) == 0 {
+ return false
+ }
+ for _, rs := range recipeStations {
+ if (rs == "fire" && stationName == "fire") || (rs == "cooking_range" && stationName == "cooking range") {
+ return true
+ }
+ }
+ return false
+}
+
+func (g *Game) findCookingStation(roomID int) string {
+ for _, obj := range g.World.AllObjInstances(roomID) {
+ if obj.Depleted {
+ continue
+ }
+ if obj.DefID == "fire" {
+ return "fire"
+ }
+ if obj.DefID == "cooking_range" {
+ return "cooking range"
+ }
+ }
+ return ""
+}
+
+func (g *Game) handleCookRecipe(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ input = strings.TrimSpace(input)
+ if input == "" {
+ return
+ }
+
+ choice, err := strconv.Atoi(input)
+ if err != nil {
+ sess.State = net.StateGame
+ sess.PendingRecipeItem = ""
+ sess.PendingCookMenu = nil
+ sess.WriteLine("You decide not to cook anything.")
+ g.reprompt(sess)
+ return
+ }
+
+ if len(sess.PendingCookMenu) > 0 {
+ menuData := sess.PendingCookMenu
+ sess.PendingCookMenu = nil
+ if choice <= 0 || choice > len(menuData)+1 {
+ sess.WriteLine("Invalid choice.")
+ return
+ }
+ sess.State = net.StateGame
+ if choice == len(menuData)+1 {
+ sess.WriteLine("You decide not to cook anything.")
+ g.reprompt(sess)
+ return
+ }
+ entry := menuData[choice-1]
+
+ if cid, ok := entry["combine_item"]; ok {
+ def, err := g.ItemStore.Load(cid)
+ if err == nil {
+ p := sess.Player.(*player.Player)
+ g.produceCombined(sess, p, def)
+ }
+ g.reprompt(sess)
+ return
+ }
+
+ all, err := g.RecipeStore.LoadAll()
+ if err != nil {
+ g.reprompt(sess)
+ return
+ }
+ for _, r := range all {
+ if r.ID == entry["recipe_id"] {
+ stationName := "inventory"
+ if len(r.Station) > 0 {
+ stationName = g.findCookingStation(p.RoomID)
+ }
+ g.startCook(sess, p, &r, stationName)
+ return
+ }
+ }
+ g.reprompt(sess)
+ return
+ }
+
+ sess.State = net.StateGame
+ g.reprompt(sess)
+}
diff --git a/internal/game/cmd_eat.go b/internal/game/cmd_eat.go
new file mode 100644
index 0000000..5d6292b
--- /dev/null
+++ b/internal/game/cmd_eat.go
@@ -0,0 +1,132 @@
+package game
+
+import (
+ "fmt"
+ "time"
+
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/object"
+ "thirdcollapse/internal/player"
+)
+
+func (g *Game) doEat(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if input == "" {
+ sess.WriteLine("Eat what?")
+ return
+ }
+
+ qty, itemName := parseQty(input)
+ _ = qty
+
+ matches := g.findInventoryMatches(itemName, p)
+ if len(matches) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemName))
+ return
+ }
+
+ unique := uniqueItemNames(matches)
+ if len(unique) > 1 {
+ sess.WriteLine("Which one?")
+ for _, name := range unique {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
+ itemID := matches[0].ID
+ def, _ := g.ItemStore.Load(itemID)
+
+ if def == nil || def.EatMessage == "" {
+ sess.WriteLine("You can't eat that.")
+ return
+ }
+
+ if p.ConsumeCooldown > 0 {
+ sess.WriteLine("You're still digesting your last meal.")
+ return
+ }
+
+ g.cancelRest(p.Name)
+
+ if p.Action == nil && len(p.WalkSequence) == 0 {
+ g.doEatNow(sess, p, itemID, def)
+ return
+ }
+
+ g.CancelAction(p)
+
+ g.consumeQueue[p.Name] = &QueuedCommand{
+ Session: sess,
+ Command: "eat",
+ Args: itemID,
+ Timestamp: time.Now(),
+ }
+
+ if !p.OptionBool("queue_silently") {
+ sess.WriteLine(fmt.Sprintf("\nYou prepare to eat %s.", def.Name))
+ }
+}
+
+func (g *Game) doEatNow(sess *net.Session, p *player.Player, itemID string, def *object.ItemDef) {
+ if def == nil {
+ var err error
+ def, err = g.ItemStore.Load(itemID)
+ if err != nil || def.EatMessage == "" {
+ return
+ }
+ }
+
+ p.RemoveItem(itemID, 1)
+
+ p.HP += def.HealValue
+ maxHP := p.MaxHP()
+ if p.HP > maxHP {
+ p.HP = maxHP
+ }
+ if p.HP < 0 {
+ p.HP = 0
+ }
+
+ if def.HealValue != 0 {
+ p.StartRegen()
+ }
+
+ p.ConsumeCooldown = 3
+ g.AccountStore.SaveCharacter(p)
+
+ p.ActionState = &ActionState{Type: ActionEating, TargetName: def.Name}
+
+ sess.WriteLine(def.EatMessage)
+
+ if p.HP <= 0 {
+ g.endCombat(sess, p, nil)
+ }
+}
+
+func (g *Game) processConsumeQueue(p *player.Player, sess *net.Session) bool {
+ qc, ok := g.consumeQueue[p.Name]
+ if !ok {
+ return false
+ }
+ delete(g.consumeQueue, p.Name)
+
+ itemID := qc.Args
+ def, err := g.ItemStore.Load(itemID)
+ if err != nil || def.EatMessage == "" {
+ return false
+ }
+
+ if p.ConsumeCooldown > 0 {
+ return false
+ }
+
+ if !p.HasItem(itemID) {
+ sess.WriteLine("You no longer have that to eat.")
+ return false
+ }
+
+ g.doEatNow(sess, p, itemID, def)
+ return true
+}
diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go
new file mode 100644
index 0000000..ef36e12
--- /dev/null
+++ b/internal/game/cmd_use.go
@@ -0,0 +1,200 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/object"
+ "thirdcollapse/internal/player"
+)
+
+func (g *Game) doUse(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+ g.CancelAction(p)
+
+ lower := strings.ToLower(input)
+
+ sep := ""
+ if strings.Contains(lower, " on ") {
+ sep = " on "
+ } else if strings.Contains(lower, " with ") {
+ sep = " with "
+ }
+
+ if sep != "" {
+ parts := strings.SplitN(lower, sep, 2)
+ itemA := strings.TrimSpace(parts[0])
+ itemB := strings.TrimSpace(parts[1])
+ if itemA == itemB {
+ sess.WriteLine("You can't use that with itself.")
+ return
+ }
+ g.doUseItemOnTarget(sess, p, itemA, itemB)
+ return
+ }
+
+ if len(strings.Fields(input)) >= 2 {
+ g.doUseItemOnTarget(sess, p, strings.Fields(input)[0], strings.Join(strings.Fields(input)[1:], " "))
+ return
+ }
+
+ g.StartAction(sess, "use", input)
+}
+
+func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, itemBName string) {
+ matchesA := g.findInventoryMatches(itemAName, p)
+ if len(matchesA) == 0 {
+ sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", itemAName))
+ return
+ }
+
+ uniqueA := uniqueItemNames(matchesA)
+ if len(uniqueA) > 1 {
+ sess.WriteLine("Which one?")
+ for _, name := range uniqueA {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
+ itemAID := matchesA[0].ID
+
+ matchesB := g.findInventoryMatches(itemBName, p)
+
+ objInstances := g.World.FindObjInstances(p.RoomID, itemBName)
+ if len(objInstances) > 0 {
+ objSt := &objInstances[0]
+ def, _ := g.ObjectStore.Load(objSt.DefID)
+ if def == nil {
+ g.StartAction(sess, "use", itemBName)
+ return
+ }
+
+ stationIDs := []string{def.ID}
+ recipes, err := g.RecipeStore.FindByStation(itemAID, stationIDs, "")
+ if err == nil && len(recipes) > 0 {
+ if len(recipes) == 1 {
+ g.startCook(sess, p, &recipes[0], def.Name)
+ return
+ }
+ sess.PendingRecipeItem = itemAID
+ sess.PendingCookMenu = recipesToMenuData(recipes)
+ sess.State = net.StateCookRecipe
+ sess.WriteLine(fmt.Sprintf("\nWhat would you like to make with %s on the %s?", itemAName, def.Name))
+ for i, r := range recipes {
+ sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r)))
+ }
+ return
+ }
+
+ g.StartAction(sess, "use", itemBName)
+ return
+ }
+
+ matchesB = g.findInventoryMatches(itemBName, p)
+ if len(matchesB) > 0 {
+ uniqueB := uniqueItemNames(matchesB)
+ if len(uniqueB) > 1 {
+ sess.WriteLine("Which '" + itemBName + "'?")
+ for _, name := range uniqueB {
+ sess.WriteLine(fmt.Sprintf(" - %s", name))
+ }
+ return
+ }
+
+ itemBID := matchesB[0].ID
+
+ matched := g.findCombineResults(sess, p, itemAID, itemBID)
+ if len(matched) == 1 {
+ g.produceCombined(sess, p, matched[0])
+ return
+ }
+ if len(matched) > 1 {
+ sess.PendingCookMenu = combineMenuData(matched)
+ sess.State = net.StateCookRecipe
+ sess.WriteLine("\nWhat would you like to make?")
+ for i, def := range matched {
+ sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.itemColorize(sess, def, def.Name)))
+ }
+ return
+ }
+
+ sess.WriteLine("You can't combine those items.")
+ return
+ }
+
+ sess.WriteLine(fmt.Sprintf("There's no '%s' here to use that on.", itemBName))
+}
+
+func (g *Game) findCombineResults(sess *net.Session, p *player.Player, itemAID, itemBID string) []*object.ItemDef {
+ items, err := g.ItemStore.LoadAll()
+ if err != nil {
+ return nil
+ }
+ var matched []*object.ItemDef
+ for _, def := range items {
+ if len(def.MadeFrom) == 0 {
+ continue
+ }
+ aEntry := -1
+ bEntry := -1
+ for ei, entry := range def.MadeFrom {
+ for _, id := range entry.Items {
+ if id == itemAID && aEntry < 0 {
+ aEntry = ei
+ }
+ if id == itemBID && bEntry < 0 {
+ bEntry = ei
+ }
+ }
+ }
+ if aEntry >= 0 && bEntry >= 0 && aEntry != bEntry && madeFromItemsAvailable(p, def.MadeFrom) {
+ matched = append(matched, def)
+ }
+ }
+ return matched
+}
+
+func (g *Game) produceCombined(sess *net.Session, p *player.Player, def *object.ItemDef) {
+ freeSlot := p.FirstFreeSlot()
+ if freeSlot == -1 {
+ sess.WriteLine("Your inventory is too full.")
+ return
+ }
+ for _, entry := range def.MadeFrom {
+ for _, id := range entry.Items {
+ if p.HasItem(id) {
+ p.RemoveItem(id, entry.Qty)
+ break
+ }
+ }
+ }
+ p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: def.ID, Quantity: 1})
+ g.AccountStore.SaveCharacter(p)
+ sess.WriteLine(fmt.Sprintf("You combine the ingredients and create %s.", g.itemColorize(sess, def, def.Name)))
+}
+
+func combineMenuData(defs []*object.ItemDef) []map[string]string {
+ out := make([]map[string]string, len(defs))
+ for i, d := range defs {
+ out[i] = map[string]string{"combine_item": d.ID}
+ }
+ return out
+}
+
+func madeFromItemsAvailable(p *player.Player, madeFrom []object.MadeFromEntry) bool {
+ for _, entry := range madeFrom {
+ found := false
+ for _, id := range entry.Items {
+ if p.HasItem(id) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/game/game.go b/internal/game/game.go
index 456419f..33dd0d1 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -40,6 +40,7 @@ type Game struct {
AccountStore *player.AccountStore
MobStore *world.MobStore
BehaviorStore *action.Store
+ RecipeStore *action.RecipeStore
Hub *net.Hub
Ticks *engine.Engine
WorldFlags map[string]any
@@ -51,6 +52,7 @@ type Game struct {
combatPadWidth int
freeQueue map[string][]QueuedCommand
activeQueue map[string]*QueuedCommand
+ consumeQueue map[string]*QueuedCommand
pendingDepletions []pendingDepletion
}
@@ -62,6 +64,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
AccountStore: player.NewAccountStore(dataDir),
MobStore: world.NewMobStore(dataDir),
BehaviorStore: action.NewStore(dataDir),
+ RecipeStore: action.NewRecipeStore(dataDir),
Ticks: engine.New(),
WorldFlags: make(map[string]any),
ColorConfig: colorConfig,
@@ -70,6 +73,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig) *Game {
loggedInChars: make(map[string]*net.Session),
freeQueue: make(map[string][]QueuedCommand),
activeQueue: make(map[string]*QueuedCommand),
+ consumeQueue: make(map[string]*QueuedCommand),
pendingDepletions: nil,
}
}
@@ -118,6 +122,8 @@ func (g *Game) HandleSession(sess *net.Session, input string) {
g.handleTalkInput(sess, input)
case net.StateDropAllConfirm:
g.handleDropAllConfirm(sess, input)
+ case net.StateCookRecipe:
+ g.handleCookRecipe(sess, input)
}
}
@@ -134,8 +140,10 @@ func classifyCommand(cmd string) CommandClass {
"attack", "kill",
"north", "n", "south", "s", "east", "e",
"west", "w", "up", "u", "down", "d",
- "quit", "use", "burn", "stoke", "search", "walk":
+ "quit", "use", "burn", "stoke", "search", "walk", "cook":
return ClassActive
+ case "eat":
+ return ClassFree
}
if _, ok := verbAliases[cmd]; ok {
return ClassActive
@@ -314,7 +322,7 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
} else {
g.doHelp(sess, strings.Join(args, " "))
}
- case "mine", "chop", "fish", "cut", "use", "pull", "push":
+ case "mine", "chop", "fish", "cut", "pull", "push":
g.CancelAction(p)
if len(args) == 0 {
target := g.resolveDefaultTarget(p.RoomID, cmd)
@@ -328,6 +336,15 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI
g.StartAction(sess, cmd, strings.Join(args, " "))
return
}
+ case "use":
+ g.doUse(sess, strings.Join(args, " "))
+ return
+ case "cook":
+ g.doCook(sess, strings.Join(args, " "))
+ return
+ case "eat":
+ g.doEat(sess, strings.Join(args, " "))
+ return
case "talk", "speak", "ask":
g.CancelAction(p)
if len(args) == 0 {
@@ -413,7 +430,7 @@ func (g *Game) ProcessQueuedCommands() {
}
switch as.Type {
case ActionGathering, ActionCombating, ActionUsing, ActionTalking,
- ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking:
+ ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking:
default:
p.ActionState = nil
}
@@ -455,7 +472,8 @@ func (g *Game) ProcessQueuedCommands() {
g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args)
}
isBusy := p.Action != nil || len(p.WalkSequence) > 0 || combat.GetCombat(p.Name) != nil
- if wasBusy || !isBusy {
+ _, isResting := g.restTimers[p.Name]
+ if !isResting && (wasBusy || !isBusy) {
g.writePrompt(qc.Session)
}
}
diff --git a/internal/game/help.go b/internal/game/help.go
index 4d716ad..d3f34c3 100644
--- a/internal/game/help.go
+++ b/internal/game/help.go
@@ -29,8 +29,10 @@ var commandList = []cmdEntry{
{"burn", "Active", "Start a fire"},
{"chop / cut", "Active", "Chop trees (Woodcutting)"},
{"color / colors", "Instant", "Customize display colors"},
+ {"cook", "Active", "Cook raw food on a fire or range"},
{"description / desc", "Instant", "Set your character description"},
{"drop", "Active", "Drop items to the ground"},
+ {"eat", "Free", "Eat food to restore hitpoints"},
{"equipment / eq", "Instant", "Show equipped items"},
{"exits", "Instant", "List available exits"},
{"fish", "Active", "Fish at fishing spots (Fishing)"},
diff --git a/internal/game/tick.go b/internal/game/tick.go
index f326ad8..7567ca7 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -198,3 +198,18 @@ func (g *Game) legalMobExits(inst *world.MobInstance) []int {
}
return legal
}
+
+func (g *Game) ConsumeTick() {
+ if g.Hub == nil {
+ return
+ }
+ for _, sess := range g.Hub.AllSessions() {
+ p, ok := sess.Player.(*player.Player)
+ if !ok || p == nil {
+ continue
+ }
+ if p.ConsumeCooldown > 0 {
+ p.ConsumeCooldown--
+ }
+ }
+}
diff --git a/internal/net/server.go b/internal/net/server.go
index d909cc2..1b6c99b 100644
--- a/internal/net/server.go
+++ b/internal/net/server.go
@@ -29,17 +29,20 @@ const (
StateChangeDescription
StateTalk
StateDropAllConfirm
+ StateCookRecipe
)
type Session struct {
- Conn Conn
- State SessionState
- Account *AccountEntry
- Player interface{}
- PendingChar string
- PendingPass string
- Disconnecting bool
- DisconnectTicks int
+ Conn Conn
+ State SessionState
+ Account *AccountEntry
+ Player interface{}
+ PendingChar string
+ PendingPass string
+ PendingRecipeItem string
+ PendingCookMenu []map[string]string
+ Disconnecting bool
+ DisconnectTicks int
}
type AccountEntry struct {
diff --git a/internal/object/item.go b/internal/object/item.go
index ecdb1e9..7b2a107 100644
--- a/internal/object/item.go
+++ b/internal/object/item.go
@@ -34,7 +34,6 @@ type ItemDef struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Color string `yaml:"color"`
- Aliases []string `yaml:"aliases"`
Description string `yaml:"description"`
Value int `yaml:"value"`
Stackable bool `yaml:"stackable"`
@@ -53,6 +52,14 @@ type ItemDef struct {
SearchMiscTable string `yaml:"search_misc_table"`
SearchTicks float64 `yaml:"search_ticks"`
SearchMessage string `yaml:"search_message"`
+ HealValue int `yaml:"heal_value"`
+ EatMessage string `yaml:"eat_message"`
+ MadeFrom []MadeFromEntry `yaml:"made_from,omitempty"`
+}
+
+type MadeFromEntry struct {
+ Items []string `yaml:"items"`
+ Qty int `yaml:"qty"`
}
type ItemStats struct {
@@ -71,18 +78,5 @@ func (d *ItemDef) MatchesName(input string) bool {
if strings.ToLower(d.Name) == lower {
return true
}
- for _, alias := range d.Aliases {
- if strings.ToLower(alias) == lower {
- return true
- }
- }
- if world.WordPrefixMatch(lower, d.Name) {
- return true
- }
- for _, alias := range d.Aliases {
- if world.WordPrefixMatch(lower, alias) {
- return true
- }
- }
- return false
+ return world.WordPrefixMatch(lower, d.Name)
}
diff --git a/internal/object/item_store.go b/internal/object/item_store.go
index 6d62143..9e932ba 100644
--- a/internal/object/item_store.go
+++ b/internal/object/item_store.go
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "strings"
"gopkg.in/yaml.v3"
)
@@ -36,3 +37,24 @@ func (s *ItemStore) Load(id string) (*ItemDef, error) {
s.cache[id] = &def
return &def, nil
}
+
+func (s *ItemStore) LoadAll() ([]*ItemDef, error) {
+ dir := filepath.Join(s.dataDir, "items")
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+ var defs []*ItemDef
+ for _, e := range entries {
+ if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" {
+ continue
+ }
+ id := strings.TrimSuffix(e.Name(), ".yaml")
+ def, err := s.Load(id)
+ if err != nil {
+ continue
+ }
+ defs = append(defs, def)
+ }
+ return defs, nil
+}
diff --git a/internal/player/player.go b/internal/player/player.go
index 4a6f065..e0c7ab9 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -156,6 +156,7 @@ type Player struct {
Action *action.Action `yaml:"-"`
ActionState any `yaml:"-"`
WalkSequence []string `yaml:"-"`
+ ConsumeCooldown int `yaml:"-"`
}
func (p *Player) OptionBool(name string) bool {
diff --git a/worldbuilding_guide/README.md b/worldbuilding_guide/README.md
new file mode 100644
index 0000000..8555e49
--- /dev/null
+++ b/worldbuilding_guide/README.md
@@ -0,0 +1,25 @@
+# World Building Guide
+
+Third Collapse is data-driven. Everything — rooms, items, mobs, objects, behaviors, drop tables, and recipes — is defined in YAML files under `data/`. No code changes needed to build a world.
+
+## Files
+
+| Document | Covers |
+|----------|--------|
+| [Rooms](rooms.md) | Room definitions, exits (simple and conditional), spawns, placed objects and mobs, on_enter scripts |
+| [Items](items.md) | Item definitions, stats, equip slots, weapon types, tool types, firemaking, food/healing |
+| [Mobs](mobs.md) | Mob definitions, combat stats, drops, behavior, idle descriptions |
+| [Objects](objects.md) | Interactive objects, behaviors, hidden objects, in-room descriptions |
+| [Behaviors](behaviors.md) | Gather, use, talk, toggle behaviors with full YAML reference |
+| [Recipes](recipes.md) | Recipe system for cooking, crafting, smithing — station and item-on-item |
+| [Conditions](conditions.md) | All condition types: flag, player_flag, has_item, all_of, any_of, not |
+| [Drop Tables](drops.md) | Shared weighted drop tables for mob loot and search tables |
+| [State](state.md) | World flags vs player flags — global vs per-character state |
+| [Doors](doors.md) | Door examples with world flags and player flags |
+| [Quests](quests.md) | Complete multi-room quest example with talk behaviors |
+| [Wandering Objects](wandering_objects.md) | Objects that teleport between rooms on a timer |
+| [Wandering Mobs](wandering_mobs.md) | Mobs that wander through legal exits |
+| [Toggles](toggles.md) | Player toggle options (tiny_map, xp_drops, automap, etc.) |
+| [Search](search.md) | The search command and searchable items |
+| [Hidden Objects](hidden_objects.md) | Hidden interactive objects |
+| [Tips](tips.md) | Practical tips for building a world |
diff --git a/worldbuilding_guide/behaviors.md b/worldbuilding_guide/behaviors.md
new file mode 100644
index 0000000..a275c60
--- /dev/null
+++ b/worldbuilding_guide/behaviors.md
@@ -0,0 +1,264 @@
+## Behaviors
+
+### Gather (mining, fishing, woodcutting)
+
+Mining — per-drop depletion:
+```yaml
+id: mine_copper
+type: gather
+skill: mining
+level: 1
+xp: 17 # XP awarded per successful gather
+base_wait: 8 # ticks between attempts
+tool: pickaxe # requires item with tool_type: pickaxe
+success:
+ base: 0.40 # 40% base chance
+ per_level: 0.01 # +1% per level above requirement
+ cap: 0.95 # 95% max
+gather_message: "You swing your pickaxe at the rock..."
+fail_message: "You chip away but get nothing useful."
+drops:
+ - item_id: copper_ore
+ weight: 90 # 90% chance when roll succeeds
+ depletes: true # rock becomes depleted after this drop
+ message: "You manage to mine some copper ore."
+ - table: gem_table # reference a shared drop table
+ weight: 10
+ depletes: false # gem drops don't deplete the rock
+ message: "You spot a glint of something valuable!"
+respawn_timer: 50 # ticks until rock respawns
+respawn_message: "You see more ore in the rock."
+respawn_broadcast: "A glint of copper catches your eye from some {name}."
+```
+
+Non-depleting gather (fishing):
+```yaml
+id: fish_trout
+type: gather
+skill: fishing
+level: 1
+xp: 10
+base_wait: 4
+tool: fishing_rod
+success:
+ base: 0.30
+ per_level: 0.01
+ cap: 0.90
+gather_message: "You cast your line into the water..."
+fail_message: "Nothing seems to bite."
+drops:
+ - item_id: raw_trout
+ weight: 100
+ depletes: false # never depletes
+ message: "You catch a trout!"
+```
+
+Woodcutting with shared depletion and bird's nests:
+```yaml
+id: chop_oak
+type: gather
+skill: woodcutting
+level: 15
+xp: 37
+base_wait: 6
+tool: axe
+success:
+ base: 0.40
+ per_level: 0.01
+ cap: 0.90
+gather_message: "You swing your axe at the oak tree..."
+fail_message: "You swing but get no logs."
+drops:
+ - item_id: oak_logs
+ weight: 100
+ depletes: false # depletion is timer-based (deplete_timer)
+ message: "You get some oak logs."
+respawn_timer: 14 # ticks until tree respawns after being cut down
+deplete_timer: 45 # max ticks before next gather depletes (counts down while chopping)
+nest_chance: 256 # 1/256 chance for a bird's nest on each successful gather
+respawn_message: "A new oak sapling grows in its place."
+respawn_broadcast: "An {name} grows back."
+```
+
+Regular tree — always depletes on first gather, no shared timer, no nests:
+```yaml
+id: chop_tree
+type: gather
+skill: woodcutting
+level: 1
+xp: 25
+base_wait: 4
+tool: axe
+success:
+ base: 0.50
+ per_level: 0.01
+ cap: 0.95
+gather_message: "You swing your axe at the tree..."
+fail_message: "You swing but get no logs."
+drops:
+ - item_id: logs
+ weight: 100
+ depletes: true # regular tree depletes on first successful gather
+ message: "You get some logs."
+respawn_timer: 80
+respawn_message: "A new tree grows in its place."
+respawn_broadcast: "A {name} grows back."
+```
+
+#### Shared depletion explained
+
+When `deplete_timer > 0`, the tree has a shared despawn timer:
+- The timer starts at `deplete_timer` max when the first player begins chopping.
+- Each tick, if anyone is chopping, the timer counts down.
+- When the timer reaches 0, the NEXT successful gather depletes the tree.
+- If no one is chopping and the tree isn't depleted, the timer ticks back UP.
+- All players chopping the same tree are interrupted when it depletes.
+
+Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks.
+
+#### Bird's nests
+
+When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop
+a bird's nest. The nest goes to inventory (or to the ground if inventory is full).
+Use the `search` command to open nests — they roll on the `birds_nest_drop` table.
+
+#### XP drops
+
+When `xp > 0`, the gather awards XP on each successful drop. If the player's `xpdrops`
+toggle is on, the output includes the XP gain: `(+37xp wct)`.
+
+### Talk (dialog trees)
+
+Full conversation with conditions, actions, and player flag tracking:
+```yaml
+id: guard_talk
+type: talk
+nodes:
+ start:
+ message: "\"Halt! This area is restricted.\""
+ options:
+ - text: "\"What's behind that gate?\""
+ goto: about_gate
+ - text: "\"I have copper ore.\"" # only shows if player has ore
+ goto: trade_ore
+ condition:
+ has_item: copper_ore
+ - text: "\"I have a pass.\"" # only shows if player earned a pass
+ goto: has_pass
+ condition:
+ player_flag: got_pass
+ value: true
+ - text: "\"Goodbye.\""
+ end: true
+
+ about_gate:
+ message: "\"Bring me some copper ore and I'll stamp you a pass.\""
+ action:
+ set_player_flags: # player-local: only this player
+ talked_to_guard: true
+ options:
+ - text: "\"I'll be back.\""
+ end: true
+ - text: "\"I have some right here.\""
+ goto: trade_ore
+ condition:
+ has_item: copper_ore
+
+ trade_ore:
+ message: "\"Good quality ore.\" He stamps a pass and hands it to you."
+ action:
+ take_item: copper_ore # removes 1 copper ore
+ give_item: pass_stub # gives pass stub
+ set_player_flags:
+ got_pass: true # player now "has a pass"
+ options:
+ - text: "\"Thanks.\""
+ end: true
+
+ has_pass:
+ message: "\"Alright, I'll open the gate for you.\""
+ action:
+ set_flags: # WORLD flag: gate opens for everyone
+ gate_open: true
+ options:
+ - text: "\"Thanks.\""
+ end: true
+```
+
+#### Node action reference
+
+| Field | Effect |
+|---|---|
+| `set_flags` | Sets world flags (global, shared by all players) |
+| `set_player_flags` | Sets player-local flags (per-character, quest progress) |
+| `give_item` | Gives an item to the player's inventory |
+| `take_item` | Removes an item from the player's inventory |
+| `teleport` | Moves the player to a room ID |
+| `heal` | Restores that many hitpoints |
+
+All fields in a single action are processed together — you can give an item, take an item, set flags, and heal all in one node.
+
+Example — quest completion:
+```yaml
+action:
+ take_item: dragon_head
+ give_item: dragon_slayer_medal
+ set_player_flags:
+ dragon_quest: complete
+ dragon_slain: true
+ heal: 99
+ teleport: 1 # return to town
+```
+
+### Toggle (levers, switches, gates)
+
+Simple toggle that sets a world flag:
+```yaml
+id: iron_gate_toggle
+type: toggle
+message: "You push the heavy iron gate open."
+set_flags:
+ gate_open: true
+check: # only works when gate is closed
+ flag: gate_open
+ value: true
+ not: true
+```
+
+Lever that toggles between two states:
+```yaml
+id: bridge_lever
+type: toggle
+message: "You pull the lever. Mechanisms groan somewhere in the distance."
+set_flags:
+ bridge_extended: true
+check:
+ flag: bridge_extended
+ value: true
+ not: true
+```
+
+### Use (crafting stations)
+
+```yaml
+id: smelt_copper
+type: use
+message: "You place the ore in the furnace..."
+wait: 4 # ticks between crafts
+consume: # items consumed per craft
+ copper_ore: 1
+reward: # item produced
+ item_id: copper_bar
+ quantity: 1
+fail_message: "The ore crumbles to dust."
+success:
+ base: 0.60
+ per_level: 0.01
+ cap: 0.95
+skill: smithing
+level: 1
+xp: 15 # XP awarded per successful craft
+```
+
+---
+
diff --git a/worldbuilding_guide/conditions.md b/worldbuilding_guide/conditions.md
new file mode 100644
index 0000000..ce9f74c
--- /dev/null
+++ b/worldbuilding_guide/conditions.md
@@ -0,0 +1,66 @@
+## Conditions Reference
+
+Conditions are used in talk options, exit gates, on-enter scripts, and toggle checks.
+
+### Simple conditions
+
+```yaml
+# Check a world flag
+condition:
+ flag: gate_open
+ value: true
+
+# Check a world flag is NOT set
+condition:
+ flag: gate_open
+ not: true
+
+# Check a player flag
+condition:
+ player_flag: finished_tutorial
+ value: true
+
+# Check if player has an item
+condition:
+ has_item: bronze_key
+
+# Check if player does NOT have an item
+condition:
+ has_item: bronze_key
+ not: true
+```
+
+### Compound conditions
+
+All must pass:
+```yaml
+condition:
+ all_of:
+ - flag: gate_open
+ value: true
+ - has_item: pass_stub
+```
+
+Any one must pass:
+```yaml
+condition:
+ any_of:
+ - has_item: bronze_key
+ - has_item: iron_key
+ - player_flag: master_of_unlocking
+ value: true
+```
+
+Nested compounds:
+```yaml
+condition:
+ all_of:
+ - player_flag: quest_started
+ value: true
+ - any_of:
+ - has_item: wolf_pelt
+ - has_item: bear_pelt
+```
+
+---
+
diff --git a/worldbuilding_guide/doors.md b/worldbuilding_guide/doors.md
new file mode 100644
index 0000000..afe4556
--- /dev/null
+++ b/worldbuilding_guide/doors.md
@@ -0,0 +1,102 @@
+## Global vs Player State: Door Examples
+
+### Example A: Door with a button in another room (GLOBAL)
+
+A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is open for everyone.
+
+**Button object** (`data/objects/door_button.yaml`):
+```yaml
+id: door_button
+name: stone button
+behavior: button_toggle
+hidden: true
+```
+
+**Button behavior** (`data/behaviors/button_toggle.yaml`):
+```yaml
+id: button_toggle
+type: toggle
+message: "You press the stone button. You hear grinding stone in the distance."
+set_flags:
+ secret_door_open: true # WORLD flag
+check:
+ flag: secret_door_open
+ not: true # only works when door is closed
+```
+
+**Room 3** — contains the button:
+```yaml
+id: 3
+name: "Button Chamber"
+description: "A small stone chamber. A button protrudes from the east wall."
+exits:
+ south: 1
+objects:
+ - id: door_button
+```
+
+**Room 7** — contains the door:
+```yaml
+id: 7
+name: "Hidden Passage"
+description: "A dusty corridor. A heavy stone door blocks the way north."
+exits:
+ south: 2
+ north:
+ room: 8
+ condition:
+ flag: secret_door_open # checks WORLD flag
+ value: true
+ blocked_message: "A heavy stone door blocks the way."
+objects:
+ - id: stone_door
+ hidden: true
+```
+
+### Example B: Key-locked door (PLAYER-LOCAL)
+
+A locked door that only opens for a player carrying the key. Each player must find their own key.
+
+**Key item** (`data/items/rusty_key.yaml`):
+```yaml
+id: rusty_key
+name: rusty key
+description: "An old iron key, still functional."
+value: 0
+stackable: false
+```
+
+**Room 5** — locked door:
+```yaml
+id: 5
+name: "Locked Storage"
+description: "A small storage room. The way east is blocked by a locked iron door."
+exits:
+ west: 2
+ east:
+ room: 6
+ condition:
+ has_item: rusty_key # checks PLAYER inventory
+ blocked_message: "The iron door is locked. You need a key."
+objects:
+ - id: iron_door
+ hidden: true
+```
+
+**Room 6** — the other side (no key needed to exit):
+```yaml
+id: 6
+name: "Storage Closet"
+description: "Shelves of dusty crates."
+exits:
+ west: 5 # exit back — no condition
+spawns:
+ - item_id: uncut_ruby
+ quantity: 1
+ respawn_ticks: 500
+```
+
+Key difference: the button door uses `flag` (shared state — one player presses, everyone benefits), the key door uses `has_item` (per-player inventory check — each player needs their own key).
+
+---
+
diff --git a/worldbuilding_guide/drops.md b/worldbuilding_guide/drops.md
new file mode 100644
index 0000000..e5a17ff
--- /dev/null
+++ b/worldbuilding_guide/drops.md
@@ -0,0 +1,52 @@
+## Drop Tables
+
+Shared drop tables can be referenced by multiple behaviors:
+```yaml
+# data/drops/gem_table.yaml
+id: gem_table
+drops:
+ - item_id: uncut_sapphire
+ weight: 47
+ - item_id: uncut_emerald
+ weight: 16
+ - item_id: uncut_ruby
+ weight: 4
+ - item_id: uncut_diamond
+ weight: 1
+```
+
+Bird's nest drop table:
+```yaml
+# data/drops/birds_nest_drop.yaml
+id: birds_nest_drop
+drops:
+ - item_id: credits
+ weight: 50
+ quantity: 200
+ - item_id: credits
+ weight: 30
+ quantity: 500
+ - item_id: credits
+ weight: 15
+ quantity: 1000
+ - item_id: credits
+ weight: 4
+ quantity: 3000
+ - item_id: credits
+ weight: 1
+ quantity: 10000
+```
+
+Referenced from a gather behavior:
+```yaml
+drops:
+ - item_id: copper_ore
+ weight: 90
+ depletes: true
+ - table: gem_table # pulls from data/drops/gem_table.yaml
+ weight: 10
+ depletes: false
+```
+
+---
+
diff --git a/worldbuilding_guide/hidden_objects.md b/worldbuilding_guide/hidden_objects.md
new file mode 100644
index 0000000..3cb8a1e
--- /dev/null
+++ b/worldbuilding_guide/hidden_objects.md
@@ -0,0 +1,21 @@
+## Hidden Objects
+
+Objects with `hidden: true` don't appear in the room's object listing. Players discover them by reading room descriptions or trying commands. The object is still fully interactable — `push gate`, `look gate`, etc.
+
+```yaml
+# data/objects/secret_lever.yaml
+id: secret_lever
+name: stone lever
+behavior: secret_toggle
+hidden: true
+props:
+ description: "A cleverly concealed lever behind a loose stone."
+```
+
+Room description hints at it:
+```yaml
+description: "A dusty corridor. One of the wall stones looks slightly out of place."
+```
+
+---
+
diff --git a/worldbuilding_guide/index.md b/worldbuilding_guide/index.md
new file mode 100644
index 0000000..e19c207
--- /dev/null
+++ b/worldbuilding_guide/index.md
@@ -0,0 +1,10 @@
+## Quick Reference
+
+| What you want | Where to put it |
+|---|---|
+| A room | `data/rooms/<id>.yaml` |
+| An item (sword, ore, key) | `data/items/<id>.yaml` |
+| A mob (NPC, monster) | `data/mobs/<id>.yaml` |
+| An interactive object (rock, lever, door) | `data/objects/<id>.yaml` |
+| A behavior (mining, dialog, toggle) | `data/behaviors/<id>.yaml` |
+| A shared drop table | `data/drops/<id>.yaml` |
diff --git a/worldbuilding_guide/items.md b/worldbuilding_guide/items.md
new file mode 100644
index 0000000..73f497b
--- /dev/null
+++ b/worldbuilding_guide/items.md
@@ -0,0 +1,68 @@
+## Items
+
+```yaml
+id: bronze_pickaxe
+name: bronze pickaxe
+aliases: ["pick", "pickaxe"]
+description: "A sturdy bronze pickaxe."
+value: 10
+stackable: false
+equip_slot: main_hand # optional — where it equips
+weapon_type: melee # optional — melee or ranged
+stats: # optional — combat bonuses
+ attack_bonus: 2
+ strength_bonus: 1
+speed: 5 # ticks between attacks
+tool_type: pickaxe # used by gather behaviors that require "tool: pickaxe"
+tool_speed: 2 # reduces gather wait time
+```
+
+Key item (quest token, not equippable):
+```yaml
+id: pass_stub
+name: pass stub
+description: "A crumpled slip of paper stamped with the guard's seal."
+value: 0
+stackable: false
+```
+
+---
+
+## Firemaking
+
+```yaml
+id: logs
+name: logs
+fire_level: 1 # firemaking level required
+fire_xp: 40 # XP for burning/stoking
+burn_ticks: 24 # how long the fire burns
+```
+
+## Food / Healing
+
+```yaml
+id: bread
+name: bread
+color: "fg=bright_yellow"
+description: "A fresh loaf of bread, still warm from the oven."
+value: 5
+heal_value: 5 # positive = heal, negative = damage
+eat_message: "You eat the bread. Warm and satisfying."
+```
+
+## MadeFrom — Item Combinations
+
+Define what items combine to make this item. Used by the `use` command.
+
+```yaml
+id: bread_dough
+name: bread dough
+color: "fg=bright_yellow"
+made_from:
+ - items: [pot_of_flour]
+ qty: 1
+ - items: [bucket_of_water, pitcher_of_water]
+ qty: 1
+```
+
+Each entry is an ingredient slot. Multiple `items` means any of them works. The first matching item found in inventory is consumed. See also `recipes.md` for station-based recipes.
diff --git a/worldbuilding_guide/mobs.md b/worldbuilding_guide/mobs.md
new file mode 100644
index 0000000..a025e86
--- /dev/null
+++ b/worldbuilding_guide/mobs.md
@@ -0,0 +1,54 @@
+## Mobs
+
+Basic combat mob:
+```yaml
+id: "man"
+name: "man"
+description: "A shabby-looking man."
+attack: 1
+strength: 1
+defense: 1
+hp: 7
+speed: 5
+aggressive: false
+respawn_ticks: 30
+drops:
+ remains: "bones" # always dropped on death
+ loot:
+ - item_id: "credits"
+ weight: 98
+ quantity: 10
+ - item_id: "credits"
+ weight: 2
+ quantity: 150
+idle_descriptions:
+ - "scribbles something in a small notebook"
+ - "gazes skyward at the clouds"
+combat_descriptions:
+ - "is engaged in a fight to the death with %s"
+```
+
+Mob with a behavior — can be talked to, toggled, etc:
+```yaml
+id: "guard"
+name: "Guard"
+behavior: guard_talk # links to data/behaviors/guard_talk.yaml
+unique: true # displays as "Guard" not "a guard"
+protected: true # cannot be attacked
+attack: 5
+strength: 5
+defense: 5
+hp: 30
+speed: 5
+aggressive: false
+respawn_ticks: 60
+idle_descriptions:
+ - "scans the area with a watchful eye"
+ - "adjusts the grip on his weapon"
+```
+
+Note: `wander_rooms` and `wander_interval` are NOT set on the mob definition. Wander config
+is per-instance in the room YAML (see Rooms > Mobs section above).
+
+---
+
diff --git a/worldbuilding_guide/objects.md b/worldbuilding_guide/objects.md
new file mode 100644
index 0000000..94aead7
--- /dev/null
+++ b/worldbuilding_guide/objects.md
@@ -0,0 +1,37 @@
+## Objects
+
+Object defined — links to a behavior:
+```yaml
+id: copper_rock
+name: copper rock
+behavior: mine_copper
+```
+
+Tree object:
+```yaml
+id: oak_tree
+name: oak tree
+behavior: chop_oak
+```
+
+Hidden object — doesn't appear in room's object list, only discoverable via description or experimentation:
+```yaml
+id: iron_gate
+name: iron gate
+behavior: iron_gate_toggle
+hidden: true
+props:
+ description: "A heavy iron gate set into the north wall."
+```
+
+Decorative object (no behavior):
+```yaml
+id: lumby_fountain
+name: town fountain
+behavior: ""
+props:
+ description: "Clear water sparkles in the sunlight."
+```
+
+---
+
diff --git a/worldbuilding_guide/quests.md b/worldbuilding_guide/quests.md
new file mode 100644
index 0000000..443aa6b
--- /dev/null
+++ b/worldbuilding_guide/quests.md
@@ -0,0 +1,107 @@
+## Complete Quest Example: "Clear the Rats"
+
+### 1. Quest giver mob (`data/mobs/quest_giver.yaml`)
+```yaml
+id: "quest_giver"
+name: "Elder"
+behavior: rat_quest_talk
+unique: true
+protected: true
+attack: 1
+strength: 1
+defense: 1
+hp: 20
+speed: 5
+aggressive: false
+idle_descriptions:
+ - "mutters about the rat infestation"
+```
+
+### 2. Dialog behavior (`data/behaviors/rat_quest_talk.yaml`)
+```yaml
+id: rat_quest_talk
+type: talk
+nodes:
+ start:
+ message: "\"Rats! Rats everywhere in the cellar. Clear them out and I'll reward you.\""
+ options:
+ - text: "\"I'll handle it.\""
+ goto: accept_quest
+ condition:
+ player_flag: rat_quest
+ not: true
+ - text: "\"I killed the rats.\""
+ goto: turn_in
+ condition:
+ player_flag: rat_quest
+ value: started
+ - text: "\"Goodbye.\""
+ end: true
+
+ accept_quest:
+ message: "\"Good lad. The cellar is west of here. Come back when they're dead.\""
+ action:
+ set_player_flags:
+ rat_quest: started
+ options:
+ - text: "\"On my way.\""
+ end: true
+
+ turn_in:
+ message: "\"You did it! The village owes you a debt. Here — take this.\""
+ action:
+ give_item: rusty_sword
+ set_player_flags:
+ rat_quest: complete
+ heal: 10
+ options:
+ - text: "\"Thanks!\""
+ end: true
+```
+
+### 3. Rat mobs (`data/mobs/rat.yaml`)
+```yaml
+id: "rat"
+name: "giant rat"
+attack: 2
+strength: 1
+defense: 1
+hp: 3
+speed: 4
+aggressive: true
+respawn_ticks: 60
+drops:
+ remains: "rat bones"
+```
+
+### 4. Cellar room (`data/rooms/20.yaml`)
+```yaml
+id: 20
+name: "Cellar"
+description: "A damp, dark cellar. The floor scuttles with movement."
+exits:
+ east: 1
+mobs:
+ - "rat"
+ - "rat"
+ - "rat"
+ - "rat"
+ - "rat"
+```
+
+### 5. Room 1 with quest giver (`data/rooms/1.yaml`)
+```yaml
+id: 1
+name: "Town Square"
+description: "Cobblestone paths lead in all directions. The Elder stands near the fountain."
+exits:
+ west: 20
+ north: 2
+mobs:
+ - "quest_giver"
+objects:
+ - id: lumby_fountain
+```
+
+---
+
diff --git a/worldbuilding_guide/recipes.md b/worldbuilding_guide/recipes.md
new file mode 100644
index 0000000..f40a9a7
--- /dev/null
+++ b/worldbuilding_guide/recipes.md
@@ -0,0 +1,112 @@
+# Third Collapse - World Building Guide
+
+## Recipes
+
+Recipes define how items are processed on stations to produce new items. They are the foundation for cooking, smithing, crafting, and fletching.
+
+Recipe files live in `data/recipes/<id>.yaml`.
+
+### Recipe vs MadeFrom
+
+- **Recipes** (`data/recipes/`) use a station (fire, range, furnace, anvil, etc.). The player must be near the station object.
+- **MadeFrom** (on the item YAML directly) is for item-on-item combinations that need no station. The player combines ingredients from their inventory.
+
+### Recipe YAML Reference
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | string | Unique recipe identifier |
+| `type` | string | Recipe type (cooking, smithing, crafting, etc.) |
+| `skill` | string | Skill checked for success |
+| `level` | int | Required skill level |
+| `xp` | int | XP awarded on success |
+| `wait` | float64 | Ticks per craft cycle (default 6) |
+| `station` | []string | Station object IDs required |
+| `consume` | []ConsumeEntry | Ingredients consumed (see below) |
+| `output` | string | ItemID produced on success |
+| `fail` | string | ItemID produced on failure (optional) |
+| `message` | string | Success message |
+| `fail_message` | string | Failure message |
+| `success` | SuccessFormula | Optional skill check formula (see Behaviors) |
+
+The recipe's name in menus comes from the output item's `name` field, colored using the output item's `color`.
+
+### ConsumeEntry — Multi-Item Ingredient Slots
+
+Each consume entry defines an ingredient slot with multiple valid items. The first matching item found in the player's inventory is consumed.
+
+```yaml
+consume:
+ - items: [item_id, alternative_id, ...]
+ qty: 1
+```
+
+### Station-based recipe (cooking on a fire)
+
+```yaml
+id: cook_trout
+type: cooking
+skill: cooking
+level: 15
+xp: 70
+wait: 6
+station: [fire, cooking_range]
+consume:
+ - items: [raw_trout]
+ qty: 1
+output: trout
+fail: burnt_fish
+message: "Cooked to perfection. It looks great!"
+fail_message: "You accidentally burn the trout."
+```
+
+### Station-specific recipe (range only, not fire)
+
+```yaml
+id: cook_bread
+type: cooking
+skill: cooking
+level: 1
+xp: 40
+wait: 6
+station: [cooking_range]
+consume:
+ - items: [bread_dough]
+ qty: 1
+output: bread
+fail: burnt_meat
+message: "You bake the dough into a fresh loaf of bread."
+fail_message: "You burn the bread to a crisp."
+```
+
+### MadeFrom — Item-on-item combinations
+
+Item-on-item combinations are defined directly on the output item via the `made_from` field. No recipe file needed.
+
+```yaml
+# data/items/bread_dough.yaml
+id: bread_dough
+name: bread dough
+color: "fg=bright_yellow"
+made_from:
+ - items: [pot_of_flour]
+ qty: 1
+ - items: [bucket_of_water, pitcher_of_water, vial_of_water]
+ qty: 1
+```
+
+The player uses `use flour on water` to combine them. The system finds that `bread_dough` can be made from these ingredients and produces it.
+
+### Food/Healing Items
+
+Items with `heal_value` and `eat_message` can be consumed via the `eat` command.
+
+```yaml
+id: bread
+name: bread
+color: "fg=bright_yellow"
+description: "A fresh loaf of bread, still warm from the oven."
+value: 5
+heal_value: 5
+eat_message: "You eat the bread. Warm and satisfying."
+```
diff --git a/worldbuilding_guide/rooms.md b/worldbuilding_guide/rooms.md
new file mode 100644
index 0000000..95e1972
--- /dev/null
+++ b/worldbuilding_guide/rooms.md
@@ -0,0 +1,122 @@
+## Rooms
+
+Minimal room:
+```yaml
+id: 1
+name: "Town Square"
+description: "Cobblestone paths lead in all directions. A fountain gurgles peacefully."
+exits:
+ north: 2
+ west: 7
+ east: 3
+```
+
+### Exits — simple vs conditional
+
+Simple exit — always passable:
+```yaml
+exits:
+ north: 2
+```
+
+Conditional exit — blocked until a world flag is set:
+```yaml
+exits:
+ north:
+ room: 11
+ condition:
+ flag: gate_open
+ value: true
+ blocked_message: "A heavy iron gate blocks the way north."
+```
+
+Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state):
+```yaml
+exits:
+ east:
+ room: 12
+ condition:
+ player_flag: has_vault_key
+ value: true
+ blocked_message: "The vault door is locked. You need a key."
+```
+
+Conditional exit with compound condition — requires both a world flag AND a player flag:
+```yaml
+exits:
+ north:
+ room: 20
+ condition:
+ all_of:
+ - flag: bridge_repaired
+ value: true
+ - player_flag: paid_toll
+ value: true
+ blocked_message: "The bridge is out, and the toll collector blocks the path."
+```
+
+### Spawns — ground items that respawn
+
+```yaml
+spawns:
+ - item_id: bronze_pickaxe
+ quantity: 1
+ respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up
+ - item_id: copper_ore
+ quantity: 3
+ respawn_ticks: 50
+```
+
+### Mobs — NPCs placed in the room
+
+Simple string (no wandering):
+```yaml
+mobs:
+ - "newbie_trainer"
+ - "man"
+```
+
+With wander config per-instance:
+```yaml
+mobs:
+ - id: man
+ wander_interval: 10 # attempts to wander every 10 ticks
+ - id: man
+ wander_interval: 15
+ wander_rooms: [1, 4, 5] # optional — only exit to these rooms
+```
+
+Mob wander config lives in the room YAML, not in the mob definition. This keeps mobs generic
+so the same `man` can wander differently depending on where it's placed. Mobs wander through
+legal (unconditioned) room exits. If no legal exits exist, the mob stays still. Mobs with
+no `wander_interval` never wander.
+
+### Objects — interactive fixtures
+
+```yaml
+objects:
+ - id: copper_rock # simple placement
+ - id: copper_rock # second instance
+ - id: fishing_spot
+ wander_rooms: [7, 8, 9] # teleports between these rooms
+ wander_interval: 12 # every 12 ticks
+ - id: iron_gate # hidden object (see below)
+```
+
+### On-enter scripts — messages when a player arrives
+
+```yaml
+on_enter:
+ - message: "The guard barks: \"State your business!\""
+ condition:
+ player_flag: talked_to_guard
+ not: true # only first visit
+
+ - message: "The guard nods. \"Back again?\""
+ condition:
+ player_flag: talked_to_guard
+ value: true # subsequent visits
+```
+
+---
+
diff --git a/worldbuilding_guide/search.md b/worldbuilding_guide/search.md
new file mode 100644
index 0000000..e6e2a14
--- /dev/null
+++ b/worldbuilding_guide/search.md
@@ -0,0 +1,15 @@
+## The `search` Command
+
+Used to open searchable items in your inventory (bird's nests, etc.):
+```
+Usage: search <item>
+
+Example: search nest
+ search birds nest
+```
+
+Searches your inventory for the named item. If found, removes it and rolls on a drop table.
+Currently only bird's nests are searchable.
+
+---
+
diff --git a/worldbuilding_guide/state.md b/worldbuilding_guide/state.md
new file mode 100644
index 0000000..2eefcc4
--- /dev/null
+++ b/worldbuilding_guide/state.md
@@ -0,0 +1,8 @@
+## State: World vs Player
+
+**World flags** (`set_flags`, checked with `flag`) are shared by every player on the server. A door opened by one player is open for everyone. A lever pulled once changes the world for all.
+
+**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins.
+
+---
+
diff --git a/worldbuilding_guide/tips.md b/worldbuilding_guide/tips.md
new file mode 100644
index 0000000..451b9bc
--- /dev/null
+++ b/worldbuilding_guide/tips.md
@@ -0,0 +1,21 @@
+## Tips
+
+1. **Use player flags for quest progress, world flags for environmental state.** If a bridge is repaired, that's world state. If a player has read a sign, that's player state.
+
+2. **Conditions on enter scripts** make rooms feel alive. A guard who only barks the first time, a room that changes after a quest completes.
+
+3. **Hidden objects** keep room descriptions clean. Mention them in the room's description text instead of auto-listing them.
+
+4. **Drop tables** are shareable. The `gem_table` is used by copper rocks AND mob loot. Define once, reference everywhere.
+
+5. **Objects are for fixtures, mobs are for living things.** If it has HP and can die, it's a mob. If it's a rock, lever, door, or crafting station, it's an object. Both can have behaviors.
+
+6. **The `behavior` field on mobs** lets you talk to them directly — `talk guard` finds the guard mob, no duplicate object entry needed.
+
+7. **Exits accept both `int` and `map` formats.** `north: 2` is shorthand for `north: {room: 2}`. Add `condition` and `blocked_message` only when needed. Mob room entries accept both `"man"` and `{id: man, ...}`.
+
+8. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed.
+
+9. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather.
+
+10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms.
diff --git a/worldbuilding_guide/toggles.md b/worldbuilding_guide/toggles.md
new file mode 100644
index 0000000..00b48ba
--- /dev/null
+++ b/worldbuilding_guide/toggles.md
@@ -0,0 +1,18 @@
+## Player Toggles
+
+Players can toggle personal settings with the `toggle` command:
+
+| Toggle | Effect |
+|---|---|
+| `description` | Show full room description when moving |
+| `tiny_map` | Mini-map display: `off`/`right`/`left` |
+| `xp_drops` | Show XP gained in gather/craft/combat messages |
+| `exits` | Show exit destinations inline in look output |
+| `mob_enter` | Notify when a mob enters the room |
+| `mob_leave` | Notify when a mob leaves the room |
+| `mob_spawn` | Notify when a mob spawns in the area |
+| `reserve` | Show full reserved item details in look |
+| `depletion` | Show depletion and despawn timers on objects |
+
+---
+
diff --git a/worldbuilding_guide/wandering_mobs.md b/worldbuilding_guide/wandering_mobs.md
new file mode 100644
index 0000000..8d49ed9
--- /dev/null
+++ b/worldbuilding_guide/wandering_mobs.md
@@ -0,0 +1,20 @@
+## Wandering Mobs
+
+Mob wandering is configured per-instance in the room YAML, not on the mob definition:
+```yaml
+# In a room:
+mobs:
+ - id: man
+ wander_interval: 10 # attempt to wander every 10 ticks
+ - id: man
+ wander_interval: 15
+ wander_rooms: [5, 6, 7] # optional — restrict which rooms via exits
+```
+
+Mobs wander through legal (unconditioned) room exits. When the wander interval expires,
+the mob picks a random exit with no conditions and moves through it. If `wander_rooms`
+is set, only exits leading to those room IDs are legal. Mobs without `wander_interval`
+never wander. Mobs stop wandering while in combat. Dead mobs respawn at their home room.
+
+---
+
diff --git a/worldbuilding_guide/wandering_objects.md b/worldbuilding_guide/wandering_objects.md
new file mode 100644
index 0000000..9a47ed2
--- /dev/null
+++ b/worldbuilding_guide/wandering_objects.md
@@ -0,0 +1,23 @@
+## Wandering Objects
+
+Fishing spots that move between rooms:
+```yaml
+# data/objects/fishing_spot.yaml
+id: fishing_spot
+name: fishing spot
+behavior: fish_trout
+```
+
+```yaml
+# In a room:
+objects:
+ - id: fishing_spot
+ wander_rooms: [7, 8, 9]
+ wander_interval: 12
+```
+
+Objects teleport between rooms in their `wander_rooms` list. Players gathering from a
+wandering object are silently interrupted when it moves.
+
+---
+