Renewable resources: tree growth + WildGrowth + Quarry on BigRockNode

Trees: 4 growth stages (Sapling→Young→Growing→Mature), only Mature
yields wood. WildGrowth ticker fires every in-game hour; rejection-
samples grass tiles and plants a sapling with ~30% probability (capped
at MAP_TREE_LIMIT=60). New `paint_plant_tree` designation lets the
player manually plant — ghost sapling registered as a build_site that
ConstructionProvider fulfils. Stage round-trips through save/load.
Initial seed mixes 4 saplings + 6 mature so growth is visible day 1.

Quarry: new BigRockNode entity (2×2 permanent stone outcrop, never
depletes). 3 nodes seeded far from cabin. New QuarryWorkbench
(extends Workbench, auto-FOREVER `quarry_stone` bill, recipe drops
1 stone per 300 work-ticks). New `paint_quarry` designation only
accepts BigRockNode tiles. CraftingProvider now supports recipes
with `ingredient_count == 0` — skips ingredient-fetch and goes
straight to walk+craft toils. Recipe gains `ingredient_count` field
(defaults 0). Save/load layering: big_rock_node spawns at priority 0
(same as rock/tree), quarry_workbench at priority 2 (after the node).

UI: Plant tree + Build quarry buttons added to Build drawer.
build_drawer_thumb gains `plant_tree` (sapling sprout in dirt) and
`paint_quarry` (stone block + chisel + cut-stone pile) shapes.
inspect_tooltip recognises BigRockNode + shows tree growth stage on
hover.

Delegation: gdscript-refactor (Sonnet ×2) for trees full impl +
quarry skeleton; quick-edit (Haiku) for CraftingProvider no-ingredient
plumbing + TopBar polish; integration handled on Opus.
This commit is contained in:
megaproxy 2026-05-16 16:36:16 +01:00
parent 296894ff7a
commit d98d2c2425
20 changed files with 716 additions and 38 deletions

View file

@ -69,14 +69,24 @@ func find_best_for(pawn) -> Job:
_emit_bill_blocked(b.recipe.label, &"skill_too_low", wb)
continue
# Confirm a qualifying ingredient exists on the floor.
var src = _find_ingredient_item(b.recipe.ingredient_type)
if src == null:
_emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb)
continue
# If ingredient_count is 0, no ingredient is required; proceed directly.
# Otherwise, confirm a qualifying ingredient exists on the floor.
var src = null
if b.recipe.ingredient_count > 0:
src = _find_ingredient_item(b.recipe.ingredient_type)
if src == null:
_emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb)
continue
# Score: total Manhattan travel distance.
# If no ingredient (count==0), distance is just pawn → workbench.
# Otherwise, distance is pawn → ingredient → workbench.
var d: int
if b.recipe.ingredient_count > 0:
d = _manhattan(pawn.tile, src.tile) + _manhattan(src.tile, wb.tile)
else:
d = _manhattan(pawn.tile, wb.tile)
# Score: total Manhattan travel distance pawn → ingredient → workbench.
var d: int = _manhattan(pawn.tile, src.tile) + _manhattan(src.tile, wb.tile)
if d < best_dist:
best_dist = d
best_wb = wb
@ -87,16 +97,22 @@ func find_best_for(pawn) -> Job:
if best_wb == null:
return null
# Re-resolve the source item in case multiple bills tied on the same item.
var src_item = _find_ingredient_item(best_bill.recipe.ingredient_type)
if src_item == null:
return null
var src_item = null
# If ingredient_count > 0, re-resolve the source item in case multiple bills tied on the same item.
if best_bill.recipe.ingredient_count > 0:
src_item = _find_ingredient_item(best_bill.recipe.ingredient_type)
if src_item == null:
return null
var j := Job.new()
j.label = "Craft %s at %s" % [best_bill.recipe.label, best_wb.get("label_text") if best_wb.get("label_text") != null else "workbench"]
j.target_node = best_wb
j.toils.append(Toil.walk_to(src_item.tile))
j.toils.append(Toil.pickup())
# Only add ingredient-haul toils if ingredient is required.
if best_bill.recipe.ingredient_count > 0:
j.toils.append(Toil.walk_to(src_item.tile))
j.toils.append(Toil.pickup())
j.toils.append(Toil.walk_to(best_wb.tile))
j.toils.append(Toil.craft_at(best_wb.get_path(), best_bill_index))
return j

View file

@ -19,6 +19,9 @@ var id: StringName = &""
## Item type consumed by this recipe (single-ingredient for Phase 6).
var ingredient_type: StringName = &""
## Count of ingredient_type required by this recipe. 0 = no ingredient (work only).
var ingredient_count: int = 0
## Phase 14 — optional secondary ingredient. Empty string = no secondary.
## CraftingProvider Phase 14 follow-up: enforce pickup of ingredient2 before
## assigning a pawn to this bill (currently stub — only ingredient_type enforced).
@ -59,6 +62,7 @@ func to_dict() -> Dictionary:
return {
"id": String(id),
"ingredient_type": String(ingredient_type),
"ingredient_count": ingredient_count,
"ingredient2_type": String(ingredient2_type),
"ingredient2_count": ingredient2_count,
"output_type": String(output_type),
@ -73,6 +77,7 @@ static func from_dict(d: Dictionary) -> Recipe:
var r := Recipe.new()
r.id = StringName(d.get("id", ""))
r.ingredient_type = StringName(d.get("ingredient_type", ""))
r.ingredient_count = int(d.get("ingredient_count", 0))
r.ingredient2_type = StringName(d.get("ingredient2_type", ""))
r.ingredient2_count = int(d.get("ingredient2_count", 0))
r.output_type = StringName(d.get("output_type", ""))

View file

@ -111,6 +111,21 @@ static func cremate_corpse() -> Recipe:
return r
## Quarry workbench — no input, produces 1 stone per work cycle. Used by the
## QuarryWorkbench placed on a BigRockNode for renewable stone supply.
static func quarry_stone() -> Recipe:
var r := Recipe.new()
r.id = &"quarry_stone"
r.label = "Quarry stone"
r.ingredient_type = &"" # no input
r.ingredient_count = 0
r.output_type = Item.TYPE_STONE
r.work_ticks = 300
r.required_skill = &"manual_labor"
r.skill_threshold = 0
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`.
@ -122,4 +137,5 @@ static func all() -> Array[Recipe]:
bread(),
meal_from_vegetables(),
cremate_corpse(),
quarry_stone(),
]