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:
parent
296894ff7a
commit
d98d2c2425
20 changed files with 716 additions and 38 deletions
|
|
@ -1,10 +1,15 @@
|
|||
## Tree entity — choppable by a pawn with a Chop job. Drops wood Item nodes
|
||||
## when felled.
|
||||
## when felled. Trees also grow through four stages (Sapling → Young → Growing
|
||||
## → Mature); only Mature trees can be chopped.
|
||||
##
|
||||
## Chopping model (docs/implementation.md Phase 4):
|
||||
## A ChopProvider creates a Job whose INTERACT toil calls on_chop_tick() once
|
||||
## per sim tick via JobRunner. After CHOP_TICKS ticks the tree is felled.
|
||||
##
|
||||
## Growth model: on_sim_tick() is called once per sim tick by world.gd's sweep.
|
||||
## After STAGE_TICKS ticks at each sub-mature stage the tree advances one stage
|
||||
## and _refresh_sprite() updates the visual.
|
||||
##
|
||||
## World registration (World.register_tree / World.unregister_tree) is called
|
||||
## here but the methods land in World during Opus integration.
|
||||
|
||||
|
|
@ -22,6 +27,19 @@ const WOOD_DROPS_ON_FELL: int = 3
|
|||
## Stack size per dropped Item (Phase 4 simplicity: 3 items of stack 1 each).
|
||||
const STACK_SIZE_PER_DROP: int = 1
|
||||
|
||||
# ── growth stage constants ─────────────────────────────────────────────────────
|
||||
|
||||
## Growth stage indices.
|
||||
const STAGE_SAPLING: int = 0
|
||||
const STAGE_YOUNG: int = 1
|
||||
const STAGE_GROWING: int = 2
|
||||
const STAGE_MATURE: int = 3
|
||||
|
||||
## Sim ticks spent in each sub-mature stage before advancing.
|
||||
## 5 in-game hours per stage at 20 Hz = 1200 ticks/hour × 5 = 6000 ticks.
|
||||
## At default 5× speed that is ~5 min real time per stage, ~15 min seed → mature.
|
||||
const STAGE_TICKS: int = 6000
|
||||
|
||||
# ── state ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
var tile: Vector2i = Vector2i.ZERO
|
||||
|
|
@ -31,6 +49,17 @@ var chop_progress: int = 0
|
|||
## ignores undesignated trees (Rimworld parity — pawns don't auto-chop).
|
||||
var chop_designated: bool = false
|
||||
|
||||
## Current growth stage (STAGE_SAPLING..STAGE_MATURE). Default MATURE so
|
||||
## existing seed trees load at full size with no visual regression.
|
||||
var growth_stage: int = STAGE_MATURE
|
||||
## Ticks elapsed within the current sub-mature stage.
|
||||
var growth_progress: int = 0
|
||||
|
||||
## Set to true on a ghost tree spawned by the plant_tree designation. The
|
||||
## ConstructionProvider will issue a build job; on completion this flag clears
|
||||
## and the tree grows normally.
|
||||
var pending_plant: bool = false
|
||||
|
||||
# Preloaded scene for spawned wood items.
|
||||
const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn")
|
||||
|
||||
|
|
@ -57,18 +86,27 @@ const _TREE_SILHOUETTES: int = 4 # silhouettes per atlas (columns)
|
|||
|
||||
func _ready() -> void:
|
||||
position = _tile_to_world(tile)
|
||||
_build_sprite()
|
||||
_refresh_sprite()
|
||||
# Y-sort so the canopy draws behind walls/pawns that are visually south of
|
||||
# the trunk base. Position.y is the trunk-base row.
|
||||
y_sort_enabled = true
|
||||
World.register_tree(self)
|
||||
|
||||
|
||||
## Adds a Sprite2D child painted with one of the 12 ElvGames tree variants
|
||||
## (4 silhouettes × 3 season palettes). Variant chosen deterministically
|
||||
## from the tile coord so the same tile always gets the same tree silhouette
|
||||
## across boots and load/save.
|
||||
func _build_sprite() -> void:
|
||||
## Rebuild the Sprite2D child to match the current growth_stage.
|
||||
## Sapling (stage 0): no Sprite2D — rendered procedurally in _draw().
|
||||
## Young (1) → scale 0.35, Growing (2) → 0.65, Mature (3) → 1.0.
|
||||
## Any existing "Sprite" child is removed first so re-calls don't stack.
|
||||
func _refresh_sprite() -> void:
|
||||
var old := get_node_or_null("Sprite")
|
||||
if old != null:
|
||||
old.queue_free()
|
||||
if growth_stage == STAGE_SAPLING:
|
||||
# No Sprite2D for saplings — all rendering done in _draw().
|
||||
queue_redraw()
|
||||
return
|
||||
var scale_map: Array[float] = [1.0, 0.35, 0.65, 1.0] # indexed by stage
|
||||
var sprite_scale: float = scale_map[growth_stage]
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.name = "Sprite"
|
||||
var hash_seed: int = tile.x * 31 + tile.y * 17
|
||||
|
|
@ -83,9 +121,18 @@ func _build_sprite() -> void:
|
|||
# Sprite center is at offset.y; sprite half-height is _TREE_VARIANT_H/2 = 40.
|
||||
# We want bottom edge at +8 (tile bottom) → center at 8 - 40 = -32.
|
||||
sprite.offset = Vector2(0, -32)
|
||||
sprite.scale = Vector2(sprite_scale, sprite_scale)
|
||||
# Render behind pawns/items that are at higher z_index; trees live at z=0.
|
||||
sprite.z_index = 0
|
||||
add_child(sprite)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## Adds a Sprite2D child painted with one of the 12 ElvGames tree variants
|
||||
## (4 silhouettes × 3 season palettes). Kept for call-site compatibility but
|
||||
## now delegates to _refresh_sprite(). New code should call _refresh_sprite().
|
||||
func _build_sprite() -> void:
|
||||
_refresh_sprite()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
|
|
@ -95,17 +142,81 @@ func _exit_tree() -> void:
|
|||
# ── public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
## One-shot initialiser. Call after add_child() so _ready() already fired.
|
||||
func setup(start_tile: Vector2i) -> void:
|
||||
## start_stage defaults to STAGE_MATURE for backward compatibility.
|
||||
func setup(start_tile: Vector2i, start_stage: int = STAGE_MATURE) -> void:
|
||||
tile = start_tile
|
||||
chop_progress = 0
|
||||
growth_stage = start_stage
|
||||
growth_progress = 0
|
||||
position = _tile_to_world(tile)
|
||||
_refresh_sprite()
|
||||
queue_redraw()
|
||||
Audit.log("tree", "spawned at %s" % tile)
|
||||
Audit.log("tree", "spawned at %s (stage=%d)" % [tile, growth_stage])
|
||||
|
||||
|
||||
## True when the tree hasn't been fully chopped yet.
|
||||
## True when the tree is mature, unChopped, and not a pending-plant ghost.
|
||||
## Only mature trees yield wood — saplings/young/growing cannot be felled.
|
||||
func is_choppable() -> bool:
|
||||
return chop_progress < CHOP_TICKS
|
||||
return chop_progress < CHOP_TICKS and growth_stage == STAGE_MATURE and not pending_plant
|
||||
|
||||
|
||||
## Called by world.gd's sim-tick sweep once per sim tick.
|
||||
## Advances growth_progress and promotes the stage on threshold. No-op for
|
||||
## mature trees and for pending-plant ghosts (ghost must be built first).
|
||||
func on_sim_tick() -> void:
|
||||
if growth_stage >= STAGE_MATURE or pending_plant:
|
||||
return
|
||||
growth_progress += 1
|
||||
if growth_progress >= STAGE_TICKS:
|
||||
growth_stage += 1
|
||||
growth_progress = 0
|
||||
_refresh_sprite()
|
||||
Audit.log("tree", "grew to stage %d at %s" % [growth_stage, tile])
|
||||
|
||||
|
||||
# ── pending-plant / build-site duck-type API ──────────────────────────────────
|
||||
# ConstructionProvider requires is_buildable() / on_build_tick() / label()
|
||||
# on every entity in World.build_queue. A pending_plant tree satisfies this
|
||||
# interface so the provider can assign a pawn to "build" (plant) it.
|
||||
|
||||
## Ticks of pawn work needed to complete a manual planting job.
|
||||
const PLANT_TICKS: int = 30
|
||||
|
||||
## Progress counter within the planting job (0..PLANT_TICKS).
|
||||
var _plant_progress: int = 0
|
||||
|
||||
|
||||
## True while the tree is a pending-plant ghost awaiting pawn work.
|
||||
func is_buildable() -> bool:
|
||||
return pending_plant and _plant_progress < PLANT_TICKS
|
||||
|
||||
|
||||
## Human-readable label for the ConstructionProvider job entry.
|
||||
func label() -> String:
|
||||
return "Plant tree"
|
||||
|
||||
|
||||
## Called by JobRunner's BUILD toil once per sim tick while a pawn works this
|
||||
## site. After PLANT_TICKS the ghost becomes a real sapling.
|
||||
func on_build_tick() -> void:
|
||||
if not is_buildable():
|
||||
return
|
||||
_plant_progress += 1
|
||||
queue_redraw()
|
||||
if _plant_progress >= PLANT_TICKS:
|
||||
_complete_plant()
|
||||
|
||||
|
||||
## Finish the planting job: clear the pending flag, register as a real sapling,
|
||||
## remove from World.build_queue, and clear the designation highlight.
|
||||
func _complete_plant() -> void:
|
||||
pending_plant = false
|
||||
_plant_progress = 0
|
||||
World.unregister_build_site(self)
|
||||
World.clear_designation_at(tile)
|
||||
_refresh_sprite()
|
||||
queue_redraw()
|
||||
Audit.log("tree", "planted at %s — sapling begins growing" % tile)
|
||||
|
||||
|
||||
## Called by the INTERACT toil in JobRunner once per sim tick while the pawn
|
||||
|
|
@ -145,6 +256,10 @@ func to_dict() -> Dictionary:
|
|||
"tile_y": tile.y,
|
||||
"chop_progress": chop_progress,
|
||||
"chop_designated": chop_designated,
|
||||
"growth_stage": growth_stage,
|
||||
"growth_progress": growth_progress,
|
||||
"pending_plant": pending_plant,
|
||||
"plant_progress": _plant_progress,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -154,15 +269,34 @@ static func from_dict(d: Dictionary) -> Dictionary:
|
|||
"tile_y": int(d.get("tile_y", 0)),
|
||||
"chop_progress": int(d.get("chop_progress", 0)),
|
||||
"chop_designated": bool(d.get("chop_designated", false)),
|
||||
# Default to STAGE_MATURE (3) so pre-growth-system saves load as mature trees.
|
||||
"growth_stage": int(d.get("growth_stage", 3)),
|
||||
"growth_progress": int(d.get("growth_progress", 0)),
|
||||
"pending_plant": bool(d.get("pending_plant", false)),
|
||||
"plant_progress": int(d.get("plant_progress", 0)),
|
||||
}
|
||||
|
||||
|
||||
# ── render ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _draw() -> void:
|
||||
# Canopy + trunk now come from the Sprite2D child (see _build_sprite).
|
||||
# Sapling stage: draw a procedural sprout — no atlas sprite available.
|
||||
# Three small green leaf-dots clustered above a thin brown stem.
|
||||
if growth_stage == STAGE_SAPLING:
|
||||
# Ghost tint for pending-plant saplings so the player can tell it's
|
||||
# waiting for a pawn to build it.
|
||||
var alpha := 0.55 if pending_plant else 1.0
|
||||
# Stem
|
||||
draw_line(Vector2(0.0, 6.0), Vector2(0.0, 0.0), Color(0.35, 0.22, 0.10, alpha), 1.5)
|
||||
# Three leaf dots
|
||||
draw_circle(Vector2(0.0, -2.0), 2.5, Color(0.30, 0.65, 0.20, alpha))
|
||||
draw_circle(Vector2(-3.0, 1.0), 1.8, Color(0.25, 0.58, 0.18, alpha))
|
||||
draw_circle(Vector2(3.0, 0.5), 1.8, Color(0.28, 0.62, 0.19, alpha))
|
||||
return
|
||||
|
||||
# Mature / growing stages: canopy + trunk come from the Sprite2D child.
|
||||
# This _draw renders only the chop-progress notch overlaid on the trunk.
|
||||
if chop_progress > 0:
|
||||
if chop_progress > 0 and growth_stage == STAGE_MATURE:
|
||||
var ratio := float(chop_progress) / float(CHOP_TICKS)
|
||||
var notch_depth := ratio * 3.0
|
||||
draw_line(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue