From ec2e94e3463654a6288464a4c070b48ef9bf7368 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 17 Jun 2026 21:13:07 -0400 Subject: feat: smithing added, item/object interaction reworked. recipes and menus refactored. --- AGENTS.md | 37 ++- data/help/cook.yaml | 18 +- data/help/equipment.yaml | 11 +- data/help/recipe.yaml | 29 +- data/help/smelt.yaml | 22 ++ data/help/smith.yaml | 20 ++ data/help/smithing.yaml | 26 ++ data/help/use.yaml | 28 +- data/help/wear.yaml | 16 +- data/items/adamant_arrowtips.yaml | 6 + data/items/adamant_bolts_unf.yaml | 6 + data/items/adamant_dagger.yaml | 12 + data/items/adamant_full_helm.yaml | 9 + data/items/adamant_med_helm.yaml | 9 + data/items/adamant_nails.yaml | 6 + data/items/adamant_platebody.yaml | 9 + data/items/adamant_sword.yaml | 12 + data/items/bread_dough.yaml | 5 +- data/items/bronze_arrowtips.yaml | 6 + data/items/bronze_bolts_unf.yaml | 6 + data/items/bronze_dagger.yaml | 12 + data/items/bronze_full_helm.yaml | 9 + data/items/bronze_med_helm.yaml | 9 + data/items/bronze_nails.yaml | 6 + data/items/bronze_platebody.yaml | 9 + data/items/bronze_sword.yaml | 7 +- data/items/empty_bucket.yaml | 6 + data/items/empty_pitcher.yaml | 6 + data/items/empty_pot.yaml | 6 + data/items/hammer.yaml | 7 + data/items/iron_arrowtips.yaml | 6 + data/items/iron_bolts_unf.yaml | 6 + data/items/iron_dagger.yaml | 12 + data/items/iron_full_helm.yaml | 9 + data/items/iron_med_helm.yaml | 9 + data/items/iron_nails.yaml | 6 + data/items/iron_platebody.yaml | 9 + data/items/iron_sword.yaml | 12 + data/items/mithril_arrowtips.yaml | 6 + data/items/mithril_bolts_unf.yaml | 6 + data/items/mithril_dagger.yaml | 12 + data/items/mithril_full_helm.yaml | 9 + data/items/mithril_med_helm.yaml | 9 + data/items/mithril_nails.yaml | 6 + data/items/mithril_platebody.yaml | 9 + data/items/mithril_sword.yaml | 12 + data/items/pitcher_of_water.yaml | 6 + data/items/rune_arrowtips.yaml | 6 + data/items/rune_bolts_unf.yaml | 6 + data/items/rune_dagger.yaml | 12 + data/items/rune_full_helm.yaml | 9 + data/items/rune_med_helm.yaml | 9 + data/items/rune_nails.yaml | 6 + data/items/rune_platebody.yaml | 9 + data/items/rune_sword.yaml | 12 + data/items/steel_arrowtips.yaml | 6 + data/items/steel_bolts_unf.yaml | 6 + data/items/steel_dagger.yaml | 12 + data/items/steel_full_helm.yaml | 9 + data/items/steel_med_helm.yaml | 9 + data/items/steel_nails.yaml | 6 + data/items/steel_platebody.yaml | 9 + data/items/steel_sword.yaml | 12 + data/objects/anglerfish_spot.yaml | 3 +- data/objects/anvil.yaml | 9 + data/objects/bait_fishing_spot.yaml | 3 +- data/objects/cage_fishing_spot.yaml | 3 +- data/objects/cooking_range.yaml | 3 +- data/objects/fire.yaml | 4 +- data/objects/fly_fishing_spot.yaml | 3 +- data/objects/furnace.yaml | 3 +- data/objects/harpoon_fishing_spot.yaml | 3 +- data/objects/iron_gate.yaml | 3 +- data/objects/lumby_fountain.yaml | 3 +- data/objects/net_fishing_spot.yaml | 3 +- data/objects/sacred_eel_spot.yaml | 3 +- data/objects/swamp_fishing_spot.yaml | 3 +- data/recipes/smith_adamant_arrowtips.yaml | 13 + data/recipes/smith_adamant_bolts_unf.yaml | 13 + data/recipes/smith_adamant_dagger.yaml | 12 + data/recipes/smith_adamant_full_helm.yaml | 12 + data/recipes/smith_adamant_med_helm.yaml | 12 + data/recipes/smith_adamant_nails.yaml | 13 + data/recipes/smith_adamant_platebody.yaml | 12 + data/recipes/smith_adamant_sword.yaml | 12 + data/recipes/smith_bronze_arrowtips.yaml | 13 + data/recipes/smith_bronze_bolts_unf.yaml | 13 + data/recipes/smith_bronze_dagger.yaml | 12 + data/recipes/smith_bronze_full_helm.yaml | 12 + data/recipes/smith_bronze_med_helm.yaml | 12 + data/recipes/smith_bronze_nails.yaml | 13 + data/recipes/smith_bronze_platebody.yaml | 12 + data/recipes/smith_bronze_sword.yaml | 12 + data/recipes/smith_iron_arrowtips.yaml | 13 + data/recipes/smith_iron_bolts_unf.yaml | 13 + data/recipes/smith_iron_dagger.yaml | 12 + data/recipes/smith_iron_full_helm.yaml | 12 + data/recipes/smith_iron_med_helm.yaml | 12 + data/recipes/smith_iron_nails.yaml | 13 + data/recipes/smith_iron_platebody.yaml | 12 + data/recipes/smith_iron_sword.yaml | 12 + data/recipes/smith_mithril_arrowtips.yaml | 13 + data/recipes/smith_mithril_bolts_unf.yaml | 13 + data/recipes/smith_mithril_dagger.yaml | 12 + data/recipes/smith_mithril_full_helm.yaml | 12 + data/recipes/smith_mithril_med_helm.yaml | 12 + data/recipes/smith_mithril_nails.yaml | 13 + data/recipes/smith_mithril_platebody.yaml | 12 + data/recipes/smith_mithril_sword.yaml | 12 + data/recipes/smith_rune_arrowtips.yaml | 13 + data/recipes/smith_rune_bolts_unf.yaml | 13 + data/recipes/smith_rune_dagger.yaml | 12 + data/recipes/smith_rune_full_helm.yaml | 12 + data/recipes/smith_rune_med_helm.yaml | 12 + data/recipes/smith_rune_nails.yaml | 13 + data/recipes/smith_rune_platebody.yaml | 12 + data/recipes/smith_rune_sword.yaml | 12 + data/recipes/smith_steel_arrowtips.yaml | 13 + data/recipes/smith_steel_bolts_unf.yaml | 13 + data/recipes/smith_steel_dagger.yaml | 12 + data/recipes/smith_steel_full_helm.yaml | 12 + data/recipes/smith_steel_med_helm.yaml | 12 + data/recipes/smith_steel_nails.yaml | 13 + data/recipes/smith_steel_platebody.yaml | 12 + data/recipes/smith_steel_sword.yaml | 12 + data/rooms/12.yaml | 4 + internal/action/recipe.go | 6 +- internal/game/action.go | 8 +- internal/game/action_cook.go | 153 ---------- internal/game/action_production.go | 467 ++++++++++++++++++++++++++++++ internal/game/action_smelt.go | 153 ---------- internal/game/action_state.go | 9 +- internal/game/cmd_cook.go | 158 ++-------- internal/game/cmd_look.go | 12 +- internal/game/cmd_smelt.go | 163 ++--------- internal/game/cmd_smith.go | 342 ++++++++++++++++++++++ internal/game/cmd_use.go | 69 +++-- internal/game/game.go | 17 +- internal/game/stations.go | 28 ++ internal/net/server.go | 9 +- internal/object/item.go | 6 +- internal/object/object.go | 26 +- worldbuilding_guide/hidden_objects.md | 3 +- worldbuilding_guide/items.md | 38 ++- worldbuilding_guide/objects.md | 90 +++++- worldbuilding_guide/recipes.md | 81 ++++++ 146 files changed, 2380 insertions(+), 757 deletions(-) create mode 100644 data/help/smelt.yaml create mode 100644 data/help/smith.yaml create mode 100644 data/help/smithing.yaml create mode 100644 data/items/adamant_arrowtips.yaml create mode 100644 data/items/adamant_bolts_unf.yaml create mode 100644 data/items/adamant_dagger.yaml create mode 100644 data/items/adamant_full_helm.yaml create mode 100644 data/items/adamant_med_helm.yaml create mode 100644 data/items/adamant_nails.yaml create mode 100644 data/items/adamant_platebody.yaml create mode 100644 data/items/adamant_sword.yaml create mode 100644 data/items/bronze_arrowtips.yaml create mode 100644 data/items/bronze_bolts_unf.yaml create mode 100644 data/items/bronze_dagger.yaml create mode 100644 data/items/bronze_full_helm.yaml create mode 100644 data/items/bronze_med_helm.yaml create mode 100644 data/items/bronze_nails.yaml create mode 100644 data/items/bronze_platebody.yaml create mode 100644 data/items/empty_bucket.yaml create mode 100644 data/items/empty_pitcher.yaml create mode 100644 data/items/empty_pot.yaml create mode 100644 data/items/hammer.yaml create mode 100644 data/items/iron_arrowtips.yaml create mode 100644 data/items/iron_bolts_unf.yaml create mode 100644 data/items/iron_dagger.yaml create mode 100644 data/items/iron_full_helm.yaml create mode 100644 data/items/iron_med_helm.yaml create mode 100644 data/items/iron_nails.yaml create mode 100644 data/items/iron_platebody.yaml create mode 100644 data/items/iron_sword.yaml create mode 100644 data/items/mithril_arrowtips.yaml create mode 100644 data/items/mithril_bolts_unf.yaml create mode 100644 data/items/mithril_dagger.yaml create mode 100644 data/items/mithril_full_helm.yaml create mode 100644 data/items/mithril_med_helm.yaml create mode 100644 data/items/mithril_nails.yaml create mode 100644 data/items/mithril_platebody.yaml create mode 100644 data/items/mithril_sword.yaml create mode 100644 data/items/pitcher_of_water.yaml create mode 100644 data/items/rune_arrowtips.yaml create mode 100644 data/items/rune_bolts_unf.yaml create mode 100644 data/items/rune_dagger.yaml create mode 100644 data/items/rune_full_helm.yaml create mode 100644 data/items/rune_med_helm.yaml create mode 100644 data/items/rune_nails.yaml create mode 100644 data/items/rune_platebody.yaml create mode 100644 data/items/rune_sword.yaml create mode 100644 data/items/steel_arrowtips.yaml create mode 100644 data/items/steel_bolts_unf.yaml create mode 100644 data/items/steel_dagger.yaml create mode 100644 data/items/steel_full_helm.yaml create mode 100644 data/items/steel_med_helm.yaml create mode 100644 data/items/steel_nails.yaml create mode 100644 data/items/steel_platebody.yaml create mode 100644 data/items/steel_sword.yaml create mode 100644 data/objects/anvil.yaml create mode 100644 data/recipes/smith_adamant_arrowtips.yaml create mode 100644 data/recipes/smith_adamant_bolts_unf.yaml create mode 100644 data/recipes/smith_adamant_dagger.yaml create mode 100644 data/recipes/smith_adamant_full_helm.yaml create mode 100644 data/recipes/smith_adamant_med_helm.yaml create mode 100644 data/recipes/smith_adamant_nails.yaml create mode 100644 data/recipes/smith_adamant_platebody.yaml create mode 100644 data/recipes/smith_adamant_sword.yaml create mode 100644 data/recipes/smith_bronze_arrowtips.yaml create mode 100644 data/recipes/smith_bronze_bolts_unf.yaml create mode 100644 data/recipes/smith_bronze_dagger.yaml create mode 100644 data/recipes/smith_bronze_full_helm.yaml create mode 100644 data/recipes/smith_bronze_med_helm.yaml create mode 100644 data/recipes/smith_bronze_nails.yaml create mode 100644 data/recipes/smith_bronze_platebody.yaml create mode 100644 data/recipes/smith_bronze_sword.yaml create mode 100644 data/recipes/smith_iron_arrowtips.yaml create mode 100644 data/recipes/smith_iron_bolts_unf.yaml create mode 100644 data/recipes/smith_iron_dagger.yaml create mode 100644 data/recipes/smith_iron_full_helm.yaml create mode 100644 data/recipes/smith_iron_med_helm.yaml create mode 100644 data/recipes/smith_iron_nails.yaml create mode 100644 data/recipes/smith_iron_platebody.yaml create mode 100644 data/recipes/smith_iron_sword.yaml create mode 100644 data/recipes/smith_mithril_arrowtips.yaml create mode 100644 data/recipes/smith_mithril_bolts_unf.yaml create mode 100644 data/recipes/smith_mithril_dagger.yaml create mode 100644 data/recipes/smith_mithril_full_helm.yaml create mode 100644 data/recipes/smith_mithril_med_helm.yaml create mode 100644 data/recipes/smith_mithril_nails.yaml create mode 100644 data/recipes/smith_mithril_platebody.yaml create mode 100644 data/recipes/smith_mithril_sword.yaml create mode 100644 data/recipes/smith_rune_arrowtips.yaml create mode 100644 data/recipes/smith_rune_bolts_unf.yaml create mode 100644 data/recipes/smith_rune_dagger.yaml create mode 100644 data/recipes/smith_rune_full_helm.yaml create mode 100644 data/recipes/smith_rune_med_helm.yaml create mode 100644 data/recipes/smith_rune_nails.yaml create mode 100644 data/recipes/smith_rune_platebody.yaml create mode 100644 data/recipes/smith_rune_sword.yaml create mode 100644 data/recipes/smith_steel_arrowtips.yaml create mode 100644 data/recipes/smith_steel_bolts_unf.yaml create mode 100644 data/recipes/smith_steel_dagger.yaml create mode 100644 data/recipes/smith_steel_full_helm.yaml create mode 100644 data/recipes/smith_steel_med_helm.yaml create mode 100644 data/recipes/smith_steel_nails.yaml create mode 100644 data/recipes/smith_steel_platebody.yaml create mode 100644 data/recipes/smith_steel_sword.yaml delete mode 100644 internal/game/action_cook.go create mode 100644 internal/game/action_production.go delete mode 100644 internal/game/action_smelt.go create mode 100644 internal/game/cmd_smith.go create mode 100644 internal/game/stations.go diff --git a/AGENTS.md b/AGENTS.md index d3eec03..3217614 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ internal/ | `login_char.go` | Character creation, connection, rename, delete | | `cmd_*.go` | Command handlers for each command (see Commands section) | | `action.go` | StartAction, CancelAction, AdvanceActions, checkCondition, verbAliases, verbSkill | -| `action_*.go` | Action lifecycle per type (gather, talk, use, toggle) | +| `action_*.go` | Action lifecycle per type (gather, talk, use, toggle, production) | | `action_burn.go` | Burn/stoke firemaking (standalone, not behavior-YAML-driven) | | `action_search.go` | Search bird's nests for loot | | `action_room.go` | RunEnterSteps (on_enter scripts), BroadcastRespawns | @@ -137,7 +137,7 @@ Item and object YAML `color` fields also support gradient syntax: `color: "g:196 | `say ` | Room chat (preserves case from raw input) | | `score` / `sc` | Character stats and skills | | `inventory` / `i` / `inv` | Inventory contents (28 slots) | -| `equipment` / `eq` | Equipped items by slot | +| `equip` / `eq` / `equipment` / `wear` / `wield` | View or change equipped items (no args = view, with args = equip) | | `look` / `l` | Room view (mobs, objects, ground items, exits, players) | | `look ` / `l ` | Examine mobs, objects, items, players, directions | | `exits` | List available exits | @@ -158,8 +158,7 @@ Item and object YAML `color` fields also support gradient syntax: `color: "g:196 ### Free (stackable, execute before active) | Command | Description | |---------|-------------| -| `wear` / `wield ` | Equip item to correct slot. `wear all` auto-equips best per slot | -| `wear all` | Auto-equips highest-value item per slot from inventory | +| `equip` / `wear` / `wield ` | Equip item to correct slot. `equip all` auto-equips best per slot | | `remove` / `unwear` / `unwield ` | Unequip item back to inventory. `remove all` unequips everything | | `eat ` | Eat food to heal (3-tick cooldown, can eat during combat) | @@ -175,6 +174,8 @@ Item and object YAML `color` fields also support gradient syntax: `color: "g:196 | `mine` / `chop` / `cut` / `fish ` | Gathering with smart default (verbAliases map to "gather") | | `use [on/with] ` | Universal item interaction: recipes, crafting, gathering shortcuts | | `cook ` | Cook raw food on a fire or cooking range (Cooking skill) | +| `smelt [item]` | Smelt ore into bars at a furnace (Smithing skill) | +| `smith [item]` | Smith bars into items at an anvil with a hammer (Smithing 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) | @@ -237,6 +238,9 @@ The `verbSkill` map maps verb → skill name for default target resolution: | `moving` | n/s/e/w/u/d | Multi-tick movement state | | `picking_up` | get | One-tick pickup state | | `dropping` | drop | One-tick drop state | +| `cooking` | cook | Cooking food on fire/range | +| `smelting` | smelt | Smelting ore into bars at furnace | +| `smithing` | smith | Smithing bars into items at anvil | ### Behavior Types (YAML-driven) @@ -254,6 +258,9 @@ The `verbSkill` map maps verb → skill name for default target resolution: | burn | `action_burn.go` | 3-phase firemaking: drop log → try burn → skill check → create fire object | | stoke | `action_burn.go` | Add logs to fire, XP per log, increments fire quality | | search | `action_search.go` | Search items with search_table, consumes one, rolls on drop table | +| cook | `action_production.go` | Recipe-driven cooking on fire/range stations | +| smelt | `action_production.go` | Recipe-driven smelting of ore into bars at furnace | +| smith | `action_production.go` | Recipe-driven smithing of bars into items at anvil (requires hammer) | ### GatherConfig Fields @@ -419,6 +426,8 @@ From `internal/object/item.go`: | `search_misc_table` | Secondary drop table | | `search_ticks` | Ticks per search action | | `search_message` | Custom search message | +| `made_from` | MadeFrom entries for item-on-item combinations (byproducts supported) | +| `ticks` | Ticks per production cycle for MadeFrom combines (default 2) | ## ObjectDef Fields @@ -432,7 +441,8 @@ From `internal/object/object.go`: | `hidden` | Interactable but not shown in room listing | | `inroom_description` | Custom "A X is here." text | | `removal_item` | Item dropped when instance is removed (fire → ashes) | -| `props` | Arbitrary key-value map (description, quality_description with `{quality}` placeholder) | +| `description` | Text shown when examining the object (supports `{quality}` placeholder for quality-based objects) | +| `use_interactions` | List of item-on-object interactions with conditions and actions (see worldbuilding guide) | ## World ObjState Fields @@ -475,7 +485,9 @@ From `internal/net/server.go`: | `StateChangeDescription` | Setting player description | | `StateTalk` | NPC conversation input | | `StateDropAllConfirm` | Confirming `drop all` | -| `StateCookRecipe` | Cook recipe selection | +| `StateRecipeChoice` | Recipe/product selection menu | +| `StateHowMany` | "How many?" production count prompt | +| `StateSmithProduct` | Smith product selection with table display | | `StateColorChoice` | New account color preference | ## Adding a New Skill @@ -491,7 +503,16 @@ Set `xp` on the behavior to award XP on successful gathers. Set `deplete_timer` ## Adding a New Production Skill -Same as gathering but use `type: use` behavior (see `UseConfig` in `internal/action/behavior.go`). Smithing is the reference example planned. +Production skills (cooking, smelting, smithing) share a unified system in `action_production.go`: + +1. Create recipe YAMLs in `data/recipes/` with the new `type` (e.g., `type: fletching`) +2. Create a station object in `data/objects/` if needed +3. Add a `cmd_.go` command handler following the `cmd_cook.go` pattern +4. Add the command to `classifyCommand()` and `executeCommand()` in `game.go` +5. Add the action type string to `AdvanceActions` in `action.go` +6. Add a case in `startProductionFromRecipe()` for default messages + +The unified production cycle handles: "How many?" prompt, start message, timed loops, success/fail rolls, XP, output quantity, stackable stacking, byproducts, and end message. Tool requirements (e.g., hammer for smithing, needle for crafting) are checked in the command handler before calling `promptHowMany`. ## Adding a New Command @@ -508,7 +529,7 @@ Same as gathering but use `type: use` behavior (see `UseConfig` in `internal/act | Production | Cooking, Smithing, Crafting, Fletching, Alchemy, Construction, Firemaking | | Utility | Agility | -Implemented: Mining, Fishing, Woodcutting, Firemaking. The rest are stubs (skill defined, XP tracked, but no behaviors/objects/items yet). +Implemented: Mining, Fishing, Woodcutting, Firemaking, Cooking, Smithing (smelting + smithing). The rest are stubs (skill defined, XP tracked, but no behaviors/objects/items yet). ## Places to Be Careful diff --git a/data/help/cook.yaml b/data/help/cook.yaml index 20ee72f..ec26e20 100644 --- a/data/help/cook.yaml +++ b/data/help/cook.yaml @@ -8,22 +8,20 @@ description: | 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. + and cannot be done on an open fire. - 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. + Before cooking starts, you are prompted "How many?" — press + return for all, type a number, or type anything else to cancel. + + A Cooking skill check determines whether you produce a cooked + item or a burnt one. On failure, you get a burnt variant. 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. + If only one thing can be cooked, it skips the menu. 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 + See also: eat, use, recipe, smithing diff --git a/data/help/equipment.yaml b/data/help/equipment.yaml index 1a4b229..3c0d70c 100644 --- a/data/help/equipment.yaml +++ b/data/help/equipment.yaml @@ -1,10 +1,13 @@ name: "equipment" category: "Items" description: | - View your equipped items. + View or change your equipped items. - Usage: equipment, eq + Usage: equip View equipped items + equip Equip an item from inventory + equip all Auto-equip best items per slot + eq, equipment Same as equip + wear, wield Same as equip - Shows what you have equipped in each slot (main hand, head, torso, etc.). - Use "wear " or "wield " to equip items from your inventory. + Shows what you have in each slot (main hand, head, torso, etc.). Use "remove " to unequip. diff --git a/data/help/recipe.yaml b/data/help/recipe.yaml index 9df0a5b..bd8bde6 100644 --- a/data/help/recipe.yaml +++ b/data/help/recipe.yaml @@ -1,31 +1,26 @@ 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. + The recipe system lets you combine items and use stations to + create new items. Recipes are the core of cooking, smithing, + smelting, and other production skills. There are two ways to make things: - Station recipes — use a cooking station (fire or range): + Station recipes — use a crafting station: cook Cook on a fire or range - use on Use an item on a cooking station + smelt Smelt ore at a furnace + smith Smith bars at an anvil + use on Use an item on a station Item combinations — combine items directly from inventory: use with Combine two items (e.g., flour + water) + All production prompts "How many?" before starting. Press return + for all, type a number, or anything else to cancel. + 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. + finished product. Failure may produce a ruined item. - See also: cook, eat, use + See also: cook, smelt, smith, use diff --git a/data/help/smelt.yaml b/data/help/smelt.yaml new file mode 100644 index 0000000..ac38546 --- /dev/null +++ b/data/help/smelt.yaml @@ -0,0 +1,22 @@ +name: "smelt" +category: "Skills" +description: | + Smelt ore into bars at a furnace. + + Usage: smelt Smelt a specific ore + smelt Show all bars you can smelt right now + + Smelting requires a furnace in the room. Before smelting starts, + you are prompted "How many?" — press return for all, type a + number, or type anything else to cancel. + + If an ore can produce multiple products (e.g. iron ore can make + iron bars or steel bars with coal), a menu appears. + + You can also smelt by using ore on the furnace: + use copper ore on furnace + + Iron smelting has a 50% chance of failure — the ore is consumed + but no bar is produced. + + See also: smithing, smith, use diff --git a/data/help/smith.yaml b/data/help/smith.yaml new file mode 100644 index 0000000..9471d83 --- /dev/null +++ b/data/help/smith.yaml @@ -0,0 +1,20 @@ +name: "smith" +category: "Skills" +description: | + Smith bars into weapons, armor, and components at an anvil. + + Usage: smith Show products for a specific bar type + smith Show products (auto-selects if one bar type) + + Smithing requires an anvil in the room and a hammer in your + inventory. A table shows all possible products for the bar type, + with unavailable items (insufficient level or bars) shown dimmed. + + At the prompt, type a product name to start smithing. You can + prefix with a number to limit quantity (e.g., "3 dagger"). + Press enter to repeat the last product you made with that bar. + + You can also smith by using bars on the anvil: + use bronze bar on anvil + + See also: smithing, smelt diff --git a/data/help/smithing.yaml b/data/help/smithing.yaml new file mode 100644 index 0000000..c081b15 --- /dev/null +++ b/data/help/smithing.yaml @@ -0,0 +1,26 @@ +name: "smithing" +category: "Skills" +description: | + Smithing is a production skill for creating metal items. + + There are two parts to smithing: + + Smelting (furnace): + Combine ores into bars. Use the "smelt" command near a furnace. + Bronze requires both copper and tin ore. Higher metals need coal. + + Smithing (anvil): + Hammer bars into weapons, armor, and components. Use the "smith" + command near an anvil with a hammer in your inventory. + + Bar Smelt Lvl Ores Required + Bronze 1 Copper ore + Tin ore + Iron 20 Iron ore (50% fail rate) + Silver 25 Silver ore + Steel 40 Iron ore + 2 Coal + Gold 45 Gold ore + Mithril 50 Mithril ore + 4 Coal + Adamantite 65 Adamantite ore + 6 Coal + Runite 80 Runite ore + 8 Coal + + See also: smelt, smith diff --git a/data/help/use.yaml b/data/help/use.yaml index a6efafb..92c717f 100644 --- a/data/help/use.yaml +++ b/data/help/use.yaml @@ -4,26 +4,28 @@ description: | Use an item on a target — the universal interaction command. Usage: - use Use an item on a room object use on Use item on a specific target - use with Combine two inventory items (shorthand) + use with Combine two inventory items The "use" command routes to different systems based on the target: - 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) + Station object (furnace, anvil, fire, range) + If recipes match the item and station, opens a production menu. - Item target (inventory or ground) - If a recipe matches both items: combines them (e.g., flour + water) + Object with use_interactions + Shows a message or triggers an action (flags, teleport, etc.). - Mob target - Future: give items to NPCs for quests + Object with behavior + Dispatches to the object's behavior (gather, talk, toggle). + + Inventory item + If a MadeFrom recipe matches both items, starts a combine + production action. You are prompted "How many?" before starting. 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) + use copper ore on furnace Smelt copper ore + use bronze bar on anvil Smith bronze items + use flour on water Combine into dough - See also: cook, recipe, gather, talk + See also: cook, smelt, smith, recipe diff --git a/data/help/wear.yaml b/data/help/wear.yaml index d0b665d..2a92fbd 100644 --- a/data/help/wear.yaml +++ b/data/help/wear.yaml @@ -1,16 +1,8 @@ name: "wear" category: "Items" description: | - Equip weapons and tools from your inventory. + Alias for "equip". See "help equipment". - Usage: wear , wield - wear all - - Equips the named item into its appropriate equipment slot. If something - is already in that slot, they are swapped. "wear all" equips the - highest-value item for each slot from your inventory. - - Equipped tools (pickaxes, axes, fishing rods) are used for gathering - actions and always take priority over inventory tools. - - Worn items appear on the equipment/eq screen. + Usage: wear Equip an item from inventory + wear all Auto-equip best items per slot + wear View equipped items diff --git a/data/items/adamant_arrowtips.yaml b/data/items/adamant_arrowtips.yaml new file mode 100644 index 0000000..49ddb57 --- /dev/null +++ b/data/items/adamant_arrowtips.yaml @@ -0,0 +1,6 @@ +id: adamant_arrowtips +name: adamant arrowtips +color: "120" +description: "A set of adamant arrowtips." +value: 16 +stackable: true diff --git a/data/items/adamant_bolts_unf.yaml b/data/items/adamant_bolts_unf.yaml new file mode 100644 index 0000000..db709db --- /dev/null +++ b/data/items/adamant_bolts_unf.yaml @@ -0,0 +1,6 @@ +id: adamant_bolts_unf +name: adamant bolts (unf) +color: "120" +description: "Unfinished adamant crossbow bolts." +value: 20 +stackable: true diff --git a/data/items/adamant_dagger.yaml b/data/items/adamant_dagger.yaml new file mode 100644 index 0000000..10c203a --- /dev/null +++ b/data/items/adamant_dagger.yaml @@ -0,0 +1,12 @@ +id: adamant_dagger +name: adamant dagger +color: "120" +description: "A small adamant dagger." +value: 400 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 14 + strength_bonus: 10 +speed: 3 diff --git a/data/items/adamant_full_helm.yaml b/data/items/adamant_full_helm.yaml new file mode 100644 index 0000000..2b5da4e --- /dev/null +++ b/data/items/adamant_full_helm.yaml @@ -0,0 +1,9 @@ +id: adamant_full_helm +name: adamant full helm +color: "120" +description: "An adamant full helmet." +value: 1000 +stackable: false +equip_slot: head +stats: + defense_bonus: 17 diff --git a/data/items/adamant_med_helm.yaml b/data/items/adamant_med_helm.yaml new file mode 100644 index 0000000..f251b18 --- /dev/null +++ b/data/items/adamant_med_helm.yaml @@ -0,0 +1,9 @@ +id: adamant_med_helm +name: adamant med helm +color: "120" +description: "An adamant medium helmet." +value: 400 +stackable: false +equip_slot: head +stats: + defense_bonus: 12 diff --git a/data/items/adamant_nails.yaml b/data/items/adamant_nails.yaml new file mode 100644 index 0000000..c40d228 --- /dev/null +++ b/data/items/adamant_nails.yaml @@ -0,0 +1,6 @@ +id: adamant_nails +name: adamant nails +color: "120" +description: "A set of adamant nails." +value: 16 +stackable: true diff --git a/data/items/adamant_platebody.yaml b/data/items/adamant_platebody.yaml new file mode 100644 index 0000000..5f18fb2 --- /dev/null +++ b/data/items/adamant_platebody.yaml @@ -0,0 +1,9 @@ +id: adamant_platebody +name: adamant platebody +color: "120" +description: "An adamant platebody." +value: 5000 +stackable: false +equip_slot: torso +stats: + defense_bonus: 33 diff --git a/data/items/adamant_sword.yaml b/data/items/adamant_sword.yaml new file mode 100644 index 0000000..5e1f992 --- /dev/null +++ b/data/items/adamant_sword.yaml @@ -0,0 +1,12 @@ +id: adamant_sword +name: adamant sword +color: "120" +description: "An adamant sword." +value: 700 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 17 + strength_bonus: 13 +speed: 4 diff --git a/data/items/bread_dough.yaml b/data/items/bread_dough.yaml index 47df9b4..c40d0e0 100644 --- a/data/items/bread_dough.yaml +++ b/data/items/bread_dough.yaml @@ -4,8 +4,11 @@ color: "222" description: "A ball of uncooked bread dough. It needs to be baked." value: 3 stackable: false +ticks: 2 made_from: - items: [pot_of_flour] qty: 1 - - items: [bucket_of_water] + byproducts: [empty_pot] + - items: [bucket_of_water, pitcher_of_water] qty: 1 + byproducts: [empty_bucket, empty_pitcher] diff --git a/data/items/bronze_arrowtips.yaml b/data/items/bronze_arrowtips.yaml new file mode 100644 index 0000000..df42e59 --- /dev/null +++ b/data/items/bronze_arrowtips.yaml @@ -0,0 +1,6 @@ +id: bronze_arrowtips +name: bronze arrowtips +color: "178" +description: "A set of bronze arrowtips." +value: 1 +stackable: true diff --git a/data/items/bronze_bolts_unf.yaml b/data/items/bronze_bolts_unf.yaml new file mode 100644 index 0000000..538d8ab --- /dev/null +++ b/data/items/bronze_bolts_unf.yaml @@ -0,0 +1,6 @@ +id: bronze_bolts_unf +name: bronze bolts (unf) +color: "178" +description: "Unfinished bronze crossbow bolts." +value: 1 +stackable: true diff --git a/data/items/bronze_dagger.yaml b/data/items/bronze_dagger.yaml new file mode 100644 index 0000000..eca6cdb --- /dev/null +++ b/data/items/bronze_dagger.yaml @@ -0,0 +1,12 @@ +id: bronze_dagger +name: bronze dagger +color: "178" +description: "A small bronze dagger." +value: 8 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 3 + strength_bonus: 2 +speed: 3 diff --git a/data/items/bronze_full_helm.yaml b/data/items/bronze_full_helm.yaml new file mode 100644 index 0000000..1823f80 --- /dev/null +++ b/data/items/bronze_full_helm.yaml @@ -0,0 +1,9 @@ +id: bronze_full_helm +name: bronze full helm +color: "178" +description: "A bronze full helmet." +value: 20 +stackable: false +equip_slot: head +stats: + defense_bonus: 4 diff --git a/data/items/bronze_med_helm.yaml b/data/items/bronze_med_helm.yaml new file mode 100644 index 0000000..7aad494 --- /dev/null +++ b/data/items/bronze_med_helm.yaml @@ -0,0 +1,9 @@ +id: bronze_med_helm +name: bronze med helm +color: "178" +description: "A bronze medium helmet." +value: 8 +stackable: false +equip_slot: head +stats: + defense_bonus: 2 diff --git a/data/items/bronze_nails.yaml b/data/items/bronze_nails.yaml new file mode 100644 index 0000000..513e9c4 --- /dev/null +++ b/data/items/bronze_nails.yaml @@ -0,0 +1,6 @@ +id: bronze_nails +name: bronze nails +color: "178" +description: "A set of bronze nails." +value: 1 +stackable: true diff --git a/data/items/bronze_platebody.yaml b/data/items/bronze_platebody.yaml new file mode 100644 index 0000000..eea105b --- /dev/null +++ b/data/items/bronze_platebody.yaml @@ -0,0 +1,9 @@ +id: bronze_platebody +name: bronze platebody +color: "178" +description: "A bronze platebody." +value: 60 +stackable: false +equip_slot: torso +stats: + defense_bonus: 8 diff --git a/data/items/bronze_sword.yaml b/data/items/bronze_sword.yaml index 0ae7a07..01a96ae 100644 --- a/data/items/bronze_sword.yaml +++ b/data/items/bronze_sword.yaml @@ -1,13 +1,12 @@ id: bronze_sword name: bronze sword color: "178" -description: "A basic bronze short sword." +description: "A basic bronze sword." value: 12 stackable: false -equip_slot: "main_hand" -weapon_type: "melee" +equip_slot: main_hand +weapon_type: melee stats: attack_bonus: 4 strength_bonus: 3 - defense_bonus: 0 speed: 4 diff --git a/data/items/empty_bucket.yaml b/data/items/empty_bucket.yaml new file mode 100644 index 0000000..6debb02 --- /dev/null +++ b/data/items/empty_bucket.yaml @@ -0,0 +1,6 @@ +id: empty_bucket +name: empty bucket +color: "137" +description: "An empty wooden bucket." +value: 1 +stackable: false diff --git a/data/items/empty_pitcher.yaml b/data/items/empty_pitcher.yaml new file mode 100644 index 0000000..d1fbb49 --- /dev/null +++ b/data/items/empty_pitcher.yaml @@ -0,0 +1,6 @@ +id: empty_pitcher +name: empty pitcher +color: "137" +description: "An empty ceramic pitcher." +value: 1 +stackable: false diff --git a/data/items/empty_pot.yaml b/data/items/empty_pot.yaml new file mode 100644 index 0000000..a57b441 --- /dev/null +++ b/data/items/empty_pot.yaml @@ -0,0 +1,6 @@ +id: empty_pot +name: empty pot +color: "130" +description: "An empty clay pot." +value: 1 +stackable: false diff --git a/data/items/hammer.yaml b/data/items/hammer.yaml new file mode 100644 index 0000000..74302a4 --- /dev/null +++ b/data/items/hammer.yaml @@ -0,0 +1,7 @@ +id: hammer +name: hammer +color: "250" +description: "A sturdy hammer for smithing metal on an anvil." +value: 10 +stackable: false +tool_type: hammer diff --git a/data/items/iron_arrowtips.yaml b/data/items/iron_arrowtips.yaml new file mode 100644 index 0000000..54ffd11 --- /dev/null +++ b/data/items/iron_arrowtips.yaml @@ -0,0 +1,6 @@ +id: iron_arrowtips +name: iron arrowtips +color: "250" +description: "A set of iron arrowtips." +value: 2 +stackable: true diff --git a/data/items/iron_bolts_unf.yaml b/data/items/iron_bolts_unf.yaml new file mode 100644 index 0000000..0a51ca0 --- /dev/null +++ b/data/items/iron_bolts_unf.yaml @@ -0,0 +1,6 @@ +id: iron_bolts_unf +name: iron bolts (unf) +color: "250" +description: "Unfinished iron crossbow bolts." +value: 2 +stackable: true diff --git a/data/items/iron_dagger.yaml b/data/items/iron_dagger.yaml new file mode 100644 index 0000000..d11b9ad --- /dev/null +++ b/data/items/iron_dagger.yaml @@ -0,0 +1,12 @@ +id: iron_dagger +name: iron dagger +color: "250" +description: "A small iron dagger." +value: 20 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 5 + strength_bonus: 3 +speed: 3 diff --git a/data/items/iron_full_helm.yaml b/data/items/iron_full_helm.yaml new file mode 100644 index 0000000..f0e95f9 --- /dev/null +++ b/data/items/iron_full_helm.yaml @@ -0,0 +1,9 @@ +id: iron_full_helm +name: iron full helm +color: "250" +description: "An iron full helmet." +value: 50 +stackable: false +equip_slot: head +stats: + defense_bonus: 6 diff --git a/data/items/iron_med_helm.yaml b/data/items/iron_med_helm.yaml new file mode 100644 index 0000000..26585e4 --- /dev/null +++ b/data/items/iron_med_helm.yaml @@ -0,0 +1,9 @@ +id: iron_med_helm +name: iron med helm +color: "250" +description: "An iron medium helmet." +value: 20 +stackable: false +equip_slot: head +stats: + defense_bonus: 4 diff --git a/data/items/iron_nails.yaml b/data/items/iron_nails.yaml new file mode 100644 index 0000000..77323c9 --- /dev/null +++ b/data/items/iron_nails.yaml @@ -0,0 +1,6 @@ +id: iron_nails +name: iron nails +color: "250" +description: "A set of iron nails." +value: 2 +stackable: true diff --git a/data/items/iron_platebody.yaml b/data/items/iron_platebody.yaml new file mode 100644 index 0000000..783d099 --- /dev/null +++ b/data/items/iron_platebody.yaml @@ -0,0 +1,9 @@ +id: iron_platebody +name: iron platebody +color: "250" +description: "An iron platebody." +value: 150 +stackable: false +equip_slot: torso +stats: + defense_bonus: 12 diff --git a/data/items/iron_sword.yaml b/data/items/iron_sword.yaml new file mode 100644 index 0000000..3e0b577 --- /dev/null +++ b/data/items/iron_sword.yaml @@ -0,0 +1,12 @@ +id: iron_sword +name: iron sword +color: "250" +description: "An iron sword." +value: 30 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 6 + strength_bonus: 4 +speed: 4 diff --git a/data/items/mithril_arrowtips.yaml b/data/items/mithril_arrowtips.yaml new file mode 100644 index 0000000..8f68a34 --- /dev/null +++ b/data/items/mithril_arrowtips.yaml @@ -0,0 +1,6 @@ +id: mithril_arrowtips +name: mithril arrowtips +color: "75" +description: "A set of mithril arrowtips." +value: 8 +stackable: true diff --git a/data/items/mithril_bolts_unf.yaml b/data/items/mithril_bolts_unf.yaml new file mode 100644 index 0000000..8015f1a --- /dev/null +++ b/data/items/mithril_bolts_unf.yaml @@ -0,0 +1,6 @@ +id: mithril_bolts_unf +name: mithril bolts (unf) +color: "75" +description: "Unfinished mithril crossbow bolts." +value: 10 +stackable: true diff --git a/data/items/mithril_dagger.yaml b/data/items/mithril_dagger.yaml new file mode 100644 index 0000000..9582f8b --- /dev/null +++ b/data/items/mithril_dagger.yaml @@ -0,0 +1,12 @@ +id: mithril_dagger +name: mithril dagger +color: "75" +description: "A small mithril dagger." +value: 150 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 10 + strength_bonus: 7 +speed: 3 diff --git a/data/items/mithril_full_helm.yaml b/data/items/mithril_full_helm.yaml new file mode 100644 index 0000000..73d59de --- /dev/null +++ b/data/items/mithril_full_helm.yaml @@ -0,0 +1,9 @@ +id: mithril_full_helm +name: mithril full helm +color: "75" +description: "A mithril full helmet." +value: 400 +stackable: false +equip_slot: head +stats: + defense_bonus: 13 diff --git a/data/items/mithril_med_helm.yaml b/data/items/mithril_med_helm.yaml new file mode 100644 index 0000000..35e8dd4 --- /dev/null +++ b/data/items/mithril_med_helm.yaml @@ -0,0 +1,9 @@ +id: mithril_med_helm +name: mithril med helm +color: "75" +description: "A mithril medium helmet." +value: 150 +stackable: false +equip_slot: head +stats: + defense_bonus: 9 diff --git a/data/items/mithril_nails.yaml b/data/items/mithril_nails.yaml new file mode 100644 index 0000000..aeb0544 --- /dev/null +++ b/data/items/mithril_nails.yaml @@ -0,0 +1,6 @@ +id: mithril_nails +name: mithril nails +color: "75" +description: "A set of mithril nails." +value: 8 +stackable: true diff --git a/data/items/mithril_platebody.yaml b/data/items/mithril_platebody.yaml new file mode 100644 index 0000000..506938a --- /dev/null +++ b/data/items/mithril_platebody.yaml @@ -0,0 +1,9 @@ +id: mithril_platebody +name: mithril platebody +color: "75" +description: "A mithril platebody." +value: 1500 +stackable: false +equip_slot: torso +stats: + defense_bonus: 25 diff --git a/data/items/mithril_sword.yaml b/data/items/mithril_sword.yaml new file mode 100644 index 0000000..b5f5562 --- /dev/null +++ b/data/items/mithril_sword.yaml @@ -0,0 +1,12 @@ +id: mithril_sword +name: mithril sword +color: "75" +description: "A mithril sword." +value: 250 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 13 + strength_bonus: 10 +speed: 4 diff --git a/data/items/pitcher_of_water.yaml b/data/items/pitcher_of_water.yaml new file mode 100644 index 0000000..a258411 --- /dev/null +++ b/data/items/pitcher_of_water.yaml @@ -0,0 +1,6 @@ +id: pitcher_of_water +name: pitcher of water +color: "81" +description: "A ceramic pitcher filled with fresh water." +value: 5 +stackable: false diff --git a/data/items/rune_arrowtips.yaml b/data/items/rune_arrowtips.yaml new file mode 100644 index 0000000..c2d80e0 --- /dev/null +++ b/data/items/rune_arrowtips.yaml @@ -0,0 +1,6 @@ +id: rune_arrowtips +name: rune arrowtips +color: "87" +description: "A set of rune arrowtips." +value: 30 +stackable: true diff --git a/data/items/rune_bolts_unf.yaml b/data/items/rune_bolts_unf.yaml new file mode 100644 index 0000000..ecaf494 --- /dev/null +++ b/data/items/rune_bolts_unf.yaml @@ -0,0 +1,6 @@ +id: rune_bolts_unf +name: rune bolts (unf) +color: "87" +description: "Unfinished rune crossbow bolts." +value: 40 +stackable: true diff --git a/data/items/rune_dagger.yaml b/data/items/rune_dagger.yaml new file mode 100644 index 0000000..86360f2 --- /dev/null +++ b/data/items/rune_dagger.yaml @@ -0,0 +1,12 @@ +id: rune_dagger +name: rune dagger +color: "87" +description: "A small rune dagger." +value: 5000 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 19 + strength_bonus: 14 +speed: 3 diff --git a/data/items/rune_full_helm.yaml b/data/items/rune_full_helm.yaml new file mode 100644 index 0000000..f91521b --- /dev/null +++ b/data/items/rune_full_helm.yaml @@ -0,0 +1,9 @@ +id: rune_full_helm +name: rune full helm +color: "87" +description: "A rune full helmet." +value: 12000 +stackable: false +equip_slot: head +stats: + defense_bonus: 22 diff --git a/data/items/rune_med_helm.yaml b/data/items/rune_med_helm.yaml new file mode 100644 index 0000000..32e5358 --- /dev/null +++ b/data/items/rune_med_helm.yaml @@ -0,0 +1,9 @@ +id: rune_med_helm +name: rune med helm +color: "87" +description: "A rune medium helmet." +value: 5000 +stackable: false +equip_slot: head +stats: + defense_bonus: 16 diff --git a/data/items/rune_nails.yaml b/data/items/rune_nails.yaml new file mode 100644 index 0000000..5d2dfc5 --- /dev/null +++ b/data/items/rune_nails.yaml @@ -0,0 +1,6 @@ +id: rune_nails +name: rune nails +color: "87" +description: "A set of rune nails." +value: 30 +stackable: true diff --git a/data/items/rune_platebody.yaml b/data/items/rune_platebody.yaml new file mode 100644 index 0000000..b06efac --- /dev/null +++ b/data/items/rune_platebody.yaml @@ -0,0 +1,9 @@ +id: rune_platebody +name: rune platebody +color: "87" +description: "A rune platebody." +value: 40000 +stackable: false +equip_slot: torso +stats: + defense_bonus: 42 diff --git a/data/items/rune_sword.yaml b/data/items/rune_sword.yaml new file mode 100644 index 0000000..6539b59 --- /dev/null +++ b/data/items/rune_sword.yaml @@ -0,0 +1,12 @@ +id: rune_sword +name: rune sword +color: "87" +description: "A rune sword." +value: 10000 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 23 + strength_bonus: 18 +speed: 4 diff --git a/data/items/steel_arrowtips.yaml b/data/items/steel_arrowtips.yaml new file mode 100644 index 0000000..49000d2 --- /dev/null +++ b/data/items/steel_arrowtips.yaml @@ -0,0 +1,6 @@ +id: steel_arrowtips +name: steel arrowtips +color: "253" +description: "A set of steel arrowtips." +value: 4 +stackable: true diff --git a/data/items/steel_bolts_unf.yaml b/data/items/steel_bolts_unf.yaml new file mode 100644 index 0000000..b65d5fa --- /dev/null +++ b/data/items/steel_bolts_unf.yaml @@ -0,0 +1,6 @@ +id: steel_bolts_unf +name: steel bolts (unf) +color: "253" +description: "Unfinished steel crossbow bolts." +value: 5 +stackable: true diff --git a/data/items/steel_dagger.yaml b/data/items/steel_dagger.yaml new file mode 100644 index 0000000..2022f76 --- /dev/null +++ b/data/items/steel_dagger.yaml @@ -0,0 +1,12 @@ +id: steel_dagger +name: steel dagger +color: "253" +description: "A small steel dagger." +value: 60 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 7 + strength_bonus: 5 +speed: 3 diff --git a/data/items/steel_full_helm.yaml b/data/items/steel_full_helm.yaml new file mode 100644 index 0000000..dd2c59b --- /dev/null +++ b/data/items/steel_full_helm.yaml @@ -0,0 +1,9 @@ +id: steel_full_helm +name: steel full helm +color: "253" +description: "A steel full helmet." +value: 150 +stackable: false +equip_slot: head +stats: + defense_bonus: 9 diff --git a/data/items/steel_med_helm.yaml b/data/items/steel_med_helm.yaml new file mode 100644 index 0000000..bad3e07 --- /dev/null +++ b/data/items/steel_med_helm.yaml @@ -0,0 +1,9 @@ +id: steel_med_helm +name: steel med helm +color: "253" +description: "A steel medium helmet." +value: 60 +stackable: false +equip_slot: head +stats: + defense_bonus: 6 diff --git a/data/items/steel_nails.yaml b/data/items/steel_nails.yaml new file mode 100644 index 0000000..4890d18 --- /dev/null +++ b/data/items/steel_nails.yaml @@ -0,0 +1,6 @@ +id: steel_nails +name: steel nails +color: "253" +description: "A set of steel nails." +value: 4 +stackable: true diff --git a/data/items/steel_platebody.yaml b/data/items/steel_platebody.yaml new file mode 100644 index 0000000..e9fe7e1 --- /dev/null +++ b/data/items/steel_platebody.yaml @@ -0,0 +1,9 @@ +id: steel_platebody +name: steel platebody +color: "253" +description: "A steel platebody." +value: 500 +stackable: false +equip_slot: torso +stats: + defense_bonus: 18 diff --git a/data/items/steel_sword.yaml b/data/items/steel_sword.yaml new file mode 100644 index 0000000..0f65ff9 --- /dev/null +++ b/data/items/steel_sword.yaml @@ -0,0 +1,12 @@ +id: steel_sword +name: steel sword +color: "253" +description: "A steel sword." +value: 100 +stackable: false +equip_slot: main_hand +weapon_type: melee +stats: + attack_bonus: 9 + strength_bonus: 7 +speed: 4 diff --git a/data/objects/anglerfish_spot.yaml b/data/objects/anglerfish_spot.yaml index 034816c..3385ac4 100644 --- a/data/objects/anglerfish_spot.yaml +++ b/data/objects/anglerfish_spot.yaml @@ -2,5 +2,4 @@ id: anglerfish_spot name: anglerfish spot color: "81" behavior: fish_angler -props: - description: "A faint glow emanates from the depths. Use a fishing rod with bait here." +description: "A faint glow emanates from the depths. Use a fishing rod with bait here." diff --git a/data/objects/anvil.yaml b/data/objects/anvil.yaml new file mode 100644 index 0000000..22725c1 --- /dev/null +++ b/data/objects/anvil.yaml @@ -0,0 +1,9 @@ +id: anvil +name: anvil +color: "250" +description: "A heavy iron anvil for hammering metal into shape." +use_interactions: + - item: silver_bar + message: "You should use this with a mold at a furnace." + - item: gold_bar + message: "You should use this with a mold at a furnace." diff --git a/data/objects/bait_fishing_spot.yaml b/data/objects/bait_fishing_spot.yaml index 64b0a98..9ca3fba 100644 --- a/data/objects/bait_fishing_spot.yaml +++ b/data/objects/bait_fishing_spot.yaml @@ -2,5 +2,4 @@ id: bait_fishing_spot name: bait fishing spot color: "75" behavior: fish_bait -props: - description: "Small fish ripple the surface. Use a fishing rod with bait here." +description: "Small fish ripple the surface. Use a fishing rod with bait here." diff --git a/data/objects/cage_fishing_spot.yaml b/data/objects/cage_fishing_spot.yaml index ff6d24b..05e3e0e 100644 --- a/data/objects/cage_fishing_spot.yaml +++ b/data/objects/cage_fishing_spot.yaml @@ -2,5 +2,4 @@ id: cage_fishing_spot name: cage fishing spot color: "75" behavior: fish_cage -props: - description: "A rocky underwater shelf. Use a lobster pot here." +description: "A rocky underwater shelf. Use a lobster pot here." diff --git a/data/objects/cooking_range.yaml b/data/objects/cooking_range.yaml index aa7d1b3..f70b7fd 100644 --- a/data/objects/cooking_range.yaml +++ b/data/objects/cooking_range.yaml @@ -1,5 +1,4 @@ id: cooking_range name: cooking range color: "253" -props: - description: "A sturdy iron range with multiple burners. Perfect for cooking." +description: "A sturdy iron range with multiple burners. Perfect for cooking." diff --git a/data/objects/fire.yaml b/data/objects/fire.yaml index 5a036a8..a214dc7 100644 --- a/data/objects/fire.yaml +++ b/data/objects/fire.yaml @@ -5,6 +5,4 @@ behavior: "" hidden: false inroom_description: "A fire is burning warmly." removal_item: ashes -props: - description: "A crackling fire." - quality_description: "Burning for {quality} more ticks." +description: "A crackling fire. Burning for {quality} more ticks." diff --git a/data/objects/fly_fishing_spot.yaml b/data/objects/fly_fishing_spot.yaml index 59a19ce..1634ab5 100644 --- a/data/objects/fly_fishing_spot.yaml +++ b/data/objects/fly_fishing_spot.yaml @@ -2,5 +2,4 @@ id: fly_fishing_spot name: fly fishing spot color: "75" behavior: fish_fly -props: - description: "Insects dance above the water. Use a fly fishing rod with feathers here." +description: "Insects dance above the water. Use a fly fishing rod with feathers here." diff --git a/data/objects/furnace.yaml b/data/objects/furnace.yaml index 437842a..29a8552 100644 --- a/data/objects/furnace.yaml +++ b/data/objects/furnace.yaml @@ -1,5 +1,4 @@ id: furnace name: furnace color: "196" -props: - description: "A blazing hot furnace for smelting ores into bars." +description: "A blazing hot furnace for smelting ores into bars." diff --git a/data/objects/harpoon_fishing_spot.yaml b/data/objects/harpoon_fishing_spot.yaml index 23040b9..4df56b9 100644 --- a/data/objects/harpoon_fishing_spot.yaml +++ b/data/objects/harpoon_fishing_spot.yaml @@ -2,5 +2,4 @@ id: harpoon_fishing_spot name: harpoon fishing spot color: "75" behavior: fish_harpoon -props: - description: "Large shadows move beneath the waves. Use a harpoon here." +description: "Large shadows move beneath the waves. Use a harpoon here." diff --git a/data/objects/iron_gate.yaml b/data/objects/iron_gate.yaml index a1e7de5..91f59c0 100644 --- a/data/objects/iron_gate.yaml +++ b/data/objects/iron_gate.yaml @@ -3,5 +3,4 @@ name: iron gate color: "250" behavior: iron_gate_toggle hidden: true -props: - description: "A heavy iron gate. It looks like it can be pushed open." +description: "A heavy iron gate. It looks like it can be pushed open." diff --git a/data/objects/lumby_fountain.yaml b/data/objects/lumby_fountain.yaml index ff62717..1c5fd94 100644 --- a/data/objects/lumby_fountain.yaml +++ b/data/objects/lumby_fountain.yaml @@ -1,5 +1,4 @@ id: lumby_fountain name: town fountain color: "81" -props: - description: "Clear water sparkles in the sunlight." +description: "Clear water sparkles in the sunlight." diff --git a/data/objects/net_fishing_spot.yaml b/data/objects/net_fishing_spot.yaml index 287c290..10824bc 100644 --- a/data/objects/net_fishing_spot.yaml +++ b/data/objects/net_fishing_spot.yaml @@ -2,5 +2,4 @@ id: net_fishing_spot name: net fishing spot color: "75" behavior: fish_net -props: - description: "Bubbles break the surface. Use a small or big fishing net here." +description: "Bubbles break the surface. Use a small or big fishing net here." diff --git a/data/objects/sacred_eel_spot.yaml b/data/objects/sacred_eel_spot.yaml index 449f8ea..199ad96 100644 --- a/data/objects/sacred_eel_spot.yaml +++ b/data/objects/sacred_eel_spot.yaml @@ -2,5 +2,4 @@ id: sacred_eel_spot name: sacred eel spot color: "177" behavior: fish_sacred_eel -props: - description: "Waters shimmer with an otherworldly light. Use a fishing rod with bait here." +description: "Waters shimmer with an otherworldly light. Use a fishing rod with bait here." diff --git a/data/objects/swamp_fishing_spot.yaml b/data/objects/swamp_fishing_spot.yaml index bcbeb98..f5a3f13 100644 --- a/data/objects/swamp_fishing_spot.yaml +++ b/data/objects/swamp_fishing_spot.yaml @@ -2,5 +2,4 @@ id: swamp_fishing_spot name: swamp fishing spot color: "108" behavior: fish_swamp -props: - description: "Murky water bubbles ominously. Use a fishing rod with bait here." +description: "Murky water bubbles ominously. Use a fishing rod with bait here." diff --git a/data/recipes/smith_adamant_arrowtips.yaml b/data/recipes/smith_adamant_arrowtips.yaml new file mode 100644 index 0000000..464feae --- /dev/null +++ b/data/recipes/smith_adamant_arrowtips.yaml @@ -0,0 +1,13 @@ +id: smith_adamant_arrowtips +type: smithing +skill: smithing +level: 75 +xp: 62 +wait: 4 +station: [anvil] +consume: + - items: [adamantite_bar] + qty: 1 +output: adamant_arrowtips +output_qty: 15 +message: "You hammer out some adamant arrowtips." diff --git a/data/recipes/smith_adamant_bolts_unf.yaml b/data/recipes/smith_adamant_bolts_unf.yaml new file mode 100644 index 0000000..4bf30bf --- /dev/null +++ b/data/recipes/smith_adamant_bolts_unf.yaml @@ -0,0 +1,13 @@ +id: smith_adamant_bolts_unf +type: smithing +skill: smithing +level: 76 +xp: 62 +wait: 4 +station: [anvil] +consume: + - items: [adamantite_bar] + qty: 1 +output: adamant_bolts_unf +output_qty: 10 +message: "You hammer out some adamant bolts." diff --git a/data/recipes/smith_adamant_dagger.yaml b/data/recipes/smith_adamant_dagger.yaml new file mode 100644 index 0000000..38692b8 --- /dev/null +++ b/data/recipes/smith_adamant_dagger.yaml @@ -0,0 +1,12 @@ +id: smith_adamant_dagger +type: smithing +skill: smithing +level: 70 +xp: 62 +wait: 4 +station: [anvil] +consume: + - items: [adamantite_bar] + qty: 1 +output: adamant_dagger +message: "You hammer out an adamant dagger." diff --git a/data/recipes/smith_adamant_full_helm.yaml b/data/recipes/smith_adamant_full_helm.yaml new file mode 100644 index 0000000..b478da2 --- /dev/null +++ b/data/recipes/smith_adamant_full_helm.yaml @@ -0,0 +1,12 @@ +id: smith_adamant_full_helm +type: smithing +skill: smithing +level: 77 +xp: 125 +wait: 4 +station: [anvil] +consume: + - items: [adamantite_bar] + qty: 2 +output: adamant_full_helm +message: "You hammer out an adamant full helm." diff --git a/data/recipes/smith_adamant_med_helm.yaml b/data/recipes/smith_adamant_med_helm.yaml new file mode 100644 index 0000000..064064f --- /dev/null +++ b/data/recipes/smith_adamant_med_helm.yaml @@ -0,0 +1,12 @@ +id: smith_adamant_med_helm +type: smithing +skill: smithing +level: 73 +xp: 62 +wait: 4 +station: [anvil] +consume: + - items: [adamantite_bar] + qty: 1 +output: adamant_med_helm +message: "You hammer out an adamant med helm." diff --git a/data/recipes/smith_adamant_nails.yaml b/data/recipes/smith_adamant_nails.yaml new file mode 100644 index 0000000..418c277 --- /dev/null +++ b/data/recipes/smith_adamant_nails.yaml @@ -0,0 +1,13 @@ +id: smith_adamant_nails +type: smithing +skill: smithing +level: 74 +xp: 62 +wait: 4 +station: [anvil] +consume: + - items: [adamantite_bar] + qty: 1 +output: adamant_nails +output_qty: 15 +message: "You hammer out some adamant nails." diff --git a/data/recipes/smith_adamant_platebody.yaml b/data/recipes/smith_adamant_platebody.yaml new file mode 100644 index 0000000..81ad87d --- /dev/null +++ b/data/recipes/smith_adamant_platebody.yaml @@ -0,0 +1,12 @@ +id: smith_adamant_platebody +type: smithing +skill: smithing +level: 88 +xp: 312 +wait: 4 +station: [anvil] +consume: + - items: [adamantite_bar] + qty: 5 +output: adamant_platebody +message: "You hammer out an adamant platebody." diff --git a/data/recipes/smith_adamant_sword.yaml b/data/recipes/smith_adamant_sword.yaml new file mode 100644 index 0000000..1071623 --- /dev/null +++ b/data/recipes/smith_adamant_sword.yaml @@ -0,0 +1,12 @@ +id: smith_adamant_sword +type: smithing +skill: smithing +level: 74 +xp: 62 +wait: 4 +station: [anvil] +consume: + - items: [adamantite_bar] + qty: 1 +output: adamant_sword +message: "You hammer out an adamant sword." diff --git a/data/recipes/smith_bronze_arrowtips.yaml b/data/recipes/smith_bronze_arrowtips.yaml new file mode 100644 index 0000000..1f29a33 --- /dev/null +++ b/data/recipes/smith_bronze_arrowtips.yaml @@ -0,0 +1,13 @@ +id: smith_bronze_arrowtips +type: smithing +skill: smithing +level: 5 +xp: 12 +wait: 4 +station: [anvil] +consume: + - items: [bronze_bar] + qty: 1 +output: bronze_arrowtips +output_qty: 15 +message: "You hammer out some bronze arrowtips." diff --git a/data/recipes/smith_bronze_bolts_unf.yaml b/data/recipes/smith_bronze_bolts_unf.yaml new file mode 100644 index 0000000..f43218a --- /dev/null +++ b/data/recipes/smith_bronze_bolts_unf.yaml @@ -0,0 +1,13 @@ +id: smith_bronze_bolts_unf +type: smithing +skill: smithing +level: 6 +xp: 12 +wait: 4 +station: [anvil] +consume: + - items: [bronze_bar] + qty: 1 +output: bronze_bolts_unf +output_qty: 10 +message: "You hammer out some bronze bolts." diff --git a/data/recipes/smith_bronze_dagger.yaml b/data/recipes/smith_bronze_dagger.yaml new file mode 100644 index 0000000..4c6bfa9 --- /dev/null +++ b/data/recipes/smith_bronze_dagger.yaml @@ -0,0 +1,12 @@ +id: smith_bronze_dagger +type: smithing +skill: smithing +level: 1 +xp: 12 +wait: 4 +station: [anvil] +consume: + - items: [bronze_bar] + qty: 1 +output: bronze_dagger +message: "You hammer out a bronze dagger." diff --git a/data/recipes/smith_bronze_full_helm.yaml b/data/recipes/smith_bronze_full_helm.yaml new file mode 100644 index 0000000..159efdb --- /dev/null +++ b/data/recipes/smith_bronze_full_helm.yaml @@ -0,0 +1,12 @@ +id: smith_bronze_full_helm +type: smithing +skill: smithing +level: 7 +xp: 25 +wait: 4 +station: [anvil] +consume: + - items: [bronze_bar] + qty: 2 +output: bronze_full_helm +message: "You hammer out a bronze full helm." diff --git a/data/recipes/smith_bronze_med_helm.yaml b/data/recipes/smith_bronze_med_helm.yaml new file mode 100644 index 0000000..7b6f248 --- /dev/null +++ b/data/recipes/smith_bronze_med_helm.yaml @@ -0,0 +1,12 @@ +id: smith_bronze_med_helm +type: smithing +skill: smithing +level: 3 +xp: 12 +wait: 4 +station: [anvil] +consume: + - items: [bronze_bar] + qty: 1 +output: bronze_med_helm +message: "You hammer out a bronze med helm." diff --git a/data/recipes/smith_bronze_nails.yaml b/data/recipes/smith_bronze_nails.yaml new file mode 100644 index 0000000..5b0180b --- /dev/null +++ b/data/recipes/smith_bronze_nails.yaml @@ -0,0 +1,13 @@ +id: smith_bronze_nails +type: smithing +skill: smithing +level: 4 +xp: 12 +wait: 4 +station: [anvil] +consume: + - items: [bronze_bar] + qty: 1 +output: bronze_nails +output_qty: 15 +message: "You hammer out some bronze nails." diff --git a/data/recipes/smith_bronze_platebody.yaml b/data/recipes/smith_bronze_platebody.yaml new file mode 100644 index 0000000..70ba050 --- /dev/null +++ b/data/recipes/smith_bronze_platebody.yaml @@ -0,0 +1,12 @@ +id: smith_bronze_platebody +type: smithing +skill: smithing +level: 18 +xp: 62 +wait: 4 +station: [anvil] +consume: + - items: [bronze_bar] + qty: 5 +output: bronze_platebody +message: "You hammer out a bronze platebody." diff --git a/data/recipes/smith_bronze_sword.yaml b/data/recipes/smith_bronze_sword.yaml new file mode 100644 index 0000000..75179fd --- /dev/null +++ b/data/recipes/smith_bronze_sword.yaml @@ -0,0 +1,12 @@ +id: smith_bronze_sword +type: smithing +skill: smithing +level: 4 +xp: 12 +wait: 4 +station: [anvil] +consume: + - items: [bronze_bar] + qty: 1 +output: bronze_sword +message: "You hammer out a bronze sword." diff --git a/data/recipes/smith_iron_arrowtips.yaml b/data/recipes/smith_iron_arrowtips.yaml new file mode 100644 index 0000000..dab4309 --- /dev/null +++ b/data/recipes/smith_iron_arrowtips.yaml @@ -0,0 +1,13 @@ +id: smith_iron_arrowtips +type: smithing +skill: smithing +level: 25 +xp: 25 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 1 +output: iron_arrowtips +output_qty: 15 +message: "You hammer out some iron arrowtips." diff --git a/data/recipes/smith_iron_bolts_unf.yaml b/data/recipes/smith_iron_bolts_unf.yaml new file mode 100644 index 0000000..670c873 --- /dev/null +++ b/data/recipes/smith_iron_bolts_unf.yaml @@ -0,0 +1,13 @@ +id: smith_iron_bolts_unf +type: smithing +skill: smithing +level: 26 +xp: 25 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 1 +output: iron_bolts_unf +output_qty: 10 +message: "You hammer out some iron bolts." diff --git a/data/recipes/smith_iron_dagger.yaml b/data/recipes/smith_iron_dagger.yaml new file mode 100644 index 0000000..7d1e55c --- /dev/null +++ b/data/recipes/smith_iron_dagger.yaml @@ -0,0 +1,12 @@ +id: smith_iron_dagger +type: smithing +skill: smithing +level: 20 +xp: 25 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 1 +output: iron_dagger +message: "You hammer out an iron dagger." diff --git a/data/recipes/smith_iron_full_helm.yaml b/data/recipes/smith_iron_full_helm.yaml new file mode 100644 index 0000000..28bddc3 --- /dev/null +++ b/data/recipes/smith_iron_full_helm.yaml @@ -0,0 +1,12 @@ +id: smith_iron_full_helm +type: smithing +skill: smithing +level: 27 +xp: 50 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 2 +output: iron_full_helm +message: "You hammer out an iron full helm." diff --git a/data/recipes/smith_iron_med_helm.yaml b/data/recipes/smith_iron_med_helm.yaml new file mode 100644 index 0000000..a2340fc --- /dev/null +++ b/data/recipes/smith_iron_med_helm.yaml @@ -0,0 +1,12 @@ +id: smith_iron_med_helm +type: smithing +skill: smithing +level: 22 +xp: 25 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 1 +output: iron_med_helm +message: "You hammer out an iron med helm." diff --git a/data/recipes/smith_iron_nails.yaml b/data/recipes/smith_iron_nails.yaml new file mode 100644 index 0000000..85fb039 --- /dev/null +++ b/data/recipes/smith_iron_nails.yaml @@ -0,0 +1,13 @@ +id: smith_iron_nails +type: smithing +skill: smithing +level: 24 +xp: 25 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 1 +output: iron_nails +output_qty: 15 +message: "You hammer out some iron nails." diff --git a/data/recipes/smith_iron_platebody.yaml b/data/recipes/smith_iron_platebody.yaml new file mode 100644 index 0000000..8050868 --- /dev/null +++ b/data/recipes/smith_iron_platebody.yaml @@ -0,0 +1,12 @@ +id: smith_iron_platebody +type: smithing +skill: smithing +level: 33 +xp: 125 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 5 +output: iron_platebody +message: "You hammer out an iron platebody." diff --git a/data/recipes/smith_iron_sword.yaml b/data/recipes/smith_iron_sword.yaml new file mode 100644 index 0000000..9eb3b50 --- /dev/null +++ b/data/recipes/smith_iron_sword.yaml @@ -0,0 +1,12 @@ +id: smith_iron_sword +type: smithing +skill: smithing +level: 24 +xp: 25 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 1 +output: iron_sword +message: "You hammer out an iron sword." diff --git a/data/recipes/smith_mithril_arrowtips.yaml b/data/recipes/smith_mithril_arrowtips.yaml new file mode 100644 index 0000000..dd0edf4 --- /dev/null +++ b/data/recipes/smith_mithril_arrowtips.yaml @@ -0,0 +1,13 @@ +id: smith_mithril_arrowtips +type: smithing +skill: smithing +level: 55 +xp: 50 +wait: 4 +station: [anvil] +consume: + - items: [mithril_bar] + qty: 1 +output: mithril_arrowtips +output_qty: 15 +message: "You hammer out some mithril arrowtips." diff --git a/data/recipes/smith_mithril_bolts_unf.yaml b/data/recipes/smith_mithril_bolts_unf.yaml new file mode 100644 index 0000000..75a931c --- /dev/null +++ b/data/recipes/smith_mithril_bolts_unf.yaml @@ -0,0 +1,13 @@ +id: smith_mithril_bolts_unf +type: smithing +skill: smithing +level: 56 +xp: 50 +wait: 4 +station: [anvil] +consume: + - items: [mithril_bar] + qty: 1 +output: mithril_bolts_unf +output_qty: 10 +message: "You hammer out some mithril bolts." diff --git a/data/recipes/smith_mithril_dagger.yaml b/data/recipes/smith_mithril_dagger.yaml new file mode 100644 index 0000000..416776f --- /dev/null +++ b/data/recipes/smith_mithril_dagger.yaml @@ -0,0 +1,12 @@ +id: smith_mithril_dagger +type: smithing +skill: smithing +level: 50 +xp: 50 +wait: 4 +station: [anvil] +consume: + - items: [mithril_bar] + qty: 1 +output: mithril_dagger +message: "You hammer out a mithril dagger." diff --git a/data/recipes/smith_mithril_full_helm.yaml b/data/recipes/smith_mithril_full_helm.yaml new file mode 100644 index 0000000..bef5645 --- /dev/null +++ b/data/recipes/smith_mithril_full_helm.yaml @@ -0,0 +1,12 @@ +id: smith_mithril_full_helm +type: smithing +skill: smithing +level: 57 +xp: 100 +wait: 4 +station: [anvil] +consume: + - items: [mithril_bar] + qty: 2 +output: mithril_full_helm +message: "You hammer out a mithril full helm." diff --git a/data/recipes/smith_mithril_med_helm.yaml b/data/recipes/smith_mithril_med_helm.yaml new file mode 100644 index 0000000..57ef304 --- /dev/null +++ b/data/recipes/smith_mithril_med_helm.yaml @@ -0,0 +1,12 @@ +id: smith_mithril_med_helm +type: smithing +skill: smithing +level: 53 +xp: 50 +wait: 4 +station: [anvil] +consume: + - items: [mithril_bar] + qty: 1 +output: mithril_med_helm +message: "You hammer out a mithril med helm." diff --git a/data/recipes/smith_mithril_nails.yaml b/data/recipes/smith_mithril_nails.yaml new file mode 100644 index 0000000..ddda12f --- /dev/null +++ b/data/recipes/smith_mithril_nails.yaml @@ -0,0 +1,13 @@ +id: smith_mithril_nails +type: smithing +skill: smithing +level: 54 +xp: 50 +wait: 4 +station: [anvil] +consume: + - items: [mithril_bar] + qty: 1 +output: mithril_nails +output_qty: 15 +message: "You hammer out some mithril nails." diff --git a/data/recipes/smith_mithril_platebody.yaml b/data/recipes/smith_mithril_platebody.yaml new file mode 100644 index 0000000..94048cc --- /dev/null +++ b/data/recipes/smith_mithril_platebody.yaml @@ -0,0 +1,12 @@ +id: smith_mithril_platebody +type: smithing +skill: smithing +level: 68 +xp: 250 +wait: 4 +station: [anvil] +consume: + - items: [mithril_bar] + qty: 5 +output: mithril_platebody +message: "You hammer out a mithril platebody." diff --git a/data/recipes/smith_mithril_sword.yaml b/data/recipes/smith_mithril_sword.yaml new file mode 100644 index 0000000..ccc687d --- /dev/null +++ b/data/recipes/smith_mithril_sword.yaml @@ -0,0 +1,12 @@ +id: smith_mithril_sword +type: smithing +skill: smithing +level: 54 +xp: 50 +wait: 4 +station: [anvil] +consume: + - items: [mithril_bar] + qty: 1 +output: mithril_sword +message: "You hammer out a mithril sword." diff --git a/data/recipes/smith_rune_arrowtips.yaml b/data/recipes/smith_rune_arrowtips.yaml new file mode 100644 index 0000000..a484371 --- /dev/null +++ b/data/recipes/smith_rune_arrowtips.yaml @@ -0,0 +1,13 @@ +id: smith_rune_arrowtips +type: smithing +skill: smithing +level: 90 +xp: 75 +wait: 4 +station: [anvil] +consume: + - items: [runite_bar] + qty: 1 +output: rune_arrowtips +output_qty: 15 +message: "You hammer out some rune arrowtips." diff --git a/data/recipes/smith_rune_bolts_unf.yaml b/data/recipes/smith_rune_bolts_unf.yaml new file mode 100644 index 0000000..f69fb82 --- /dev/null +++ b/data/recipes/smith_rune_bolts_unf.yaml @@ -0,0 +1,13 @@ +id: smith_rune_bolts_unf +type: smithing +skill: smithing +level: 91 +xp: 75 +wait: 4 +station: [anvil] +consume: + - items: [runite_bar] + qty: 1 +output: rune_bolts_unf +output_qty: 10 +message: "You hammer out some rune bolts." diff --git a/data/recipes/smith_rune_dagger.yaml b/data/recipes/smith_rune_dagger.yaml new file mode 100644 index 0000000..e24c381 --- /dev/null +++ b/data/recipes/smith_rune_dagger.yaml @@ -0,0 +1,12 @@ +id: smith_rune_dagger +type: smithing +skill: smithing +level: 85 +xp: 75 +wait: 4 +station: [anvil] +consume: + - items: [runite_bar] + qty: 1 +output: rune_dagger +message: "You hammer out a rune dagger." diff --git a/data/recipes/smith_rune_full_helm.yaml b/data/recipes/smith_rune_full_helm.yaml new file mode 100644 index 0000000..6b69cb1 --- /dev/null +++ b/data/recipes/smith_rune_full_helm.yaml @@ -0,0 +1,12 @@ +id: smith_rune_full_helm +type: smithing +skill: smithing +level: 92 +xp: 150 +wait: 4 +station: [anvil] +consume: + - items: [runite_bar] + qty: 2 +output: rune_full_helm +message: "You hammer out a rune full helm." diff --git a/data/recipes/smith_rune_med_helm.yaml b/data/recipes/smith_rune_med_helm.yaml new file mode 100644 index 0000000..137c78f --- /dev/null +++ b/data/recipes/smith_rune_med_helm.yaml @@ -0,0 +1,12 @@ +id: smith_rune_med_helm +type: smithing +skill: smithing +level: 88 +xp: 75 +wait: 4 +station: [anvil] +consume: + - items: [runite_bar] + qty: 1 +output: rune_med_helm +message: "You hammer out a rune med helm." diff --git a/data/recipes/smith_rune_nails.yaml b/data/recipes/smith_rune_nails.yaml new file mode 100644 index 0000000..320e376 --- /dev/null +++ b/data/recipes/smith_rune_nails.yaml @@ -0,0 +1,13 @@ +id: smith_rune_nails +type: smithing +skill: smithing +level: 89 +xp: 75 +wait: 4 +station: [anvil] +consume: + - items: [runite_bar] + qty: 1 +output: rune_nails +output_qty: 15 +message: "You hammer out some rune nails." diff --git a/data/recipes/smith_rune_platebody.yaml b/data/recipes/smith_rune_platebody.yaml new file mode 100644 index 0000000..7886afc --- /dev/null +++ b/data/recipes/smith_rune_platebody.yaml @@ -0,0 +1,12 @@ +id: smith_rune_platebody +type: smithing +skill: smithing +level: 99 +xp: 375 +wait: 4 +station: [anvil] +consume: + - items: [runite_bar] + qty: 5 +output: rune_platebody +message: "You hammer out a rune platebody." diff --git a/data/recipes/smith_rune_sword.yaml b/data/recipes/smith_rune_sword.yaml new file mode 100644 index 0000000..5ff3302 --- /dev/null +++ b/data/recipes/smith_rune_sword.yaml @@ -0,0 +1,12 @@ +id: smith_rune_sword +type: smithing +skill: smithing +level: 89 +xp: 75 +wait: 4 +station: [anvil] +consume: + - items: [runite_bar] + qty: 1 +output: rune_sword +message: "You hammer out a rune sword." diff --git a/data/recipes/smith_steel_arrowtips.yaml b/data/recipes/smith_steel_arrowtips.yaml new file mode 100644 index 0000000..5acb5c5 --- /dev/null +++ b/data/recipes/smith_steel_arrowtips.yaml @@ -0,0 +1,13 @@ +id: smith_steel_arrowtips +type: smithing +skill: smithing +level: 35 +xp: 37 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 1 +output: steel_arrowtips +output_qty: 15 +message: "You hammer out some steel arrowtips." diff --git a/data/recipes/smith_steel_bolts_unf.yaml b/data/recipes/smith_steel_bolts_unf.yaml new file mode 100644 index 0000000..c4acbcf --- /dev/null +++ b/data/recipes/smith_steel_bolts_unf.yaml @@ -0,0 +1,13 @@ +id: smith_steel_bolts_unf +type: smithing +skill: smithing +level: 36 +xp: 37 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 1 +output: steel_bolts_unf +output_qty: 10 +message: "You hammer out some steel bolts." diff --git a/data/recipes/smith_steel_dagger.yaml b/data/recipes/smith_steel_dagger.yaml new file mode 100644 index 0000000..92be7b1 --- /dev/null +++ b/data/recipes/smith_steel_dagger.yaml @@ -0,0 +1,12 @@ +id: smith_steel_dagger +type: smithing +skill: smithing +level: 30 +xp: 37 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 1 +output: steel_dagger +message: "You hammer out a steel dagger." diff --git a/data/recipes/smith_steel_full_helm.yaml b/data/recipes/smith_steel_full_helm.yaml new file mode 100644 index 0000000..faa872a --- /dev/null +++ b/data/recipes/smith_steel_full_helm.yaml @@ -0,0 +1,12 @@ +id: smith_steel_full_helm +type: smithing +skill: smithing +level: 37 +xp: 75 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 2 +output: steel_full_helm +message: "You hammer out a steel full helm." diff --git a/data/recipes/smith_steel_med_helm.yaml b/data/recipes/smith_steel_med_helm.yaml new file mode 100644 index 0000000..bc1666f --- /dev/null +++ b/data/recipes/smith_steel_med_helm.yaml @@ -0,0 +1,12 @@ +id: smith_steel_med_helm +type: smithing +skill: smithing +level: 33 +xp: 37 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 1 +output: steel_med_helm +message: "You hammer out a steel med helm." diff --git a/data/recipes/smith_steel_nails.yaml b/data/recipes/smith_steel_nails.yaml new file mode 100644 index 0000000..29a4caf --- /dev/null +++ b/data/recipes/smith_steel_nails.yaml @@ -0,0 +1,13 @@ +id: smith_steel_nails +type: smithing +skill: smithing +level: 34 +xp: 37 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 1 +output: steel_nails +output_qty: 15 +message: "You hammer out some steel nails." diff --git a/data/recipes/smith_steel_platebody.yaml b/data/recipes/smith_steel_platebody.yaml new file mode 100644 index 0000000..d4f2647 --- /dev/null +++ b/data/recipes/smith_steel_platebody.yaml @@ -0,0 +1,12 @@ +id: smith_steel_platebody +type: smithing +skill: smithing +level: 48 +xp: 187 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 5 +output: steel_platebody +message: "You hammer out a steel platebody." diff --git a/data/recipes/smith_steel_sword.yaml b/data/recipes/smith_steel_sword.yaml new file mode 100644 index 0000000..66898fb --- /dev/null +++ b/data/recipes/smith_steel_sword.yaml @@ -0,0 +1,12 @@ +id: smith_steel_sword +type: smithing +skill: smithing +level: 34 +xp: 37 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 1 +output: steel_sword +message: "You hammer out a steel sword." diff --git a/data/rooms/12.yaml b/data/rooms/12.yaml index 4da9c7e..d8850eb 100644 --- a/data/rooms/12.yaml +++ b/data/rooms/12.yaml @@ -6,6 +6,7 @@ exits: west: 11 objects: - id: furnace + - id: anvil spawns: - item_id: copper_ore quantity: 3 @@ -19,3 +20,6 @@ spawns: - item_id: coal quantity: 5 respawn_ticks: 30 + - item_id: hammer + quantity: 1 + respawn_ticks: 30 diff --git a/internal/action/recipe.go b/internal/action/recipe.go index 18c08c5..2e93c06 100644 --- a/internal/action/recipe.go +++ b/internal/action/recipe.go @@ -9,8 +9,9 @@ import ( ) type ConsumeEntry struct { - Items []string `yaml:"items"` - Qty int `yaml:"qty"` + Items []string `yaml:"items"` + Qty int `yaml:"qty"` + Byproducts []string `yaml:"byproducts"` } type RecipeDef struct { @@ -22,6 +23,7 @@ type RecipeDef struct { Station []string `yaml:"station"` Consume []ConsumeEntry `yaml:"consume"` Output string `yaml:"output"` + OutputQty int `yaml:"output_qty"` Fail string `yaml:"fail"` Message string `yaml:"message"` FailMessage string `yaml:"fail_message"` diff --git a/internal/game/action.go b/internal/game/action.go index 608528d..a62ccec 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -226,9 +226,13 @@ func (g *Game) AdvanceActions() { case "search": g.advanceSearch(sess, p) case "cook": - g.advanceCook(sess, p) + g.advanceProduction(sess, p) case "smelt": - g.advanceSmelt(sess, p) + g.advanceProduction(sess, p) + case "smith": + g.advanceProduction(sess, p) + case "combine": + g.advanceProduction(sess, p) } if p.Action == nil { g.writePrompt(sess) diff --git a/internal/game/action_cook.go b/internal/game/action_cook.go deleted file mode 100644 index a5bda41..0000000 --- a/internal/game/action_cook.go +++ /dev/null @@ -1,153 +0,0 @@ -package game - -import ( - "fmt" - "math/rand" - - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/engine" - "thehouseoficarus/internal/net" - "thehouseoficarus/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_production.go b/internal/game/action_production.go new file mode 100644 index 0000000..c0b76f3 --- /dev/null +++ b/internal/game/action_production.go @@ -0,0 +1,467 @@ +package game + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" +) + +type recipeEntry struct { + ItemName string + Recipe action.RecipeDef +} + +func recipeMenuData(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 entryMenuData(entries []recipeEntry) []map[string]string { + out := make([]map[string]string, len(entries)) + for i, e := range entries { + out[i] = map[string]string{"recipe_id": e.Recipe.ID} + } + return out +} + +func (g *Game) promptHowMany(sess *net.Session, recipeID string) { + sess.PendingRecipeID = recipeID + sess.State = net.StateHowMany + sess.Write("How many (return for all)?: ") +} + +func (g *Game) handleHowMany(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + recipeID := sess.PendingRecipeID + sess.PendingRecipeID = "" + sess.State = net.StateGame + + input = strings.TrimSpace(input) + + var count int + if input == "" { + count = 0 + } else if n, err := strconv.Atoi(input); err == nil && n > 0 { + count = n + } else { + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + + var recipe *action.RecipeDef + if strings.HasPrefix(recipeID, "combine_") { + itemID := strings.TrimPrefix(recipeID, "combine_") + itemDef, err := g.ItemStore.Load(itemID) + if err != nil || len(itemDef.MadeFrom) == 0 { + g.reprompt(sess) + return + } + recipe = g.buildCombineRecipe(itemDef) + } else { + all, err := g.RecipeStore.LoadAll() + if err != nil { + g.reprompt(sess) + return + } + for _, r := range all { + if r.ID == recipeID { + recipe = &r + break + } + } + } + + if recipe == nil { + g.reprompt(sess) + return + } + + g.startProductionFromRecipe(sess, p, recipe, count) +} + +func (g *Game) buildCombineRecipe(def *object.ItemDef) *action.RecipeDef { + var consume []action.ConsumeEntry + for _, mf := range def.MadeFrom { + consume = append(consume, action.ConsumeEntry{ + Items: mf.Items, + Qty: mf.Qty, + Byproducts: mf.Byproducts, + }) + } + wait := def.Ticks + if wait <= 0 { + wait = 2 + } + return &action.RecipeDef{ + ID: "combine_" + def.ID, + Type: "combine", + Wait: wait, + Consume: consume, + Output: def.ID, + Message: fmt.Sprintf("You create %s.", def.Name), + } +} + +func (g *Game) startProduction(sess *net.Session, p *player.Player, recipe *action.RecipeDef, actionType, displayVerb, startMsg, endMsg string, count int) { + 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 do that.", recipe.Level, recipe.Skill)) + return + } + } + + if !recipe.HasAllItemsQty(p.CountItem) { + sess.WriteLine("You don't have the required materials.") + return + } + + outputQty := recipe.OutputQty + if outputQty <= 0 { + outputQty = 1 + } + + canPlace := false + outDef, _ := g.ItemStore.Load(recipe.Output) + if outDef != nil && outDef.Stackable { + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot != nil && slot.ItemID == recipe.Output { + canPlace = true + break + } + } + } + if !canPlace && p.FirstFreeSlot() == -1 { + sess.WriteLine("Your inventory is too full!") + return + } + + wait := recipe.Wait + if wait <= 0 { + wait = 4 + } + + outputName := recipe.Output + if outDef != nil { + outputName = outDef.Name + } + + p.Action = &action.Action{ + Type: actionType, + TargetID: recipe.ID, + TargetName: outputName, + Data: map[string]any{ + "recipe_id": recipe.ID, + "phase": 0, + "wait": wait, + "start_msg": startMsg, + "end_msg": endMsg, + "remaining": count, + }, + WaitLeft: engine.ToTicks(1), + } + + p.ActionState = &ActionState{Type: ActionProducing, TargetName: outputName, Verb: displayVerb} +} + +func (g *Game) startProductionFromRecipe(sess *net.Session, p *player.Player, recipe *action.RecipeDef, count int) { + _, stationName := g.findStation(p.RoomID, recipe.Station) + if stationName == "" { + stationName = "inventory" + } + + firstItem := recipe.FirstItemName(func(id string) (string, bool) { + def, err := g.ItemStore.Load(id) + if err != nil { + return id, false + } + return def.Name, true + }) + + var actionType, displayVerb, startMsg, endMsg string + switch recipe.Type { + case "cooking": + actionType = "cook" + displayVerb = "cooking" + if stationName != "inventory" { + startMsg = fmt.Sprintf("You start cooking %s on the %s.", firstItem, stationName) + } else { + startMsg = fmt.Sprintf("You start making %s.", firstItem) + } + endMsg = "You've cooked everything you can." + case "smelting": + actionType = "smelt" + displayVerb = "smelting" + startMsg = fmt.Sprintf("You begin to process %s in the furnace.", firstItem) + endMsg = "You've processed all the ore in your inventory." + case "smithing": + actionType = "smith" + displayVerb = "smithing" + startMsg = fmt.Sprintf("You hammer the %s on the anvil...", firstItem) + endMsg = "You've used all the bars you can." + case "combine": + actionType = "combine" + displayVerb = "combining" + startMsg = fmt.Sprintf("You start combining %s.", firstItem) + endMsg = "You've run out of materials." + default: + actionType = "craft" + displayVerb = "crafting" + startMsg = fmt.Sprintf("You start crafting with %s.", firstItem) + endMsg = "You've run out of materials." + } + + g.startProduction(sess, p, recipe, actionType, displayVerb, startMsg, endMsg, count) +} + +func (g *Game) loadProductionRecipe(recipeID string) *action.RecipeDef { + all, err := g.RecipeStore.LoadAll() + if err == nil { + for _, r := range all { + if r.ID == recipeID { + return &r + } + } + } + if strings.HasPrefix(recipeID, "combine_") { + itemID := strings.TrimPrefix(recipeID, "combine_") + if itemDef, err := g.ItemStore.Load(itemID); err == nil && len(itemDef.MadeFrom) > 0 { + return g.buildCombineRecipe(itemDef) + } + } + return nil +} + +func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { + recipeID := p.Action.Data["recipe_id"].(string) + phase := p.Action.Data["phase"].(int) + wait := p.Action.Data["wait"].(float64) + startMsg, _ := p.Action.Data["start_msg"].(string) + endMsg, _ := p.Action.Data["end_msg"].(string) + remaining, _ := p.Action.Data["remaining"].(int) + + recipe := g.loadProductionRecipe(recipeID) + if recipe == nil { + g.CancelAction(p) + return false + } + + if phase == 0 { + if startMsg != "" { + sess.WriteLine(fmt.Sprintf("\n%s", startMsg)) + } + 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 { + outputQty := recipe.OutputQty + if outputQty <= 0 { + outputQty = 1 + } + + byproducts := g.collectByproducts(p, recipe) + + placed := false + outDef, _ := g.ItemStore.Load(recipe.Output) + if outDef != nil && outDef.Stackable { + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot != nil && slot.ItemID == recipe.Output { + recipe.ConsumeAll(p.HasItem, p.RemoveItem) + slot.Quantity += outputQty + placed = true + break + } + } + } + if !placed { + 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: recipe.Output, Quantity: outputQty}) + } + + for _, bp := range byproducts { + if slot := p.FirstFreeSlot(); slot >= 0 { + p.SetInvSlot(slot, &player.InventorySlot{ItemID: bp, 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 == "" { + outputName := recipe.Output + if outDef != nil { + outputName = outDef.Name + } + msg = fmt.Sprintf("You produce %s.", outputName) + } + if p.OptionBool("xp_drops") && recipe.XP > 0 { + abbr := player.SkillAbbr[player.SkillName(recipe.Skill)] + msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)) + } + sess.WriteLine(msg) + } 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 fail and the materials are lost." + } + sess.WriteLine(g.colorize(sess, "damage", msg)) + } + + if remaining > 0 { + remaining-- + p.Action.Data["remaining"] = remaining + if remaining <= 0 { + if endMsg != "" { + sess.WriteLine(fmt.Sprintf("\n%s", endMsg)) + } + g.CancelAction(p) + return false + } + } + + if g.canContinueProduction(p, recipe) { + p.Action.WaitLeft = engine.ToTicks(wait) + return true + } + + if endMsg != "" { + sess.WriteLine(fmt.Sprintf("\n%s", endMsg)) + } + g.CancelAction(p) + return false +} + +func (g *Game) collectByproducts(p *player.Player, recipe *action.RecipeDef) []string { + var byproducts []string + for _, e := range recipe.Consume { + if len(e.Byproducts) == 0 { + continue + } + for i, id := range e.Items { + if p.HasItem(id) && i < len(e.Byproducts) && e.Byproducts[i] != "" { + byproducts = append(byproducts, e.Byproducts[i]) + break + } + } + } + return byproducts +} + +func (g *Game) canContinueProduction(p *player.Player, recipe *action.RecipeDef) bool { + if !recipe.HasAllItemsQty(p.CountItem) { + return false + } + outputQty := recipe.OutputQty + if outputQty <= 0 { + outputQty = 1 + } + outDef, _ := g.ItemStore.Load(recipe.Output) + if outDef != nil && outDef.Stackable { + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot != nil && slot.ItemID == recipe.Output { + return true + } + } + } + return p.FirstFreeSlot() >= 0 +} + +func (g *Game) handleRecipeChoice(sess *net.Session, input string) { + input = strings.TrimSpace(input) + if input == "" { + return + } + + choice, err := strconv.Atoi(input) + if err != nil { + sess.State = net.StateGame + sess.PendingMenu = nil + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + + if len(sess.PendingMenu) > 0 { + menuData := sess.PendingMenu + sess.PendingMenu = nil + if choice <= 0 || choice > len(menuData)+1 { + sess.WriteLine("Invalid choice.") + return + } + sess.State = net.StateGame + if choice == len(menuData)+1 { + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + entry := menuData[choice-1] + + if cid, ok := entry["combine_item"]; ok { + g.promptHowMany(sess, "combine_"+cid) + return + } + + if barID, ok := entry["bar_id"]; ok { + p := sess.Player.(*player.Player) + allRecipes, _ := g.RecipeStore.LoadAll() + g.showSmithTable(sess, p, barID, allRecipes) + return + } + + if rid, ok := entry["recipe_id"]; ok { + g.promptHowMany(sess, rid) + return + } + + g.reprompt(sess) + return + } + + sess.State = net.StateGame + g.reprompt(sess) +} diff --git a/internal/game/action_smelt.go b/internal/game/action_smelt.go deleted file mode 100644 index 75fb8a0..0000000 --- a/internal/game/action_smelt.go +++ /dev/null @@ -1,153 +0,0 @@ -package game - -import ( - "fmt" - "math/rand" - - "thehouseoficarus/internal/action" - "thehouseoficarus/internal/engine" - "thehouseoficarus/internal/net" - "thehouseoficarus/internal/player" -) - -func (g *Game) startSmelt(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 smelt that.", recipe.Level, recipe.Skill)) - return - } - } - - if !recipe.HasAllItemsQty(p.CountItem) { - sess.WriteLine("You don't have the required ores.") - return - } - - if p.FirstFreeSlot() == -1 { - sess.WriteLine("Your inventory is too full!") - return - } - - wait := recipe.Wait - if wait <= 0 { - wait = 4 - } - - outputName := recipe.Output - if def, err := g.ItemStore.Load(recipe.Output); err == nil { - outputName = def.Name - } - - p.Action = &action.Action{ - Type: "smelt", - 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: ActionSmelting, TargetName: outputName} -} - -func (g *Game) advanceSmelt(sess *net.Session, p *player.Player) bool { - recipeID := p.Action.Data["recipe_id"].(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 - }) - sess.WriteLine(fmt.Sprintf("\nYou begin to process %s in the furnace.", firstItem)) - 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 { - 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: recipe.Output, 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 == "" { - outputName := recipe.Output - if def, loadErr := g.ItemStore.Load(recipe.Output); loadErr == nil { - outputName = def.Name - } - msg = fmt.Sprintf("You remove a white hot %s!", outputName) - } - if p.OptionBool("xp_drops") && recipe.XP > 0 { - abbr := player.SkillAbbr[player.SkillName(recipe.Skill)] - msg += g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp %s)", recipe.XP, abbr)) - } - sess.WriteLine(msg) - } else { - recipe.ConsumeAll(p.HasItem, p.RemoveItem) - g.AccountStore.SaveCharacter(p) - - msg := recipe.FailMessage - if msg == "" { - msg = "You fail to smelt a usable bar." - } - sess.WriteLine(g.colorize(sess, "damage", msg)) - } - - if recipe.HasAllItemsQty(p.CountItem) && p.FirstFreeSlot() >= 0 { - p.Action.Data["phase"] = 0 - p.Action.WaitLeft = engine.ToTicks(wait) - return true - } - - sess.WriteLine("\nYou've processed all the ore in your inventory.") - g.CancelAction(p) - return false -} diff --git a/internal/game/action_state.go b/internal/game/action_state.go index 6ce3734..26c22f3 100644 --- a/internal/game/action_state.go +++ b/internal/game/action_state.go @@ -19,8 +19,7 @@ const ( ActionSearching ActionType = "searching" ActionResting ActionType = "resting" ActionWalking ActionType = "walking" - ActionCooking ActionType = "cooking" - ActionSmelting ActionType = "smelting" + ActionProducing ActionType = "producing" ActionEating ActionType = "eating" ) @@ -67,10 +66,8 @@ func (a *ActionState) Description() string { return "resting" case ActionWalking: return "walking somewhere with a purpose!" - case ActionCooking: - return "cooking some " + a.TargetName - case ActionSmelting: - return "smelting some " + a.TargetName + case ActionProducing: + return a.Verb + " some " + a.TargetName case ActionEating: return "eating " + a.TargetName } diff --git a/internal/game/cmd_cook.go b/internal/game/cmd_cook.go index 054a4b0..ce33ccd 100644 --- a/internal/game/cmd_cook.go +++ b/internal/game/cmd_cook.go @@ -2,25 +2,18 @@ package game import ( "fmt" - "strconv" - "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" "thehouseoficarus/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 == "" { + stationDefID, stationName := g.findStation(p.RoomID, []string{"fire", "cooking_range"}) + if stationDefID == "" { sess.WriteLine("You need a fire or cooking range to cook.") return } @@ -32,7 +25,7 @@ func (g *Game) doCook(sess *net.Session, input string) { } if input == "" { - g.showCookMenu(sess, p, allRecipes, stationName) + g.showCookMenu(sess, p, allRecipes, stationDefID, stationName) return } @@ -61,24 +54,23 @@ func (g *Game) doCook(sess *net.Session, input string) { if r.Type != "cooking" { continue } - if stationOK(stationName, r.Station) && r.MatchesEntry(itemID) && r.HasAllItems(p.HasItem) { + if stationMatch(stationDefID, 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.") + sess.WriteLine("You can't cook that.") return } if len(recipes) == 1 { - g.startCook(sess, p, &recipes[0], stationName) + g.promptHowMany(sess, recipes[0].ID) return } - sess.PendingRecipeItem = itemID - sess.PendingCookMenu = recipesToMenuData(recipes) - sess.State = net.StateCookRecipe + sess.PendingMenu = recipeMenuData(recipes) + sess.State = net.StateRecipeChoice 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))) @@ -86,15 +78,15 @@ func (g *Game) doCook(sess *net.Session, input string) { sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(recipes)+1)) } -func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationName string) { +func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationDefID, stationName string) { seen := make(map[string]bool) - var cookables []cookableEntry + var entries []recipeEntry for _, r := range allRecipes { if r.Type != "cooking" { continue } - if !stationOK(stationName, r.Station) { + if !stationMatch(stationDefID, r.Station) { continue } if seen[r.ID] { @@ -104,28 +96,26 @@ func (g *Game) showCookMenu(sess *net.Session, p *player.Player, allRecipes []ac continue } seen[r.ID] = true - cookables = append(cookables, cookableEntry{g.recipeName(sess, &r), r}) + entries = append(entries, recipeEntry{g.recipeName(sess, &r), r}) } - if len(cookables) == 0 { + if len(entries) == 0 { sess.WriteLine("You don't have anything you can cook.") return } - if len(cookables) == 1 { - g.startCook(sess, p, &cookables[0].Recipe, stationName) + if len(entries) == 1 { + g.promptHowMany(sess, entries[0].Recipe.ID) return } - sess.PendingRecipeItem = "" - sess.State = net.StateCookRecipe + sess.State = net.StateRecipeChoice 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)) + for i, e := range entries { + sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, e.ItemName)) } - sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(cookables)+1)) - - sess.PendingCookMenu = makeCookMenuData(cookables) + sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(entries)+1)) + sess.PendingMenu = entryMenuData(entries) } func (g *Game) recipeName(sess *net.Session, r *action.RecipeDef) string { @@ -136,111 +126,3 @@ func (g *Game) recipeName(sess *net.Session, r *action.RecipeDef) string { } 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_look.go b/internal/game/cmd_look.go index 77c8776..9809e9b 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -469,8 +469,8 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { sess.WriteLine(def.Name) } - if desc, ok := def.Props["description"].(string); ok && desc != "" { - sess.WriteLine(fmt.Sprintf(" %s", desc)) + if def.Description != "" && !strings.Contains(def.Description, "{quality}") { + sess.WriteLine(fmt.Sprintf(" %s", def.Description)) } if p.OptionBool("depletion") { @@ -491,11 +491,9 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { } } for _, ist := range instances { - if ist.Quality > 0 { - if qdesc, ok := def.Props["quality_description"].(string); ok && qdesc != "" { - qdesc = strings.ReplaceAll(qdesc, "{quality}", fmt.Sprintf("%.0f", ist.Quality)) - sess.WriteLine(fmt.Sprintf(" %s", qdesc)) - } + if ist.Quality > 0 && strings.Contains(def.Description, "{quality}") { + desc := strings.ReplaceAll(def.Description, "{quality}", fmt.Sprintf("%.0f", ist.Quality)) + sess.WriteLine(fmt.Sprintf(" %s", desc)) } } return diff --git a/internal/game/cmd_smelt.go b/internal/game/cmd_smelt.go index a308352..e0889f6 100644 --- a/internal/game/cmd_smelt.go +++ b/internal/game/cmd_smelt.go @@ -2,25 +2,18 @@ package game import ( "fmt" - "strconv" - "strings" "thehouseoficarus/internal/action" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) -type smeltableEntry struct { - ItemName string - Recipe action.RecipeDef -} - func (g *Game) doSmelt(sess *net.Session, input string) { p := sess.Player.(*player.Player) g.CancelAction(p) - stationName := g.findFurnace(p.RoomID) - if stationName == "" { + stationDefID, stationName := g.findStation(p.RoomID, []string{"furnace"}) + if stationDefID == "" { sess.WriteLine("You need a furnace to smelt.") return } @@ -32,7 +25,7 @@ func (g *Game) doSmelt(sess *net.Session, input string) { } if input == "" { - g.showSmeltMenu(sess, p, allRecipes, stationName) + g.showSmeltMenu(sess, p, allRecipes, stationDefID, stationName) return } @@ -55,38 +48,27 @@ func (g *Game) doSmelt(sess *net.Session, input string) { var recipes []action.RecipeDef for _, r := range allRecipes { - if r.Type != "smelting" { - continue - } - if !smeltStationOK(stationName, r.Station) { + if r.Type != "smelting" || !stationMatch(stationDefID, r.Station) || !r.MatchesEntry(itemID) { continue } - if !r.MatchesEntry(itemID) { - continue - } - if !r.HasAllItemsQty(p.CountItem) { - continue - } - skillLevel := p.Level(player.SkillName(r.Skill)) - if skillLevel < r.Level { + if !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.Skill)) < r.Level { continue } recipes = append(recipes, r) } if len(recipes) == 0 { - sess.WriteLine("You can't smelt that here.") + sess.WriteLine("You can't smelt that.") return } if len(recipes) == 1 { - g.startSmelt(sess, p, &recipes[0], stationName) + g.promptHowMany(sess, recipes[0].ID) return } - sess.PendingRecipeItem = itemID - sess.PendingCookMenu = smeltRecipesToMenuData(recipes) - sess.State = net.StateSmeltRecipe + sess.PendingMenu = recipeMenuData(recipes) + sess.State = net.StateRecipeChoice sess.WriteLine("\nWhat would you like to smelt?") for i, r := range recipes { sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r))) @@ -94,137 +76,36 @@ func (g *Game) doSmelt(sess *net.Session, input string) { sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(recipes)+1)) } -func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationName string) { +func (g *Game) showSmeltMenu(sess *net.Session, p *player.Player, allRecipes []action.RecipeDef, stationDefID, stationName string) { seen := make(map[string]bool) - var smeltables []smeltableEntry + var entries []recipeEntry for _, r := range allRecipes { - if r.Type != "smelting" { - continue - } - if !smeltStationOK(stationName, r.Station) { - continue - } - if seen[r.ID] { - continue - } - if !r.HasAllItemsQty(p.CountItem) { + if r.Type != "smelting" || !stationMatch(stationDefID, r.Station) { continue } - skillLevel := p.Level(player.SkillName(r.Skill)) - if skillLevel < r.Level { + if seen[r.ID] || !r.HasAllItemsQty(p.CountItem) || p.Level(player.SkillName(r.Skill)) < r.Level { continue } seen[r.ID] = true - smeltables = append(smeltables, smeltableEntry{g.recipeName(sess, &r), r}) + entries = append(entries, recipeEntry{g.recipeName(sess, &r), r}) } - if len(smeltables) == 0 { + if len(entries) == 0 { sess.WriteLine("You don't have anything you can smelt.") return } - if len(smeltables) == 1 { - g.startSmelt(sess, p, &smeltables[0].Recipe, stationName) + if len(entries) == 1 { + g.promptHowMany(sess, entries[0].Recipe.ID) return } - sess.PendingRecipeItem = "" - sess.State = net.StateSmeltRecipe + sess.State = net.StateRecipeChoice sess.WriteLine("\nWhat would you like to smelt?") - for i, s := range smeltables { - sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, s.ItemName)) - } - sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(smeltables)+1)) - - sess.PendingCookMenu = makeSmeltMenuData(smeltables) -} - -func (g *Game) handleSmeltRecipe(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 smelt 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 smelt anything.") - g.reprompt(sess) - return - } - entry := menuData[choice-1] - - all, loadErr := g.RecipeStore.LoadAll() - if loadErr != nil { - g.reprompt(sess) - return - } - for _, r := range all { - if r.ID == entry["recipe_id"] { - stationName := g.findFurnace(p.RoomID) - g.startSmelt(sess, p, &r, stationName) - return - } - } - g.reprompt(sess) - return - } - - sess.State = net.StateGame - g.reprompt(sess) -} - -func smeltStationOK(stationName string, recipeStations []string) bool { - for _, rs := range recipeStations { - if rs == "furnace" && stationName == "furnace" { - return true - } - } - return false -} - -func (g *Game) findFurnace(roomID int) string { - for _, obj := range g.World.AllObjInstances(roomID) { - if obj.Depleted { - continue - } - if obj.DefID == "furnace" { - return "furnace" - } - } - return "" -} - -func makeSmeltMenuData(smeltables []smeltableEntry) []map[string]string { - out := make([]map[string]string, len(smeltables)) - for i, s := range smeltables { - out[i] = map[string]string{"recipe_id": s.Recipe.ID} - } - return out -} - -func smeltRecipesToMenuData(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} + for i, e := range entries { + sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, e.ItemName)) } - return out + sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(entries)+1)) + sess.PendingMenu = entryMenuData(entries) } diff --git a/internal/game/cmd_smith.go b/internal/game/cmd_smith.go new file mode 100644 index 0000000..30dcddc --- /dev/null +++ b/internal/game/cmd_smith.go @@ -0,0 +1,342 @@ +package game + +import ( + "fmt" + "sort" + "strings" + + "thehouseoficarus/internal/action" + "thehouseoficarus/internal/color" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/player" +) + +func (g *Game) doSmith(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + g.CancelAction(p) + + stationDefID, _ := g.findStation(p.RoomID, []string{"anvil"}) + if stationDefID == "" { + sess.WriteLine("You need an anvil to smith.") + return + } + + if !g.hasToolType(p, "hammer") { + sess.WriteLine("You need a hammer to smith.") + return + } + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + sess.WriteLine("Error loading recipes.") + return + } + + if input != "" { + matches := g.findInventoryMatches(input, p) + if len(matches) == 0 { + sess.WriteLine(fmt.Sprintf("You don't have any '%s'.", input)) + return + } + unique := uniqueItemNames(matches) + if len(unique) > 1 { + sess.WriteLine("Which one?") + for _, name := range unique { + sess.WriteLine(fmt.Sprintf(" - %s", name)) + } + return + } + barID := matches[0].ID + if !hasSmithingRecipes(allRecipes, barID) { + sess.WriteLine("You can't smith that.") + return + } + g.showSmithTable(sess, p, barID, allRecipes) + return + } + + barTypes := findSmithableBars(allRecipes, p) + if len(barTypes) == 0 { + sess.WriteLine("You don't have any bars to smith.") + return + } + + if len(barTypes) == 1 { + g.showSmithTable(sess, p, barTypes[0], allRecipes) + return + } + + sess.PendingMenu = barMenuData(barTypes) + sess.State = net.StateRecipeChoice + sess.WriteLine("\nWhich bars would you like to smith?") + for i, barID := range barTypes { + def, _ := g.ItemStore.Load(barID) + name := barID + if def != nil { + name = g.itemColorize(sess, def, def.Name) + } + sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, name)) + } + sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(barTypes)+1)) +} + +func (g *Game) showSmithTable(sess *net.Session, p *player.Player, barID string, allRecipes []action.RecipeDef) { + barDef, _ := g.ItemStore.Load(barID) + barName := barID + if barDef != nil { + barName = barDef.Name + } + + var recipes []action.RecipeDef + for _, r := range allRecipes { + if r.Type == "smithing" && r.MatchesEntry(barID) { + recipes = append(recipes, r) + } + } + + if len(recipes) == 0 { + sess.WriteLine("There's nothing to smith with that.") + return + } + + sort.Slice(recipes, func(i, j int) bool { + return recipes[i].Level < recipes[j].Level + }) + + mode := g.colorMode(sess) + skillLevel := p.Level(player.Smithing) + barCount := p.CountItem(barID) + dimSpec := color.Parse("240") + + tbl := &Table{ + Title: titleCase(barName) + " Smithing", + Columns: []string{"Product", "Inputs", "Level Req."}, + } + + for _, r := range recipes { + outDef, _ := g.ItemStore.Load(r.Output) + productName := r.Output + if outDef != nil { + productName = outDef.Name + } + + bars := barsRequired(r, barID) + inputStr := "1 bar" + if bars > 1 { + inputStr = fmt.Sprintf("%d bars", bars) + } + levelStr := fmt.Sprint(r.Level) + + canMake := barCount >= bars && skillLevel >= r.Level + if canMake { + productName = g.itemColorize(sess, outDef, productName) + } else { + productName = color.Render(mode, dimSpec, productName) + inputStr = color.Render(mode, dimSpec, inputStr) + levelStr = color.Render(mode, dimSpec, levelStr) + } + + tbl.Rows = append(tbl.Rows, []string{productName, inputStr, levelStr}) + } + + unicode := p.OptionBool("unicode") + sess.WriteLine("") + for _, line := range tbl.Render(unicode) { + sess.WriteLine(line) + } + + lastFlag := fmt.Sprintf("last_smith_%s", barID) + lastRecipeID, _ := p.Flags[lastFlag].(string) + hint := "" + if lastRecipeID != "" { + for _, r := range recipes { + if r.ID == lastRecipeID { + if outDef, err := g.ItemStore.Load(r.Output); err == nil { + hint = outDef.Name + } + break + } + } + } + + if hint != "" { + sess.Write(fmt.Sprintf("Produce what (enter for all %s): ", hint)) + } else { + sess.Write("Produce what: ") + } + + sess.PendingRecipeID = barID + sess.State = net.StateSmithProduct +} + +func (g *Game) handleSmithProduct(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + barID := sess.PendingRecipeID + sess.PendingRecipeID = "" + sess.State = net.StateGame + + input = strings.TrimSpace(input) + + allRecipes, err := g.RecipeStore.LoadAll() + if err != nil { + g.reprompt(sess) + return + } + var recipes []action.RecipeDef + for _, r := range allRecipes { + if r.Type == "smithing" && r.MatchesEntry(barID) { + recipes = append(recipes, r) + } + } + + var targetRecipe *action.RecipeDef + var count int + + if input == "" { + lastFlag := fmt.Sprintf("last_smith_%s", barID) + lastRecipeID, _ := p.Flags[lastFlag].(string) + if lastRecipeID == "" { + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + for i := range recipes { + if recipes[i].ID == lastRecipeID { + targetRecipe = &recipes[i] + break + } + } + if targetRecipe == nil { + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + count = 0 + } else { + qty, productName := parseQty(input) + count = qty + productName = strings.ToLower(strings.TrimSpace(productName)) + + for i := range recipes { + outDef, _ := g.ItemStore.Load(recipes[i].Output) + name := recipes[i].Output + if outDef != nil { + name = outDef.Name + } + lower := strings.ToLower(name) + if lower == productName || strings.HasPrefix(lower, productName) { + targetRecipe = &recipes[i] + break + } + } + + if targetRecipe == nil { + sess.WriteLine("Never mind.") + g.reprompt(sess) + return + } + } + + skillLevel := p.Level(player.Smithing) + if skillLevel < targetRecipe.Level { + sess.WriteLine(fmt.Sprintf("You need level %d smithing to make that.", targetRecipe.Level)) + g.reprompt(sess) + return + } + if !targetRecipe.HasAllItemsQty(p.CountItem) { + sess.WriteLine("You don't have enough bars.") + g.reprompt(sess) + return + } + + if p.Flags == nil { + p.Flags = make(map[string]any) + } + p.Flags[fmt.Sprintf("last_smith_%s", barID)] = targetRecipe.ID + g.AccountStore.SaveCharacter(p) + + g.startProductionFromRecipe(sess, p, targetRecipe, count) +} + +func (g *Game) hasToolType(p *player.Player, toolType string) bool { + if itemID, ok := p.Equipment[object.SlotMainHand]; ok { + if def, err := g.ItemStore.Load(itemID); err == nil && def.ToolType == toolType { + return true + } + } + for i := 0; i < 28; i++ { + slot := p.InvSlot(i) + if slot == nil { + continue + } + if def, err := g.ItemStore.Load(slot.ItemID); err == nil && def.ToolType == toolType { + return true + } + } + return false +} + +func hasSmithingRecipes(allRecipes []action.RecipeDef, barID string) bool { + for _, r := range allRecipes { + if r.Type == "smithing" && r.MatchesEntry(barID) { + return true + } + } + return false +} + +func findSmithableBars(allRecipes []action.RecipeDef, p *player.Player) []string { + barSet := make(map[string]bool) + for _, r := range allRecipes { + if r.Type != "smithing" { + continue + } + for _, e := range r.Consume { + for _, id := range e.Items { + if p.HasItem(id) { + barSet[id] = true + } + } + } + } + var bars []string + for id := range barSet { + bars = append(bars, id) + } + sort.Strings(bars) + return bars +} + +func barsRequired(r action.RecipeDef, barID string) int { + for _, e := range r.Consume { + for _, id := range e.Items { + if id == barID { + qty := e.Qty + if qty <= 0 { + qty = 1 + } + return qty + } + } + } + return 1 +} + +func barMenuData(barIDs []string) []map[string]string { + out := make([]map[string]string, len(barIDs)) + for i, id := range barIDs { + out[i] = map[string]string{"bar_id": id} + } + return out +} + +func titleCase(s string) string { + words := strings.Fields(s) + for i, w := range words { + if len(w) > 0 { + words[i] = strings.ToUpper(w[:1]) + w[1:] + } + } + return strings.Join(words, " ") +} diff --git a/internal/game/cmd_use.go b/internal/game/cmd_use.go index 247df72..cb521f3 100644 --- a/internal/game/cmd_use.go +++ b/internal/game/cmd_use.go @@ -10,6 +10,11 @@ import ( ) func (g *Game) doUse(sess *net.Session, input string) { + if strings.TrimSpace(input) == "" { + g.doHelp(sess, "use") + return + } + p := sess.Player.(*player.Player) g.CancelAction(p) @@ -94,12 +99,11 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, return } if len(filtered) == 1 { - g.startSmelt(sess, p, &filtered[0], def.Name) + g.promptHowMany(sess, filtered[0].ID) return } - sess.PendingRecipeItem = itemAID - sess.PendingCookMenu = smeltRecipesToMenuData(filtered) - sess.State = net.StateSmeltRecipe + sess.PendingMenu = recipeMenuData(filtered) + sess.State = net.StateRecipeChoice sess.WriteLine("\nWhat would you like to smelt?") for i, r := range filtered { sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, g.recipeName(sess, &r))) @@ -107,13 +111,21 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, sess.WriteLine(fmt.Sprintf(" %d) Nothing", len(filtered)+1)) return } + if recipes[0].Type == "smithing" { + if !g.hasToolType(p, "hammer") { + sess.WriteLine("You need a hammer to smith.") + return + } + allRecipes, _ := g.RecipeStore.LoadAll() + g.showSmithTable(sess, p, itemAID, allRecipes) + return + } if len(recipes) == 1 { - g.startCook(sess, p, &recipes[0], def.Name) + g.promptHowMany(sess, recipes[0].ID) return } - sess.PendingRecipeItem = itemAID - sess.PendingCookMenu = recipesToMenuData(recipes) - sess.State = net.StateCookRecipe + sess.PendingMenu = recipeMenuData(recipes) + sess.State = net.StateRecipeChoice 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))) @@ -121,6 +133,22 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, return } + for _, ui := range def.UseInteractions { + if ui.Item != itemAID { + continue + } + if ui.Condition != nil && !g.checkCondition(sess, ui.Condition) { + continue + } + if ui.Message != "" { + sess.WriteLine(ui.Message) + } + if ui.Action != nil { + g.applyNodeAction(sess, ui.Action) + } + return + } + g.StartAction(sess, "use", itemBName) return } @@ -140,12 +168,12 @@ func (g *Game) doUseItemOnTarget(sess *net.Session, p *player.Player, itemAName, matched := g.findCombineResults(sess, p, itemAID, itemBID) if len(matched) == 1 { - g.produceCombined(sess, p, matched[0]) + g.promptHowMany(sess, "combine_"+matched[0].ID) return } if len(matched) > 1 { - sess.PendingCookMenu = combineMenuData(matched) - sess.State = net.StateCookRecipe + sess.PendingMenu = combineMenuData(matched) + sess.State = net.StateRecipeChoice 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))) @@ -189,25 +217,6 @@ func (g *Game) findCombineResults(sess *net.Session, p *player.Player, itemAID, 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 { diff --git a/internal/game/game.go b/internal/game/game.go index 49d34b6..87539b1 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -122,10 +122,12 @@ 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) - case net.StateSmeltRecipe: - g.handleSmeltRecipe(sess, input) + case net.StateRecipeChoice: + g.handleRecipeChoice(sess, input) + case net.StateHowMany: + g.handleHowMany(sess, input) + case net.StateSmithProduct: + g.handleSmithProduct(sess, input) case net.StateColorChoice: g.handleColorChoice(sess, input) } @@ -145,7 +147,7 @@ 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", "cook", "smelt": + "quit", "use", "burn", "stoke", "search", "walk", "cook", "smelt", "smith": return ClassActive case "eat": return ClassFree @@ -371,6 +373,9 @@ func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawI case "smelt": g.doSmelt(sess, strings.Join(args, " ")) return + case "smith": + g.doSmith(sess, strings.Join(args, " ")) + return case "eat": g.doEat(sess, strings.Join(args, " ")) return @@ -459,7 +464,7 @@ func (g *Game) ProcessQueuedCommands() { } switch as.Type { case ActionGathering, ActionCombating, ActionUsing, ActionTalking, - ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionCooking, ActionSmelting: + ActionToggling, ActionBurning, ActionStoking, ActionResting, ActionWalking, ActionProducing: default: if as.Type != ActionMoving || p.MoveTicks <= 0 { p.ActionState = nil diff --git a/internal/game/stations.go b/internal/game/stations.go new file mode 100644 index 0000000..d30a35c --- /dev/null +++ b/internal/game/stations.go @@ -0,0 +1,28 @@ +package game + +func (g *Game) findStation(roomID int, stationIDs []string) (defID string, displayName string) { + for _, obj := range g.World.AllObjInstances(roomID) { + if obj.Depleted { + continue + } + for _, sid := range stationIDs { + if obj.DefID == sid { + name := sid + if def, err := g.ObjectStore.Load(sid); err == nil { + name = def.Name + } + return sid, name + } + } + } + return "", "" +} + +func stationMatch(defID string, recipeStations []string) bool { + for _, rs := range recipeStations { + if rs == defID { + return true + } + } + return false +} diff --git a/internal/net/server.go b/internal/net/server.go index f22c398..bb14b04 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -31,8 +31,9 @@ const ( StateChangeDescription StateTalk StateDropAllConfirm - StateCookRecipe - StateSmeltRecipe + StateRecipeChoice + StateHowMany + StateSmithProduct StateColorChoice ) @@ -43,8 +44,8 @@ type Session struct { Player interface{} PendingChar string PendingPass string - PendingRecipeItem string - PendingCookMenu []map[string]string + PendingMenu []map[string]string + PendingRecipeID string Disconnecting bool DisconnectTicks int } diff --git a/internal/object/item.go b/internal/object/item.go index efcb2c5..a6a06e1 100644 --- a/internal/object/item.go +++ b/internal/object/item.go @@ -55,11 +55,13 @@ type ItemDef struct { HealValue int `yaml:"heal_value"` EatMessage string `yaml:"eat_message"` MadeFrom []MadeFromEntry `yaml:"made_from,omitempty"` + Ticks float64 `yaml:"ticks"` } type MadeFromEntry struct { - Items []string `yaml:"items"` - Qty int `yaml:"qty"` + Items []string `yaml:"items"` + Qty int `yaml:"qty"` + Byproducts []string `yaml:"byproducts"` } type ItemStats struct { diff --git a/internal/object/object.go b/internal/object/object.go index 46e460e..fe55865 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -1,12 +1,22 @@ package object +import "thehouseoficarus/internal/action" + +type UseInteraction struct { + Item string `yaml:"item"` + Condition *action.Condition `yaml:"condition"` + Message string `yaml:"message"` + Action *action.NodeAction `yaml:"action"` +} + type ObjectDef struct { - ID string `yaml:"id"` - Name string `yaml:"name"` - Color string `yaml:"color"` - BehaviorID string `yaml:"behavior"` - Hidden bool `yaml:"hidden"` - InRoomDescription string `yaml:"inroom_description"` - RemovalItem string `yaml:"removal_item"` - Props map[string]any `yaml:"props"` + ID string `yaml:"id"` + Name string `yaml:"name"` + Color string `yaml:"color"` + BehaviorID string `yaml:"behavior"` + Hidden bool `yaml:"hidden"` + InRoomDescription string `yaml:"inroom_description"` + RemovalItem string `yaml:"removal_item"` + Description string `yaml:"description"` + UseInteractions []UseInteraction `yaml:"use_interactions"` } diff --git a/worldbuilding_guide/hidden_objects.md b/worldbuilding_guide/hidden_objects.md index 3cb8a1e..a191051 100644 --- a/worldbuilding_guide/hidden_objects.md +++ b/worldbuilding_guide/hidden_objects.md @@ -8,8 +8,7 @@ id: secret_lever name: stone lever behavior: secret_toggle hidden: true -props: - description: "A cleverly concealed lever behind a loose stone." +description: "A cleverly concealed lever behind a loose stone." ``` Room description hints at it: diff --git a/worldbuilding_guide/items.md b/worldbuilding_guide/items.md index e9e4352..0a4592b 100644 --- a/worldbuilding_guide/items.md +++ b/worldbuilding_guide/items.md @@ -65,17 +65,51 @@ 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. +Define what items combine to make this item. Used by the `use` command. Combinations run through the production system — players are prompted "How many?" and the action loops with a timer. ```yaml id: bread_dough name: bread dough color: "222" +ticks: 2 made_from: - items: [pot_of_flour] qty: 1 + byproducts: [empty_pot] - items: [bucket_of_water, pitcher_of_water] qty: 1 + byproducts: [empty_bucket, empty_pitcher] ``` -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. +Each entry is an ingredient slot. Multiple `items` means any of them works. The first matching item found in inventory is consumed. + +`ticks` controls how many game ticks each production cycle takes (default 2). + +### Byproducts + +`byproducts` is a list matching the `items` list by index. When `bucket_of_water` (items[0]) is consumed, `empty_bucket` (byproducts[0]) is returned to inventory. When `pitcher_of_water` (items[1]) is consumed, `empty_pitcher` (byproducts[1]) is returned instead. + +Use `""` for items that produce no byproduct: + +```yaml +made_from: + - items: [pot_of_flour, loose_flour] + qty: 1 + byproducts: [empty_pot, ""] # pot returns empty_pot, loose flour returns nothing + - items: [bucket_of_water] + qty: 1 + byproducts: [empty_bucket] +``` + +Omit `byproducts` entirely if no items in that slot produce a byproduct: + +```yaml +made_from: + - items: [pot_of_flour] + qty: 1 + byproducts: [empty_pot] # returns empty_pot + - items: [salt] + qty: 1 # no byproducts field — salt is consumed entirely +``` + +See also `recipes.md` for station-based recipes. diff --git a/worldbuilding_guide/objects.md b/worldbuilding_guide/objects.md index 6929606..3644a18 100644 --- a/worldbuilding_guide/objects.md +++ b/worldbuilding_guide/objects.md @@ -24,18 +24,98 @@ id: iron_gate name: iron gate behavior: iron_gate_toggle hidden: true -props: - description: "A heavy iron gate set into the north wall." +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." +description: "Clear water sparkles in the sunlight." ``` --- +## Use Interactions + +Objects can define `use_interactions` to handle when a player uses a specific item on the object. Each interaction can check conditions, show a message, and execute actions — using the same condition and action primitives as the talk system. + +Entries are checked top-to-bottom; the first match with a passing condition wins. + +Simple message (no action): +```yaml +id: anvil +name: anvil +use_interactions: + - item: silver_bar + message: "You should use this with a mold at a furnace." + - item: gold_bar + message: "You should use this with a mold at a furnace." +``` + +Puzzle interaction (take item, set flag): +```yaml +id: crystal_slot +name: crystal slot +use_interactions: + - item: crystal_key + condition: + flag: crystal_inserted + message: "The crystal key is already in the slot." + - item: crystal_key + message: "You insert the crystal key into the slot. It clicks into place." + action: + take_item: crystal_key + set_flags: + crystal_inserted: true +``` + +Quest item exchange: +```yaml +use_interactions: + - item: ancient_scroll + condition: + player_flag: quest_started + message: "You place the scroll on the pedestal. The door rumbles open!" + action: + take_item: ancient_scroll + set_player_flags: + quest_complete: true + set_flags: + temple_door_open: true +``` + +### UseInteraction Fields + +| Field | Type | Description | +| ----------- | ---------- | ------------------------------------------------ | +| `item` | string | Item ID that triggers this interaction | +| `condition` | Condition | Optional condition (same as exits/talk/toggle) | +| `message` | string | Message shown to the player | +| `action` | NodeAction | Optional actions (same as talk node actions) | + +### Available Actions (same as talk) + +| Field | Type | Description | +| ------------------ | -------------- | ---------------------------------- | +| `set_flags` | map[string]any | Set world flags (shared) | +| `set_player_flags` | map[string]any | Set player flags (per-character) | +| `give_item` | string | Give an item to inventory | +| `take_item` | string | Remove an item from inventory | +| `teleport` | int | Move player to a room ID | +| `heal` | int | Restore hitpoints | + +### Available Conditions (same as exits/talk) + +| Field | Description | +| ------------- | -------------------------------- | +| `flag` | World flag check | +| `player_flag` | Per-character flag check | +| `has_item` | Inventory item check | +| `value` | Expected value for flag checks | +| `not` | Invert the condition | +| `all_of` | All sub-conditions must pass | +| `any_of` | Any sub-condition must pass | + +--- + diff --git a/worldbuilding_guide/recipes.md b/worldbuilding_guide/recipes.md index 500954f..799dab0 100644 --- a/worldbuilding_guide/recipes.md +++ b/worldbuilding_guide/recipes.md @@ -24,6 +24,7 @@ Recipe files live in `data/recipes/.yaml`. | `station` | []string | Station object IDs required | | `consume` | []ConsumeEntry | Ingredients consumed (see below) | | `output` | string | ItemID produced on success | +| `output_qty` | int | Quantity produced (default 1, for stackables) | | `fail` | string | ItemID produced on failure (optional) | | `message` | string | Success message | | `fail_message` | string | Failure message | @@ -39,6 +40,18 @@ Each consume entry defines an ingredient slot with multiple valid items. The fir consume: - items: [item_id, alternative_id, ...] qty: 1 + byproducts: [byproduct_for_item, byproduct_for_alt, ...] +``` + +`byproducts` is optional. When present, it matches the `items` list by index — consuming `items[0]` returns `byproducts[0]` to inventory. Use `""` for items with no byproduct: + +```yaml +consume: + - items: [bucket_of_water, vial_of_water] + qty: 1 + byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely + - items: [herb] + qty: 1 # no byproducts — herb is consumed entirely ``` ### Station-based recipe (cooking on a fire) @@ -79,6 +92,74 @@ message: "You bake the dough into a fresh loaf of bread." fail_message: "You burn the bread to a crisp." ``` +### Smelting recipe (ore to bar at a furnace) + +Smelting recipes use `type: smelting` and `station: [furnace]`. The `smelt` command or `use on furnace` triggers them. Multi-ore recipes use `qty` on consume entries. + +```yaml +id: smelt_steel +type: smelting +skill: smithing +level: 40 +xp: 17 +wait: 4 +station: [furnace] +consume: + - items: [iron_ore] + qty: 1 + - items: [coal] + qty: 2 +output: steel_bar +message: "You remove a white hot steel bar!" +fail_message: "You fail to smelt a usable bar." +``` + +Iron smelting uses a `success` formula for its 50% failure rate: + +```yaml +success: + base: 0.5 + per_level: 0 + cap: 0.5 +``` + +### Smithing recipe (bar to item at an anvil) + +Smithing recipes use `type: smithing` and `station: [anvil]`. The `smith` command or `use on anvil` triggers them. Requires a hammer (`tool_type: hammer`) in inventory. + +```yaml +id: smith_steel_platebody +type: smithing +skill: smithing +level: 48 +xp: 187 +wait: 4 +station: [anvil] +consume: + - items: [steel_bar] + qty: 5 +output: steel_platebody +message: "You hammer out a steel platebody." +``` + +For stackable outputs, use `output_qty`: + +```yaml +id: smith_iron_nails +type: smithing +skill: smithing +level: 24 +xp: 25 +wait: 4 +station: [anvil] +consume: + - items: [iron_bar] + qty: 1 +output: iron_nails +output_qty: 15 +message: "You hammer out some iron nails." +``` + ### 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. -- cgit v1.2.3