diff --git a/autoload/strings.gd b/autoload/strings.gd index 584a799..32e3141 100644 --- a/autoload/strings.gd +++ b/autoload/strings.gd @@ -39,6 +39,9 @@ const TABLE: Dictionary = { # Phase 6 — new item types (carpenter bench + smelter outputs) &"item.plank": "Plank", &"item.stone_block": "Stone block", + # Phase 19 — smelted ingots + &"item.iron_ingot": "Iron ingot", + &"item.gold_ingot": "Gold ingot", # Phase 7 — food loop and cooking chain item types &"item.flour": "Flour", &"item.bread": "Bread", diff --git a/scenes/ai/recipe.gd b/scenes/ai/recipe.gd index 5f7ebc8..a672521 100644 --- a/scenes/ai/recipe.gd +++ b/scenes/ai/recipe.gd @@ -46,6 +46,13 @@ var skill_threshold: int = 0 ## call Strings.t("item." + id) for player-visible text. var label: String = "" +## Optional workbench affinity. When non-empty, the bill picker only shows this +## recipe at workbenches whose label_text.to_lower() matches this value. +## Empty string (default) means "any workbench whose accepted_skill matches" — +## i.e. the pre-Phase-19 behaviour and the correct fallback for older saves. +## Values should be lowercase: &"carpenter", &"smelter", &"millstone". +var target_workbench: StringName = &"" + # ── save / load ─────────────────────────────────────────────────────────────── @@ -70,6 +77,7 @@ func to_dict() -> Dictionary: "required_skill": String(required_skill), "skill_threshold": skill_threshold, "label": label, + "target_workbench": String(target_workbench), } @@ -85,4 +93,7 @@ static func from_dict(d: Dictionary) -> Recipe: r.required_skill = StringName(d.get("required_skill", str(SKILL_CRAFTING))) r.skill_threshold = int(d.get("skill_threshold", 0)) r.label = str(d.get("label", "")) + # Default to empty — old saves without this key get the pre-Phase-19 + # "any matching skill" behaviour, which is correct. + r.target_workbench = StringName(d.get("target_workbench", "")) return r diff --git a/scenes/ai/recipe_catalog.gd b/scenes/ai/recipe_catalog.gd index 0ed0e4a..bf02b5d 100644 --- a/scenes/ai/recipe_catalog.gd +++ b/scenes/ai/recipe_catalog.gd @@ -23,6 +23,7 @@ static func plank() -> Recipe: r.work_ticks = 60 # ~3 sim seconds at 1× (20 Hz × 3 s = 60 ticks) r.required_skill = Recipe.SKILL_CRAFTING r.skill_threshold = 0 + r.target_workbench = &"carpenter" return r @@ -35,6 +36,7 @@ static func stone_block() -> Recipe: r.work_ticks = 80 # ~4 sim seconds at 1× r.required_skill = Recipe.SKILL_CRAFTING r.skill_threshold = 0 + r.target_workbench = &"smelter" return r @@ -52,6 +54,7 @@ static func flour() -> Recipe: r.work_ticks = 50 # ~2.5 sim seconds at 1× — quick grinding pass r.required_skill = Recipe.SKILL_CRAFTING r.skill_threshold = 0 + r.target_workbench = &"millstone" return r @@ -125,9 +128,46 @@ static func quarry_stone() -> Recipe: return r +# ── Phase 19 — iron/gold smelting ──────────────────────────────────────────── + +static func iron_smelt() -> Recipe: + ## Iron ore → iron ingot. Worked at the Smelter (accepted_skill = crafting). + ## Secondary fuel ingredient (wood) is recorded but not yet enforced at + ## runtime — CraftingProvider only checks the primary ingredient today. + var r := Recipe.new() + r.id = &"iron_smelt" + r.label = "Smelt iron ingot" + r.ingredient_type = Item.TYPE_IRON_ORE + r.ingredient2_type = Item.TYPE_WOOD + r.ingredient2_count = 1 + r.output_type = Item.TYPE_IRON_INGOT + r.work_ticks = 200 # ~10 sim seconds at 1× — slower than stone-block + r.required_skill = Recipe.SKILL_CRAFTING + r.skill_threshold = 0 + r.target_workbench = &"smelter" + return r + + +static func gold_smelt() -> Recipe: + ## Gold ore → gold ingot. Same smelting chain as iron. + var r := Recipe.new() + r.id = &"gold_smelt" + r.label = "Smelt gold ingot" + r.ingredient_type = Item.TYPE_GOLD + r.ingredient2_type = Item.TYPE_WOOD + r.ingredient2_count = 1 + r.output_type = Item.TYPE_GOLD_INGOT + r.work_ticks = 200 # ~10 sim seconds at 1× + r.required_skill = Recipe.SKILL_CRAFTING + r.skill_threshold = 0 + r.target_workbench = &"smelter" + return r + + ## Returns one fresh instance of every recipe in the catalog. Used by UI ## recipe-pickers to enumerate available bills; callers filter by -## `recipe.required_skill` against the workbench's `accepted_skill`. +## `recipe.required_skill` and `recipe.target_workbench` against the +## workbench's `accepted_skill` and `label_text`. static func all() -> Array[Recipe]: return [ plank(), @@ -137,4 +177,6 @@ static func all() -> Array[Recipe]: meal_from_vegetables(), cremate_corpse(), quarry_stone(), + iron_smelt(), + gold_smelt(), ] diff --git a/scenes/entities/item.gd b/scenes/entities/item.gd index 1d5bd14..d3490a8 100644 --- a/scenes/entities/item.gd +++ b/scenes/entities/item.gd @@ -64,6 +64,12 @@ const TYPE_BREAD: StringName = &"bread" # Phase 14 — cremation output. One ash item drops per cremated corpse. const TYPE_ASH: StringName = &"ash" +# Phase 19 — smelted ingots (iron_ore → iron_ingot, gold → gold_ingot). +# No dedicated atlas sprite; on-floor visual uses the procedural hue-hashed +# square fallback in _draw() and the matching shape is in draw_item_shape(). +const TYPE_IRON_INGOT: StringName = &"iron_ingot" +const TYPE_GOLD_INGOT: StringName = &"gold_ingot" + const ALL_TYPES: Array[StringName] = [ TYPE_WOOD, TYPE_STONE, TYPE_IRON_ORE, TYPE_COPPER_ORE, TYPE_SILVER, TYPE_GOLD, TYPE_CLOTH, TYPE_VEGETABLE, @@ -72,6 +78,7 @@ const ALL_TYPES: Array[StringName] = [ TYPE_PLANK, TYPE_STONE_BLOCK, TYPE_FLOUR, TYPE_BREAD, TYPE_ASH, + TYPE_IRON_INGOT, TYPE_GOLD_INGOT, ] # ── quality system (docs/architecture.md "Quality system") ─────────────────── diff --git a/scenes/ui/workbench_panel.gd b/scenes/ui/workbench_panel.gd index 0bfe0b5..8646f16 100644 --- a/scenes/ui/workbench_panel.gd +++ b/scenes/ui/workbench_panel.gd @@ -415,15 +415,24 @@ func _on_recipe_chosen(id: int) -> void: _populate_bills() -## Returns all catalog recipes whose required_skill matches the workbench's -## accepted_skill. Returns an empty array when no workbench is set. +## Returns all catalog recipes visible at the current workbench. +## Two-pass filter: +## 1. required_skill must match workbench.accepted_skill. +## 2. If the recipe has a target_workbench set, it must match +## workbench.label_text.to_lower() — e.g. "Carpenter" → "carpenter". +## Recipes with an empty target_workbench pass to any matching-skill bench. +## Returns an empty array when no workbench is set. func _filtered_recipes() -> Array[Recipe]: if current_workbench == null: return [] + var workbench_key: StringName = StringName(current_workbench.label_text.to_lower()) var result: Array[Recipe] = [] for r in RecipeCatalog.all(): - if r.required_skill == current_workbench.accepted_skill: - result.append(r) + if r.required_skill != current_workbench.accepted_skill: + continue + if r.target_workbench != &"" and r.target_workbench != workbench_key: + continue + result.append(r) return result