From 085e728d22a3369bd110b77409914cfbe9ebe611 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 16 Jun 2026 04:17:43 -0400 Subject: feat: recipe system, combining items, cooking skill --- AGENTS.md | 8 +- TODO.md | 2 +- cmd/mud/main.go | 1 + data/help/color.yaml | 6 +- data/help/cook.yaml | 29 ++++ data/help/eat.yaml | 25 +++ data/help/firemaking.yaml | 12 +- data/help/fishing.yaml | 6 +- data/help/gathering.yaml | 9 +- data/help/recipe.yaml | 31 ++++ data/help/use.yaml | 32 +++- data/items/adamant_axe.yaml | 1 - data/items/adamant_pickaxe.yaml | 1 - data/items/adamantite_ore.yaml | 1 - data/items/anchovies.yaml | 8 + data/items/ashes.yaml | 1 - data/items/big_fishing_net.yaml | 1 - data/items/birds_nest.yaml | 1 - data/items/black_axe.yaml | 1 - data/items/black_pickaxe.yaml | 1 - data/items/bones.yaml | 5 +- data/items/bread.yaml | 7 +- data/items/bread_dough.yaml | 11 ++ data/items/bronze_axe.yaml | 1 - data/items/bronze_pickaxe.yaml | 1 - data/items/bronze_sword.yaml | 5 +- data/items/bucket_of_water.yaml | 6 + data/items/burnt_fish.yaml | 6 + data/items/burnt_meat.yaml | 6 + data/items/christmas_cracker.yaml | 1 - data/items/clay.yaml | 1 - data/items/coal.yaml | 1 - data/items/copper_ore.yaml | 1 - data/items/credits.yaml | 5 +- data/items/dragon_axe.yaml | 1 - data/items/dragon_pickaxe.yaml | 1 - data/items/feather.yaml | 1 - data/items/firesteel.yaml | 1 - data/items/fishing_bait.yaml | 1 - data/items/fishing_rod.yaml | 1 - data/items/fly_fishing_rod.yaml | 1 - data/items/gold_ore.yaml | 1 - data/items/harpoon.yaml | 1 - data/items/herring.yaml | 8 + data/items/iron_axe.yaml | 1 - data/items/iron_ore.yaml | 1 - data/items/iron_pickaxe.yaml | 1 - data/items/lighter.yaml | 1 - data/items/lobster_pot.yaml | 1 - data/items/logs.yaml | 1 - data/items/magic_logs.yaml | 1 - data/items/mahogany_logs.yaml | 1 - data/items/maple_logs.yaml | 1 - data/items/matches.yaml | 1 - data/items/mithril_axe.yaml | 1 - data/items/mithril_ore.yaml | 1 - data/items/mithril_pickaxe.yaml | 1 - data/items/oak_logs.yaml | 1 - data/items/party_hat_blue.yaml | 1 - data/items/party_hat_green.yaml | 1 - data/items/party_hat_pink.yaml | 1 - data/items/party_hat_red.yaml | 1 - data/items/party_hat_white.yaml | 1 - data/items/party_hat_yellow.yaml | 1 - data/items/pastry_dough.yaml | 11 ++ data/items/pizza_dough.yaml | 11 ++ data/items/pot_of_flour.yaml | 6 + data/items/raw_anchovies.yaml | 1 - data/items/raw_anglerfish.yaml | 1 - data/items/raw_bass.yaml | 1 - data/items/raw_cave_eel.yaml | 1 - data/items/raw_cod.yaml | 1 - data/items/raw_herring.yaml | 1 - data/items/raw_lobster.yaml | 1 - data/items/raw_mackerel.yaml | 1 - data/items/raw_pike.yaml | 1 - data/items/raw_rainbow_fish.yaml | 1 - data/items/raw_salmon.yaml | 1 - data/items/raw_sardine.yaml | 1 - data/items/raw_shark.yaml | 1 - data/items/raw_shrimps.yaml | 1 - data/items/raw_slimy_eel.yaml | 1 - data/items/raw_swordfish.yaml | 1 - data/items/raw_trout.yaml | 1 - data/items/raw_tuna.yaml | 1 - data/items/redwood_logs.yaml | 1 - data/items/rune_axe.yaml | 1 - data/items/rune_pickaxe.yaml | 1 - data/items/runite_ore.yaml | 1 - data/items/sacred_eel.yaml | 1 - data/items/salmon.yaml | 8 + data/items/sardine.yaml | 8 + data/items/scrap_metal.yaml | 1 - data/items/shrimps.yaml | 8 + data/items/silver_ore.yaml | 1 - data/items/small_fishing_net.yaml | 1 - data/items/steel_axe.yaml | 1 - data/items/steel_pickaxe.yaml | 1 - data/items/teak_logs.yaml | 1 - data/items/tin_ore.yaml | 1 - data/items/trout.yaml | 8 + data/items/uncut_diamond.yaml | 1 - data/items/uncut_emerald.yaml | 1 - data/items/uncut_ruby.yaml | 1 - data/items/uncut_sapphire.yaml | 1 - data/items/willow_logs.yaml | 1 - data/items/yew_logs.yaml | 1 - data/mobs/guard.yaml | 4 +- data/mobs/man.yaml | 4 +- data/mobs/newbie_trainer.yaml | 5 +- data/objects/cooking_range.yaml | 5 + data/objects/lumby_fountain.yaml | 1 - data/recipes/cook_anchovies.yaml | 14 ++ data/recipes/cook_bread.yaml | 14 ++ data/recipes/cook_herring.yaml | 14 ++ data/recipes/cook_salmon.yaml | 14 ++ data/recipes/cook_sardine.yaml | 14 ++ data/recipes/cook_shrimps.yaml | 14 ++ data/recipes/cook_trout.yaml | 14 ++ data/rooms/11.yaml | 11 +- internal/action/recipe.go | 213 +++++++++++++++++++++++++ internal/game/action.go | 2 + internal/game/action_cook.go | 153 ++++++++++++++++++ internal/game/action_state.go | 6 + internal/game/cmd_attack.go | 5 + internal/game/cmd_cook.go | 246 ++++++++++++++++++++++++++++ internal/game/cmd_eat.go | 132 ++++++++++++++++ internal/game/cmd_use.go | 200 +++++++++++++++++++++++ internal/game/game.go | 26 ++- internal/game/help.go | 2 + internal/game/tick.go | 15 ++ internal/net/server.go | 19 ++- internal/object/item.go | 24 ++- internal/object/item_store.go | 22 +++ internal/player/player.go | 1 + worldbuilding_guide/README.md | 25 +++ worldbuilding_guide/behaviors.md | 264 +++++++++++++++++++++++++++++++ worldbuilding_guide/conditions.md | 66 ++++++++ worldbuilding_guide/doors.md | 102 ++++++++++++ worldbuilding_guide/drops.md | 52 ++++++ worldbuilding_guide/hidden_objects.md | 21 +++ worldbuilding_guide/index.md | 10 ++ worldbuilding_guide/items.md | 68 ++++++++ worldbuilding_guide/mobs.md | 54 +++++++ worldbuilding_guide/objects.md | 37 +++++ worldbuilding_guide/quests.md | 107 +++++++++++++ worldbuilding_guide/recipes.md | 112 +++++++++++++ worldbuilding_guide/rooms.md | 122 ++++++++++++++ worldbuilding_guide/search.md | 15 ++ worldbuilding_guide/state.md | 8 + worldbuilding_guide/tips.md | 21 +++ worldbuilding_guide/toggles.md | 18 +++ worldbuilding_guide/wandering_mobs.md | 20 +++ worldbuilding_guide/wandering_objects.md | 23 +++ 154 files changed, 2551 insertions(+), 155 deletions(-) create mode 100644 data/help/cook.yaml create mode 100644 data/help/eat.yaml create mode 100644 data/help/recipe.yaml create mode 100644 data/items/anchovies.yaml create mode 100644 data/items/bread_dough.yaml create mode 100644 data/items/bucket_of_water.yaml create mode 100644 data/items/burnt_fish.yaml create mode 100644 data/items/burnt_meat.yaml create mode 100644 data/items/herring.yaml create mode 100644 data/items/pastry_dough.yaml create mode 100644 data/items/pizza_dough.yaml create mode 100644 data/items/pot_of_flour.yaml create mode 100644 data/items/salmon.yaml create mode 100644 data/items/sardine.yaml create mode 100644 data/items/shrimps.yaml create mode 100644 data/items/trout.yaml create mode 100644 data/objects/cooking_range.yaml create mode 100644 data/recipes/cook_anchovies.yaml create mode 100644 data/recipes/cook_bread.yaml create mode 100644 data/recipes/cook_herring.yaml create mode 100644 data/recipes/cook_salmon.yaml create mode 100644 data/recipes/cook_sardine.yaml create mode 100644 data/recipes/cook_shrimps.yaml create mode 100644 data/recipes/cook_trout.yaml create mode 100644 internal/action/recipe.go create mode 100644 internal/game/action_cook.go create mode 100644 internal/game/cmd_cook.go create mode 100644 internal/game/cmd_eat.go create mode 100644 internal/game/cmd_use.go create mode 100644 worldbuilding_guide/README.md create mode 100644 worldbuilding_guide/behaviors.md create mode 100644 worldbuilding_guide/conditions.md create mode 100644 worldbuilding_guide/doors.md create mode 100644 worldbuilding_guide/drops.md create mode 100644 worldbuilding_guide/hidden_objects.md create mode 100644 worldbuilding_guide/index.md create mode 100644 worldbuilding_guide/items.md create mode 100644 worldbuilding_guide/mobs.md create mode 100644 worldbuilding_guide/objects.md create mode 100644 worldbuilding_guide/quests.md create mode 100644 worldbuilding_guide/recipes.md create mode 100644 worldbuilding_guide/rooms.md create mode 100644 worldbuilding_guide/search.md create mode 100644 worldbuilding_guide/state.md create mode 100644 worldbuilding_guide/tips.md create mode 100644 worldbuilding_guide/toggles.md create mode 100644 worldbuilding_guide/wandering_mobs.md create mode 100644 worldbuilding_guide/wandering_objects.md diff --git a/AGENTS.md b/AGENTS.md index 8c1feea..6dc559b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,6 +104,7 @@ data/objects/.yaml Object definitions (name, behavior, hidden, inroom_des data/behaviors/.yaml Behaviors (gather, talk, use, toggle) data/drops/.yaml Shared drop tables (weighted item lists) data/help/.yaml Help topics +data/recipes/.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 ` | 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 ` | Reset a color target to server default | +| `color 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 ` | Unequip item back to inventory | | `style ` | Set combat style: accurate, aggressive, defensive, balanced (prefix match) | +| `eat ` | 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 ` | Drop all (or matching) items | | `attack` / `kill ` | Combat with numbered targeting and smart default | | `mine` / `chop` / `cut` / `fish ` | Gathering with smart default (verbAliases map to "gather") | -| `use ` | Crafting station interaction | +| `use [on/with] ` | Universal item interaction: recipes, crafting, gathering shortcuts | +| `cook ` | Cook raw food on a fire or cooking range (Cooking skill) | | `talk` / `speak` / `ask ` | NPC conversations (objects or mobs with behavior) | | `pull` / `push ` | Toggle interactions (levers, gates) | | `search ` | 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 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 + + Each food item heals a specific amount of HP (shown when you + examine it with "look "). 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 Cook on a fire or range + use on Use an item on a cooking station + + Item combinations — combine items directly from inventory: + use with 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 + Usage: + use Use an item on a room object + use on Use item on a specific target + use with 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/.yaml` | +| An item (sword, ore, key) | `data/items/.yaml` | +| A mob (NPC, monster) | `data/mobs/.yaml` | +| An interactive object (rock, lever, door) | `data/objects/.yaml` | +| A behavior (mining, dialog, toggle) | `data/behaviors/.yaml` | +| A shared drop table | `data/drops/.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/.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 + +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. + +--- + -- cgit v1.2.3