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

@ -29,6 +29,7 @@ const _PAWN_SCENE: PackedScene = preload("res://scenes/pawn/pawn.tscn")
const _TREE_SCENE: PackedScene = preload("res://scenes/entities/tree.tscn")
const _ROCK_SCENE: PackedScene = preload("res://scenes/entities/rock.tscn")
const _BIG_ROCK_SCENE: PackedScene = preload("res://scenes/entities/big_rock.tscn")
const _BIG_ROCK_NODE_SCENE: PackedScene = preload("res://scenes/entities/big_rock_node.tscn")
const _ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn")
const _WALL_SCENE: PackedScene = preload("res://scenes/entities/wall.tscn")
const _FLOOR_SCENE: PackedScene = preload("res://scenes/entities/floor.tscn")
@ -276,6 +277,7 @@ func _collect_entities() -> Array:
var registries: Array = [
World.trees,
World.rocks,
World.big_rock_nodes,
World.items,
World.build_queue, # ghost walls / floors / doors / grave_slots
World.doors,
@ -316,6 +318,7 @@ const _SPAWN_PRIORITY: Dictionary = {
&"tree": 0,
&"rock": 0,
&"big_rock": 0,
&"big_rock_node": 0,
&"wall": 0,
&"floor": 0,
&"door": 1,
@ -351,6 +354,7 @@ func _register_factories() -> void:
_factories[&"tree"] = _spawn_tree
_factories[&"rock"] = _spawn_rock
_factories[&"big_rock"] = _spawn_big_rock
_factories[&"big_rock_node"] = _spawn_big_rock_node
_factories[&"item"] = _spawn_item
_factories[&"wall"] = _spawn_wall
_factories[&"floor"] = _spawn_floor
@ -377,8 +381,18 @@ func _register_factories() -> void:
func _spawn_tree(world_scene: Node, d: Dictionary) -> void:
var ent = _TREE_SCENE.instantiate()
world_scene.add_child(ent)
ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))
# Pass growth_stage to setup() so _refresh_sprite() picks the right visual.
# Default 3 (STAGE_MATURE) so pre-growth-system saves load as mature trees.
var gs: int = int(d.get("growth_stage", 3))
ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))), gs)
ent.chop_progress = int(d.get("chop_progress", 0))
ent.growth_progress = int(d.get("growth_progress", 0))
ent.chop_designated = bool(d.get("chop_designated", false))
ent.pending_plant = bool(d.get("pending_plant", false))
ent._plant_progress = int(d.get("plant_progress", 0))
# Re-register as build site if planting is still in progress.
if ent.pending_plant:
World.register_build_site(ent)
ent.queue_redraw()
@ -398,6 +412,14 @@ func _spawn_big_rock(world_scene: Node, d: Dictionary) -> void:
ent.queue_redraw()
func _spawn_big_rock_node(world_scene: Node, d: Dictionary) -> void:
var ent = _BIG_ROCK_NODE_SCENE.instantiate()
world_scene.add_child(ent)
ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))))
ent.is_quarry_site = bool(d.get("is_quarry_site", false))
ent.queue_redraw()
func _spawn_item(world_scene: Node, d: Dictionary) -> void:
var ent = _ITEM_SCENE.instantiate()
world_scene.add_child(ent)

View file

@ -194,8 +194,15 @@ const TABLE: Dictionary = {
&"tool.workbench_millstone": "Millstone",
&"tool.workbench_hearth": "Hearth",
&"tool.workbench_cremation_pyre": "Cremation Pyre",
&"tool.paint_quarry": "Build quarry",
&"tool.stockpile_general": "Stockpile",
&"tool.graveyard": "Graveyard",
&"tool.plant_tree": "Plant tree",
# Tree growth stage names (shown in inspect tooltip).
&"tree.stage.sapling": "Sapling",
&"tree.stage.young": "Young tree",
&"tree.stage.growing": "Growing tree",
&"tree.stage.mature": "Mature tree",
&"ui.bill.mode_forever": "Forever",
&"ui.bill.mode_count": "Do X times",
&"ui.bill.mode_until_n": "Do until X",

View file

@ -19,6 +19,7 @@ var work_providers: Array = []
# from their _ready/_exit_tree. Phase 16 will add stable IDs and persistence wiring.
var trees: Array = [] # Array of Tree
var rocks: Array = [] # Array of Rock
var big_rock_nodes: Array = [] # Array of BigRockNode (permanent stone outcrops)
var items: Array = [] # Array of Item (on-floor stacks)
var stockpiles: Array = [] # Array of StorageDestination (StockpileZone for now; containers Phase 5)
@ -207,6 +208,24 @@ func unregister_rock(r) -> void:
rocks.erase(r)
func register_big_rock_node(n) -> void:
if not big_rock_nodes.has(n):
big_rock_nodes.append(n)
func unregister_big_rock_node(n) -> void:
big_rock_nodes.erase(n)
## Returns the BigRockNode whose 2×2 footprint covers `tile`, or null.
## Used by `paint_quarry` designation to validate the build site.
func big_rock_node_at_tile(tile: Vector2i):
for n in big_rock_nodes:
if n.is_at(tile):
return n
return null
func register_item(it) -> void:
if items.has(it):
return