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
|
|
@ -32,6 +32,8 @@ 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 QUARRY_WORKBENCH_SCENE: PackedScene = preload("res://scenes/entities/quarry_workbench.tscn")
|
||||
const STOCKPILE_SCENE: PackedScene = preload("res://scenes/world/stockpile_zone.tscn")
|
||||
const WALL_SCENE: PackedScene = preload("res://scenes/entities/wall.tscn")
|
||||
const FLOOR_SCENE: PackedScene = preload("res://scenes/entities/floor.tscn")
|
||||
|
|
@ -60,10 +62,15 @@ const SAMPLE_PAWNS: Array[Dictionary] = [
|
|||
]
|
||||
|
||||
# Phase 4 — sample harvestables. Trees clustered east, rocks south-east.
|
||||
# Mix of 8 mature + 4 saplings so players see growth in action from day 1.
|
||||
const SAMPLE_TREES: Array[Vector2i] = [
|
||||
Vector2i(58, 30), Vector2i(60, 31), Vector2i(62, 30),
|
||||
Vector2i(61, 33), Vector2i(63, 34), Vector2i(59, 35),
|
||||
Vector2i(57, 28), Vector2i(64, 32), # 2 more mature
|
||||
Vector2i(56, 36), Vector2i(65, 29), # 2 more mature
|
||||
]
|
||||
# The first 4 in SAMPLE_TREES_SAPLING are planted as saplings (stage 0).
|
||||
const SAMPLE_TREES_SAPLING_COUNT: int = 4
|
||||
const SAMPLE_ROCKS: Array[Vector2i] = [
|
||||
Vector2i(60, 60), Vector2i(62, 60), Vector2i(63, 62), Vector2i(58, 62),
|
||||
]
|
||||
|
|
@ -75,9 +82,28 @@ const SAMPLE_BIG_ROCKS: Array[Vector2i] = [
|
|||
Vector2i(56, 64),
|
||||
]
|
||||
|
||||
# Permanent stone outcrops (BigRockNode). Scattered far from the cabin at
|
||||
# (44, 22)..(51, 28) so the player has to scout / plan transport routes.
|
||||
# Each is a 2×2 footprint that never depletes; player paints `paint_quarry`
|
||||
# to build a QuarryWorkbench on it.
|
||||
const SAMPLE_BIG_ROCK_NODES: Array[Vector2i] = [
|
||||
Vector2i(12, 30), # west, near map edge
|
||||
Vector2i(68, 12), # north-east corner area
|
||||
Vector2i(70, 60), # south-east corner
|
||||
]
|
||||
|
||||
# HaulingProvider re-flow cadence — every 5 sim seconds at 1× (100 ticks).
|
||||
const HAUL_SWEEP_INTERVAL_TICKS: int = 100
|
||||
|
||||
# WildGrowth — spontaneous sapling spawning on eligible grass tiles.
|
||||
# 1200 ticks = 1 in-game hour at 20 Hz (20 ticks/s × 60 s/min = 1200 ticks/min,
|
||||
# but 1 in-game minute = 20 ticks at 1× so 1 hour = 1200 ticks at 1×).
|
||||
const WILD_GROWTH_INTERVAL: int = 1200
|
||||
const WILD_GROWTH_SPAWN_PROBABILITY: float = 0.30
|
||||
const MAP_TREE_LIMIT: int = 60
|
||||
# Rejection-sample attempts before giving up for this tick.
|
||||
const WILD_GROWTH_MAX_ATTEMPTS: int = 10
|
||||
|
||||
# Phase 11 — global darkness tint. Day = white, night = deep cool blue.
|
||||
# Driven by Clock.darkness_factor() (0..1) each sim tick.
|
||||
const NIGHT_TINT: Color = Color(0.20, 0.22, 0.40, 1.0)
|
||||
|
|
@ -419,11 +445,15 @@ func _spawn_sample_harvestables() -> void:
|
|||
# Boot seed auto-designates so the production-chain demo runs end-to-end
|
||||
# without requiring a player to paint chop/mine first. Real player-painted
|
||||
# trees / rocks still gate on chop_designated / mine_designated (Rimworld parity).
|
||||
for t_tile in SAMPLE_TREES:
|
||||
for i in SAMPLE_TREES.size():
|
||||
var tree = TREE_SCENE.instantiate()
|
||||
add_child(tree)
|
||||
tree.setup(t_tile)
|
||||
tree.chop_designated = true
|
||||
# First SAMPLE_TREES_SAPLING_COUNT trees spawn as saplings (stage 0)
|
||||
# so the player can observe growth from day 1. The rest are mature.
|
||||
var stage: int = HarvestableTree.STAGE_SAPLING if i < SAMPLE_TREES_SAPLING_COUNT else HarvestableTree.STAGE_MATURE
|
||||
tree.setup(SAMPLE_TREES[i], stage)
|
||||
if stage == HarvestableTree.STAGE_MATURE:
|
||||
tree.chop_designated = true
|
||||
for r_tile in SAMPLE_ROCKS:
|
||||
var rock = ROCK_SCENE.instantiate()
|
||||
add_child(rock)
|
||||
|
|
@ -434,8 +464,13 @@ func _spawn_sample_harvestables() -> void:
|
|||
add_child(big)
|
||||
big.setup(br_origin)
|
||||
big.mine_designated = true
|
||||
Audit.log("world", "spawned %d trees + %d rocks + %d big rocks" % [
|
||||
SAMPLE_TREES.size(), SAMPLE_ROCKS.size(), SAMPLE_BIG_ROCKS.size()
|
||||
# Permanent stone outcrops (never deplete; Quarry workbench built on them).
|
||||
for node_origin in SAMPLE_BIG_ROCK_NODES:
|
||||
var node = BIG_ROCK_NODE_SCENE.instantiate()
|
||||
add_child(node)
|
||||
node.setup(node_origin)
|
||||
Audit.log("world", "spawned %d trees + %d rocks + %d big rocks + %d stone outcrops" % [
|
||||
SAMPLE_TREES.size(), SAMPLE_ROCKS.size(), SAMPLE_BIG_ROCKS.size(), SAMPLE_BIG_ROCK_NODES.size()
|
||||
])
|
||||
|
||||
|
||||
|
|
@ -747,6 +782,38 @@ func _on_designation_added(cell: Vector2i, tool: StringName) -> void:
|
|||
sz.accepted_types = [] as Array[StringName] # wildcard
|
||||
sz.queue_redraw()
|
||||
entity = sz
|
||||
# Quarry — must be placed on a BigRockNode tile. Spawns a
|
||||
# QuarryWorkbench ghost (auto-FOREVER bill on completion).
|
||||
&"paint_quarry":
|
||||
var node = World.big_rock_node_at_tile(cell)
|
||||
if node == null:
|
||||
Audit.log("world", "paint_quarry: %s not on a stone outcrop — rejected" % cell)
|
||||
return
|
||||
# Refuse if this node already has a quarry built/queued.
|
||||
for ws in World.workbenches:
|
||||
if "label_text" in ws and ws.label_text == "Quarry" and node.is_at(ws.tile):
|
||||
Audit.log("world", "paint_quarry: outcrop at %s already has a quarry" % cell)
|
||||
return
|
||||
var quarry = QUARRY_WORKBENCH_SCENE.instantiate()
|
||||
add_child(quarry)
|
||||
quarry.setup(cell)
|
||||
entity = quarry
|
||||
# Tree planting — spawn a ghost sapling with pending_plant=true so
|
||||
# ConstructionProvider can queue a build job (1 wood, 30 ticks of work).
|
||||
# The ghost renders as a translucent sprout until the pawn completes it.
|
||||
&"plant_tree":
|
||||
# Reject if tile is already occupied by a tree.
|
||||
for existing_t in World.trees:
|
||||
if is_instance_valid(existing_t) and existing_t.tile == cell:
|
||||
Audit.log("world", "plant_tree: tile %s already has a tree — skipped" % cell)
|
||||
return
|
||||
var pt = TREE_SCENE.instantiate()
|
||||
add_child(pt)
|
||||
pt.setup(cell, HarvestableTree.STAGE_SAPLING)
|
||||
pt.pending_plant = true
|
||||
# Register as a build site so ConstructionProvider can assign a pawn.
|
||||
World.register_build_site(pt)
|
||||
entity = pt
|
||||
_:
|
||||
Audit.log("world", "unknown designation tool: %s" % tool)
|
||||
return
|
||||
|
|
@ -839,11 +906,88 @@ func _on_designation_cleared(cell: Vector2i) -> void:
|
|||
|
||||
func _on_sim_tick_world_sweep(tick_n: int) -> void:
|
||||
_update_dark_overlay()
|
||||
|
||||
# Tree growth — tick every registered tree; mature + pending-plant trees are
|
||||
# no-ops inside on_sim_tick(), so iterating all is safe.
|
||||
for tree in World.trees:
|
||||
if is_instance_valid(tree):
|
||||
tree.on_sim_tick()
|
||||
|
||||
# WildGrowth — attempt to plant a new sapling once per WILD_GROWTH_INTERVAL.
|
||||
if tick_n % WILD_GROWTH_INTERVAL == 0:
|
||||
_try_wild_growth()
|
||||
|
||||
if tick_n % HAUL_SWEEP_INTERVAL_TICKS != 0:
|
||||
return
|
||||
hauling_provider.sweep_for_better_destinations()
|
||||
|
||||
|
||||
## Attempt to spawn one wild sapling on a random eligible grass tile.
|
||||
## Eligibility: walkable + grass terrain + no entity overlap + < 2 tree neighbours.
|
||||
## Gives up after WILD_GROWTH_MAX_ATTEMPTS rejected tries to avoid lag spikes.
|
||||
func _try_wild_growth() -> void:
|
||||
if World.trees.size() >= MAP_TREE_LIMIT:
|
||||
return
|
||||
if randf() >= WILD_GROWTH_SPAWN_PROBABILITY:
|
||||
return
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = Sim.tick + 9973 # stable within a tick, different each call cycle
|
||||
for _attempt in WILD_GROWTH_MAX_ATTEMPTS:
|
||||
var candidate := Vector2i(
|
||||
rng.randi_range(0, MAP_SIZE_TILES.x - 1),
|
||||
rng.randi_range(0, MAP_SIZE_TILES.y - 1)
|
||||
)
|
||||
if not _wild_growth_tile_eligible(candidate):
|
||||
continue
|
||||
var tree = TREE_SCENE.instantiate()
|
||||
add_child(tree)
|
||||
tree.setup(candidate, HarvestableTree.STAGE_SAPLING)
|
||||
Audit.log("world", "wild growth: sapling at %s (total %d)" % [candidate, World.trees.size()])
|
||||
return
|
||||
|
||||
|
||||
## Returns true if `tile` is a valid WildGrowth spawn location.
|
||||
## Checks: walkable, grass terrain (source 0, atlas (0,0)), no entity at tile,
|
||||
## and fewer than 2 trees among the 4 cardinal neighbours.
|
||||
func _wild_growth_tile_eligible(tile: Vector2i) -> bool:
|
||||
# Bounds check (pathfinder bounds == map bounds).
|
||||
if pathfinder == null:
|
||||
return false
|
||||
if not pathfinder.is_walkable(tile):
|
||||
return false
|
||||
# Grass terrain only — atlas (0,0) on source 0 = TILE_GRASS.
|
||||
if terrain_layer == null:
|
||||
return false
|
||||
var src_id: int = terrain_layer.get_cell_source_id(tile)
|
||||
var atlas: Vector2i = terrain_layer.get_cell_atlas_coords(tile)
|
||||
if src_id != PLACEHOLDER_SOURCE_ID or atlas != TILE_GRASS:
|
||||
return false
|
||||
# No existing tree, rock, or item at this tile.
|
||||
for t in World.trees:
|
||||
if is_instance_valid(t) and t.tile == tile:
|
||||
return false
|
||||
for r in World.rocks:
|
||||
if is_instance_valid(r):
|
||||
if r.has_method("footprint_tiles"):
|
||||
if tile in r.footprint_tiles():
|
||||
return false
|
||||
elif r.tile == tile:
|
||||
return false
|
||||
for it in World.items:
|
||||
if is_instance_valid(it) and it.tile == tile:
|
||||
return false
|
||||
# No clumping: reject if 2+ cardinal neighbours already have a tree.
|
||||
var neighbour_trees: int = 0
|
||||
var offsets: Array[Vector2i] = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]
|
||||
for offset in offsets:
|
||||
var nb: Vector2i = tile + offset
|
||||
for t in World.trees:
|
||||
if is_instance_valid(t) and t.tile == nb:
|
||||
neighbour_trees += 1
|
||||
break
|
||||
return neighbour_trees < 2
|
||||
|
||||
|
||||
# Phase 11 — interpolate CanvasModulate between DAY_TINT and NIGHT_TINT based
|
||||
# on Clock.darkness_factor() (0 = full day, 1 = full night).
|
||||
# Called every sim tick; Color.lerp is a handful of float ops — negligible cost.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue