aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-17 21:13:07 -0400
committerhistoria <[not public]>2026-06-17 21:13:07 -0400
commitec2e94e3463654a6288464a4c070b48ef9bf7368 (patch)
tree3c5df0f9a6a5968fa4c725a7d331825883ae798c
parenteef5ddaa94f8cff6010d092c30fed53d2be01524 (diff)
downloadthehouseoficarus-ec2e94e3463654a6288464a4c070b48ef9bf7368.tar.gz
feat: smithing added, item/object interaction reworked. recipes and menus refactored.
-rw-r--r--AGENTS.md37
-rw-r--r--data/help/cook.yaml18
-rw-r--r--data/help/equipment.yaml11
-rw-r--r--data/help/recipe.yaml29
-rw-r--r--data/help/smelt.yaml22
-rw-r--r--data/help/smith.yaml20
-rw-r--r--data/help/smithing.yaml26
-rw-r--r--data/help/use.yaml28
-rw-r--r--data/help/wear.yaml16
-rw-r--r--data/items/adamant_arrowtips.yaml6
-rw-r--r--data/items/adamant_bolts_unf.yaml6
-rw-r--r--data/items/adamant_dagger.yaml12
-rw-r--r--data/items/adamant_full_helm.yaml9
-rw-r--r--data/items/adamant_med_helm.yaml9
-rw-r--r--data/items/adamant_nails.yaml6
-rw-r--r--data/items/adamant_platebody.yaml9
-rw-r--r--data/items/adamant_sword.yaml12
-rw-r--r--data/items/bread_dough.yaml5
-rw-r--r--data/items/bronze_arrowtips.yaml6
-rw-r--r--data/items/bronze_bolts_unf.yaml6
-rw-r--r--data/items/bronze_dagger.yaml12
-rw-r--r--data/items/bronze_full_helm.yaml9
-rw-r--r--data/items/bronze_med_helm.yaml9
-rw-r--r--data/items/bronze_nails.yaml6
-rw-r--r--data/items/bronze_platebody.yaml9
-rw-r--r--data/items/bronze_sword.yaml7
-rw-r--r--data/items/empty_bucket.yaml6
-rw-r--r--data/items/empty_pitcher.yaml6
-rw-r--r--data/items/empty_pot.yaml6
-rw-r--r--data/items/hammer.yaml7
-rw-r--r--data/items/iron_arrowtips.yaml6
-rw-r--r--data/items/iron_bolts_unf.yaml6
-rw-r--r--data/items/iron_dagger.yaml12
-rw-r--r--data/items/iron_full_helm.yaml9
-rw-r--r--data/items/iron_med_helm.yaml9
-rw-r--r--data/items/iron_nails.yaml6
-rw-r--r--data/items/iron_platebody.yaml9
-rw-r--r--data/items/iron_sword.yaml12
-rw-r--r--data/items/mithril_arrowtips.yaml6
-rw-r--r--data/items/mithril_bolts_unf.yaml6
-rw-r--r--data/items/mithril_dagger.yaml12
-rw-r--r--data/items/mithril_full_helm.yaml9
-rw-r--r--data/items/mithril_med_helm.yaml9
-rw-r--r--data/items/mithril_nails.yaml6
-rw-r--r--data/items/mithril_platebody.yaml9
-rw-r--r--data/items/mithril_sword.yaml12
-rw-r--r--data/items/pitcher_of_water.yaml6
-rw-r--r--data/items/rune_arrowtips.yaml6
-rw-r--r--data/items/rune_bolts_unf.yaml6
-rw-r--r--data/items/rune_dagger.yaml12
-rw-r--r--data/items/rune_full_helm.yaml9
-rw-r--r--data/items/rune_med_helm.yaml9
-rw-r--r--data/items/rune_nails.yaml6
-rw-r--r--data/items/rune_platebody.yaml9
-rw-r--r--data/items/rune_sword.yaml12
-rw-r--r--data/items/steel_arrowtips.yaml6
-rw-r--r--data/items/steel_bolts_unf.yaml6
-rw-r--r--data/items/steel_dagger.yaml12
-rw-r--r--data/items/steel_full_helm.yaml9
-rw-r--r--data/items/steel_med_helm.yaml9
-rw-r--r--data/items/steel_nails.yaml6
-rw-r--r--data/items/steel_platebody.yaml9
-rw-r--r--data/items/steel_sword.yaml12
-rw-r--r--data/objects/anglerfish_spot.yaml3
-rw-r--r--data/objects/anvil.yaml9
-rw-r--r--data/objects/bait_fishing_spot.yaml3
-rw-r--r--data/objects/cage_fishing_spot.yaml3
-rw-r--r--data/objects/cooking_range.yaml3
-rw-r--r--data/objects/fire.yaml4
-rw-r--r--data/objects/fly_fishing_spot.yaml3
-rw-r--r--data/objects/furnace.yaml3
-rw-r--r--data/objects/harpoon_fishing_spot.yaml3
-rw-r--r--data/objects/iron_gate.yaml3
-rw-r--r--data/objects/lumby_fountain.yaml3
-rw-r--r--data/objects/net_fishing_spot.yaml3
-rw-r--r--data/objects/sacred_eel_spot.yaml3
-rw-r--r--data/objects/swamp_fishing_spot.yaml3
-rw-r--r--data/recipes/smith_adamant_arrowtips.yaml13
-rw-r--r--data/recipes/smith_adamant_bolts_unf.yaml13
-rw-r--r--data/recipes/smith_adamant_dagger.yaml12
-rw-r--r--data/recipes/smith_adamant_full_helm.yaml12
-rw-r--r--data/recipes/smith_adamant_med_helm.yaml12
-rw-r--r--data/recipes/smith_adamant_nails.yaml13
-rw-r--r--data/recipes/smith_adamant_platebody.yaml12
-rw-r--r--data/recipes/smith_adamant_sword.yaml12
-rw-r--r--data/recipes/smith_bronze_arrowtips.yaml13
-rw-r--r--data/recipes/smith_bronze_bolts_unf.yaml13
-rw-r--r--data/recipes/smith_bronze_dagger.yaml12
-rw-r--r--data/recipes/smith_bronze_full_helm.yaml12
-rw-r--r--data/recipes/smith_bronze_med_helm.yaml12
-rw-r--r--data/recipes/smith_bronze_nails.yaml13
-rw-r--r--data/recipes/smith_bronze_platebody.yaml12
-rw-r--r--data/recipes/smith_bronze_sword.yaml12
-rw-r--r--data/recipes/smith_iron_arrowtips.yaml13
-rw-r--r--data/recipes/smith_iron_bolts_unf.yaml13
-rw-r--r--data/recipes/smith_iron_dagger.yaml12
-rw-r--r--data/recipes/smith_iron_full_helm.yaml12
-rw-r--r--data/recipes/smith_iron_med_helm.yaml12
-rw-r--r--data/recipes/smith_iron_nails.yaml13
-rw-r--r--data/recipes/smith_iron_platebody.yaml12
-rw-r--r--data/recipes/smith_iron_sword.yaml12
-rw-r--r--data/recipes/smith_mithril_arrowtips.yaml13
-rw-r--r--data/recipes/smith_mithril_bolts_unf.yaml13
-rw-r--r--data/recipes/smith_mithril_dagger.yaml12
-rw-r--r--data/recipes/smith_mithril_full_helm.yaml12
-rw-r--r--data/recipes/smith_mithril_med_helm.yaml12
-rw-r--r--data/recipes/smith_mithril_nails.yaml13
-rw-r--r--data/recipes/smith_mithril_platebody.yaml12
-rw-r--r--data/recipes/smith_mithril_sword.yaml12
-rw-r--r--data/recipes/smith_rune_arrowtips.yaml13
-rw-r--r--data/recipes/smith_rune_bolts_unf.yaml13
-rw-r--r--data/recipes/smith_rune_dagger.yaml12
-rw-r--r--data/recipes/smith_rune_full_helm.yaml12
-rw-r--r--data/recipes/smith_rune_med_helm.yaml12
-rw-r--r--data/recipes/smith_rune_nails.yaml13
-rw-r--r--data/recipes/smith_rune_platebody.yaml12
-rw-r--r--data/recipes/smith_rune_sword.yaml12
-rw-r--r--data/recipes/smith_steel_arrowtips.yaml13
-rw-r--r--data/recipes/smith_steel_bolts_unf.yaml13
-rw-r--r--data/recipes/smith_steel_dagger.yaml12
-rw-r--r--data/recipes/smith_steel_full_helm.yaml12
-rw-r--r--data/recipes/smith_steel_med_helm.yaml12
-rw-r--r--data/recipes/smith_steel_nails.yaml13
-rw-r--r--data/recipes/smith_steel_platebody.yaml12
-rw-r--r--data/recipes/smith_steel_sword.yaml12
-rw-r--r--data/rooms/12.yaml4
-rw-r--r--internal/action/recipe.go6
-rw-r--r--internal/game/action.go8
-rw-r--r--internal/game/action_cook.go153
-rw-r--r--internal/game/action_production.go467
-rw-r--r--internal/game/action_smelt.go153
-rw-r--r--internal/game/action_state.go9
-rw-r--r--internal/game/cmd_cook.go158
-rw-r--r--internal/game/cmd_look.go12
-rw-r--r--internal/game/cmd_smelt.go163
-rw-r--r--internal/game/cmd_smith.go342
-rw-r--r--internal/game/cmd_use.go69
-rw-r--r--internal/game/game.go17
-rw-r--r--internal/game/stations.go28
-rw-r--r--internal/net/server.go9
-rw-r--r--internal/object/item.go6
-rw-r--r--internal/object/object.go26
-rw-r--r--worldbuilding_guide/hidden_objects.md3
-rw-r--r--worldbuilding_guide/items.md38
-rw-r--r--worldbuilding_guide/objects.md90
-rw-r--r--worldbuilding_guide/recipes.md81
146 files changed, 2380 insertions, 757 deletions
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 <text>` | 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 <target>` / `l <target>` | 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 <item>` | 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 <item>` | Equip item to correct slot. `equip all` auto-equips best per slot |
| `remove` / `unwear` / `unwield <item>` | Unequip item back to inventory. `remove all` unequips everything |
| `eat <item>` | 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 <object>` | Gathering with smart default (verbAliases map to "gather") |
| `use <item> [on/with] <target>` | Universal item interaction: recipes, crafting, gathering shortcuts |
| `cook <item>` | Cook raw food on a fire or cooking range (Cooking skill) |
+| `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 <target>` | NPC conversations (objects or mobs with behavior) |
| `pull` / `push <object>` | Toggle interactions (levers, gates) |
| `search <item>` | Search bird's nests / searchable items (rolls on search_table) |
@@ -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_<skill>.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 <item> 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 <item>" or "wield <item>" to equip items from your inventory.
+ Shows what you have in each slot (main hand, head, torso, etc.).
Use "remove <item>" 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 <item> Cook on a fire or range
- use <item> on <object> Use an item on a cooking station
+ smelt <item> Smelt ore at a furnace
+ smith <item> Smith bars at an anvil
+ use <item> on <object> Use an item on a station
Item combinations — combine items directly from inventory:
use <item> with <item> 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 <item> 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 <bar> 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 <item> Use an item on a room object
use <item> on <target> Use item on a specific target
- use <item> with <item> Combine two inventory items (shorthand)
+ use <item> with <item> 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 <item>, wield <item>
- 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 <item> 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/<id>.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 <ore> 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 <bar> 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.