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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", ""))
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
]
|
||||
|
|
|
|||
140
scenes/entities/big_rock_node.gd
Normal file
140
scenes/entities/big_rock_node.gd
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
class_name BigRockNode extends Node2D
|
||||
## Permanent stone outcrop — a 2×2 immovable boulder that never depletes.
|
||||
##
|
||||
## BigRockNode marks its footprint impassable in the pathfinder and registers
|
||||
## itself with World so the designation system can locate it. A QuarryWorkbench
|
||||
## can be built on one of its tiles; external code sets is_quarry_site = true
|
||||
## and quarry_workbench once construction completes.
|
||||
##
|
||||
## Draw convention: position is stamped at the TOP-LEFT pixel corner of the
|
||||
## 2×2 footprint (tile * TILE_SIZE_PX). The visual centre of the 32×32 area
|
||||
## sits at local (16, 16), so all _draw_* helpers are centred there.
|
||||
##
|
||||
## Save/load: to_dict / from_dict round-trip tile + is_quarry_site.
|
||||
## The quarry_workbench reference is an entity pointer reconstructed by the
|
||||
## save layer (SaveSystem wires it after both entities are spawned).
|
||||
|
||||
const TILE_SIZE_PX: int = 16
|
||||
|
||||
## Top-left tile of the 2×2 footprint.
|
||||
var tile: Vector2i = Vector2i.ZERO
|
||||
|
||||
## Footprint size in tiles (always 2×2; declared as a var so external code can
|
||||
## read it uniformly without special-casing BigRockNode vs hypothetical future nodes).
|
||||
var footprint: Vector2i = Vector2i(2, 2)
|
||||
|
||||
## True once a Quarry workbench has been completed on this outcrop.
|
||||
## Flipped by external code (designation / world.gd) — this file only declares it.
|
||||
var is_quarry_site: bool = false
|
||||
|
||||
## The QuarryWorkbench that sits on this node after construction.
|
||||
## null until assigned by the designation completion handler.
|
||||
var quarry_workbench = null
|
||||
|
||||
|
||||
# ── lifecycle ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func _ready() -> void:
|
||||
# Position at top-left pixel corner so footprint_tiles() aligns with world coords.
|
||||
position = Vector2(tile.x * TILE_SIZE_PX, tile.y * TILE_SIZE_PX)
|
||||
# Block pathfinding on every tile in the footprint.
|
||||
for t in footprint_tiles():
|
||||
if World.pathfinder != null:
|
||||
World.pathfinder.set_cell_walkable(t, false)
|
||||
World.register_big_rock_node(self)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
World.unregister_big_rock_node(self)
|
||||
|
||||
|
||||
# ── public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
## One-shot initialiser. Call after add_child() so _ready() has fired.
|
||||
func setup(p_tile: Vector2i) -> void:
|
||||
tile = p_tile
|
||||
position = Vector2(tile.x * TILE_SIZE_PX, tile.y * TILE_SIZE_PX)
|
||||
queue_redraw()
|
||||
Audit.log("big_rock_node", "spawned at tile %s" % tile)
|
||||
|
||||
|
||||
## Returns the four tiles covered by this node's 2×2 footprint.
|
||||
## Used by designation, save/load, and inspection code.
|
||||
func footprint_tiles() -> Array[Vector2i]:
|
||||
return [
|
||||
tile,
|
||||
tile + Vector2i(1, 0),
|
||||
tile + Vector2i(0, 1),
|
||||
tile + Vector2i(1, 1),
|
||||
]
|
||||
|
||||
|
||||
## True when tile_to_check falls inside the 2×2 footprint.
|
||||
func is_at(tile_to_check: Vector2i) -> bool:
|
||||
return (
|
||||
tile_to_check.x >= tile.x and tile_to_check.x < tile.x + footprint.x
|
||||
and tile_to_check.y >= tile.y and tile_to_check.y < tile.y + footprint.y
|
||||
)
|
||||
|
||||
|
||||
# ── save / load ───────────────────────────────────────────────────────────────
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"class_id": &"big_rock_node",
|
||||
"tile_x": tile.x,
|
||||
"tile_y": tile.y,
|
||||
"is_quarry_site": is_quarry_site,
|
||||
}
|
||||
|
||||
|
||||
## Restore from a dict produced by to_dict(). quarry_workbench is reconnected
|
||||
## by the save layer after both entities are spawned.
|
||||
static func from_dict(d: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"tile_x": int(d.get("tile_x", 0)),
|
||||
"tile_y": int(d.get("tile_y", 0)),
|
||||
"is_quarry_site": bool(d.get("is_quarry_site", false)),
|
||||
}
|
||||
|
||||
|
||||
# ── render ────────────────────────────────────────────────────────────────────
|
||||
|
||||
## Draw a procedural pile of grey rocks centred at local (16, 16) —
|
||||
## the geometric centre of the 32×32 footprint area.
|
||||
## Three layers create depth: large base blob → medium mid rock → small cap.
|
||||
func _draw() -> void:
|
||||
var cx: float = 16.0
|
||||
var cy: float = 16.0
|
||||
|
||||
# Colour palette — warm grey tones suggesting weathered granite.
|
||||
var base_fill := Color(0.60, 0.58, 0.55) # large base ellipse
|
||||
var mid_fill := Color(0.45, 0.44, 0.42) # medium middle rock
|
||||
var top_fill := Color(0.55, 0.53, 0.50) # small perched cap
|
||||
var outline_col := Color(0.20, 0.18, 0.16, 0.70) # subtle dark rim
|
||||
|
||||
# Bottom: large flattened blob — widest at the base to read as a ground-hugging mass.
|
||||
var base_w: float = 24.0
|
||||
var base_h: float = 16.0
|
||||
var base_rect := Rect2(Vector2(cx - base_w / 2.0, cy - base_h / 2.0 + 2.0), Vector2(base_w, base_h))
|
||||
draw_rect(base_rect, base_fill)
|
||||
|
||||
# Dark outline arc around the base blob.
|
||||
draw_rect(base_rect, outline_col, false, 1.0)
|
||||
|
||||
# Middle: slightly elevated, narrower rock sitting on top of the base.
|
||||
var mid_w: float = 16.0
|
||||
var mid_h: float = 12.0
|
||||
var mid_oy: float = -4.0 # shift up from centre
|
||||
var mid_rect := Rect2(Vector2(cx - mid_w / 2.0, cy - mid_h / 2.0 + mid_oy), Vector2(mid_w, mid_h))
|
||||
draw_rect(mid_rect, mid_fill)
|
||||
draw_rect(mid_rect, outline_col, false, 1.0)
|
||||
|
||||
# Top: small cap perched on the very top.
|
||||
var top_w: float = 8.0
|
||||
var top_h: float = 6.0
|
||||
var top_oy: float = -10.0 # above the mid rock
|
||||
var top_rect := Rect2(Vector2(cx - top_w / 2.0, cy - top_h / 2.0 + top_oy), Vector2(top_w, top_h))
|
||||
draw_rect(top_rect, top_fill)
|
||||
draw_rect(top_rect, outline_col, false, 1.0)
|
||||
1
scenes/entities/big_rock_node.gd.uid
Normal file
1
scenes/entities/big_rock_node.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bhn0lknhgn1od
|
||||
6
scenes/entities/big_rock_node.tscn
Normal file
6
scenes/entities/big_rock_node.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://big_rock_node_entity"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/entities/big_rock_node.gd" id="1_big_rock_node"]
|
||||
|
||||
[node name="BigRockNode" type="Node2D"]
|
||||
script = ExtResource("1_big_rock_node")
|
||||
80
scenes/entities/quarry_workbench.gd
Normal file
80
scenes/entities/quarry_workbench.gd
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
class_name QuarryWorkbench extends Workbench
|
||||
## Quarry workbench — built on a BigRockNode tile, produces stone indefinitely.
|
||||
##
|
||||
## Subclasses Workbench to reuse the build + bill machinery. A default
|
||||
## FOREVER bill with the quarry_stone recipe is auto-added in _ready() so the
|
||||
## workbench begins dripping stone as soon as construction completes.
|
||||
##
|
||||
## No-ingredient recipe: quarry_stone uses ingredient_count = 0. CraftingProvider
|
||||
## must handle the zero-ingredient path before this workbench produces anything
|
||||
## (see plan_quarry_bigrock.md Step 1 — handled separately).
|
||||
##
|
||||
## Variant appearance: overrides _draw() unconditionally so that the quarry
|
||||
## silhouette is always shown regardless of label_text (mirrors CremationPyre).
|
||||
## accepted_skill = manual_labor — any labourer can work the quarry.
|
||||
##
|
||||
## World registration: inherited from Workbench._ready / _exit_tree.
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
label_text = "Quarry"
|
||||
accepted_skill = &"manual_labor"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
label_text = "Quarry"
|
||||
accepted_skill = &"manual_labor"
|
||||
super._ready()
|
||||
# Auto-populate a default FOREVER bill so the bench is immediately usable
|
||||
# once construction completes. Mirrors CremationPyre's bill wiring.
|
||||
var b := Bill.new()
|
||||
b.recipe = RecipeCatalog.quarry_stone()
|
||||
b.mode = Bill.Mode.FOREVER
|
||||
bills.append(b)
|
||||
Audit.log("quarry", "QuarryWorkbench ready at %s — bill added" % tile)
|
||||
|
||||
|
||||
# ── render ────────────────────────────────────────────────────────────────────
|
||||
|
||||
## Override Workbench._draw() to always dispatch to _draw_quarry().
|
||||
## Alpha is 0.4 while under construction, 1.0 once complete (same as all benches).
|
||||
func _draw() -> void:
|
||||
var alpha: float = 1.0 if is_completed() else 0.4
|
||||
_draw_quarry(alpha)
|
||||
|
||||
|
||||
## Procedural quarry appearance: a wooden frame base with a large stone block
|
||||
## on top, chisel-mark details, and a small pile of freshly cut stones to the
|
||||
## right side. Local coords follow Workbench convention — (0, 0) is the
|
||||
## bottom-right of the tile; the bench draws UP into negative-y space.
|
||||
func _draw_quarry(alpha: float) -> void:
|
||||
var frame_top := Color(0.55, 0.36, 0.18, alpha) # light wood top face
|
||||
var frame_front := Color(0.42, 0.26, 0.12, alpha) # darker wood front
|
||||
var frame_edge := Color(0.25, 0.14, 0.06, alpha) # wood grain seam
|
||||
var stone_top := Color(0.60, 0.58, 0.55, alpha) # stone block top face
|
||||
var stone_front := Color(0.44, 0.43, 0.41, alpha) # stone block front
|
||||
var chisel_mark := Color(0.28, 0.27, 0.25, alpha) # cut line on stone
|
||||
var pile_light := Color(0.62, 0.61, 0.59, alpha) # loose stone pile
|
||||
var pile_dark := Color(0.38, 0.37, 0.35, alpha) # pile shadow
|
||||
var outline := Color(0.20, 0.18, 0.16, 0.70 * alpha)
|
||||
|
||||
# Wooden frame base — front face + top lip.
|
||||
draw_rect(Rect2(Vector2(-8.0, -7.0), Vector2(16.0, 7.0)), frame_front)
|
||||
draw_rect(Rect2(Vector2(-8.0, -10.0), Vector2(16.0, 3.0)), frame_top)
|
||||
draw_line(Vector2(-8.0, -7.0), Vector2(8.0, -7.0), frame_edge, 1.0)
|
||||
|
||||
# Stone block sitting on the frame — occupies most of the upper half.
|
||||
draw_rect(Rect2(Vector2(-7.0, -15.0), Vector2(11.0, 5.0)), stone_top)
|
||||
draw_rect(Rect2(Vector2(-7.0, -10.5), Vector2(11.0, 0.5)), stone_front) # thin visible front edge
|
||||
|
||||
# Chisel marks on the stone top — short diagonal cuts suggesting work in progress.
|
||||
draw_line(Vector2(-4.0, -13.0), Vector2(-2.0, -11.5), chisel_mark, 1.0)
|
||||
draw_line(Vector2(-1.0, -14.0), Vector2( 1.0, -12.5), chisel_mark, 1.0)
|
||||
draw_line(Vector2( 2.0, -13.0), Vector2( 4.0, -11.5), chisel_mark, 1.0)
|
||||
|
||||
# Small pile of cut stones on the right — two rounded rects stacked.
|
||||
draw_rect(Rect2(Vector2(5.0, -9.0), Vector2(4.0, 3.0)), pile_dark)
|
||||
draw_rect(Rect2(Vector2(5.0, -11.0), Vector2(3.0, 2.0)), pile_light)
|
||||
|
||||
# Tile outline.
|
||||
draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0)
|
||||
1
scenes/entities/quarry_workbench.gd.uid
Normal file
1
scenes/entities/quarry_workbench.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bpbcwr4dh1736
|
||||
6
scenes/entities/quarry_workbench.tscn
Normal file
6
scenes/entities/quarry_workbench.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://quarry_workbench_entity"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/entities/quarry_workbench.gd" id="1_quarry"]
|
||||
|
||||
[node name="QuarryWorkbench" type="Node2D"]
|
||||
script = ExtResource("1_quarry")
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -180,10 +180,11 @@ func _build_designate_tab() -> Control:
|
|||
var flow := _make_flow_grid()
|
||||
box.add_child(flow)
|
||||
|
||||
_add_tool_btn(flow, Strings.t(&"tool.chop"), &"chop", func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop")))
|
||||
_add_tool_btn(flow, Strings.t(&"tool.mine"), &"mine", func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine")))
|
||||
_add_tool_btn(flow, Strings.t(&"tool.dig_grave"),&"dig_grave", func() -> void: _activate(&"dig_grave", &"", Strings.t(&"tool.dig_grave")))
|
||||
_add_tool_btn(flow, Strings.t(&"tool.no_roof"), &"no_roof", func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof")))
|
||||
_add_tool_btn(flow, Strings.t(&"tool.chop"), &"chop", func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop")))
|
||||
_add_tool_btn(flow, Strings.t(&"tool.mine"), &"mine", func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine")))
|
||||
_add_tool_btn(flow, Strings.t(&"tool.dig_grave"), &"dig_grave", func() -> void: _activate(&"dig_grave", &"", Strings.t(&"tool.dig_grave")))
|
||||
_add_tool_btn(flow, Strings.t(&"tool.no_roof"), &"no_roof", func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof")))
|
||||
_add_tool_btn(flow, Strings.t(&"tool.plant_tree"), &"plant_tree", func() -> void: _activate(&"plant_tree", &"", Strings.t(&"tool.plant_tree")))
|
||||
|
||||
return box
|
||||
|
||||
|
|
@ -237,6 +238,12 @@ func _build_build_tab() -> Control:
|
|||
&"build_workbench_cremation_pyre",
|
||||
func() -> void: _activate(&"build_workbench_cremation_pyre", &"", Strings.t(&"tool.workbench_cremation_pyre")))
|
||||
|
||||
# Quarry — must be painted on a stone outcrop (BigRockNode); world.gd
|
||||
# rejects placements on plain ground.
|
||||
_add_tool_btn(flow, Strings.t(&"tool.paint_quarry"),
|
||||
&"paint_quarry",
|
||||
func() -> void: _activate(&"paint_quarry", &"", Strings.t(&"tool.paint_quarry")))
|
||||
|
||||
return box
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -332,6 +332,46 @@ func _draw() -> void:
|
|||
draw_line(Vector2(c.x, c.y + 6), Vector2(c.x, c.y - 6), arrow, 2.0)
|
||||
draw_line(Vector2(c.x, c.y - 6), Vector2(c.x - 4, c.y - 2), arrow, 2.0)
|
||||
draw_line(Vector2(c.x, c.y - 6), Vector2(c.x + 4, c.y - 2), arrow, 2.0)
|
||||
&"paint_quarry":
|
||||
# Stone block on wood frame + small pile of cut stones beside.
|
||||
var frame := Color(0.42, 0.26, 0.12)
|
||||
var frame_top := Color(0.55, 0.36, 0.18)
|
||||
var stone := Color(0.65, 0.63, 0.60)
|
||||
var stone_hi := Color(0.82, 0.80, 0.76)
|
||||
var stone_dark := Color(0.32, 0.30, 0.28)
|
||||
var chisel_steel := Color(0.78, 0.80, 0.85)
|
||||
# Wood frame base.
|
||||
draw_rect(Rect2(c.x - 14, c.y + 6, 28, 8), frame)
|
||||
draw_rect(Rect2(c.x - 14, c.y + 3, 28, 3), frame_top)
|
||||
# Large stone block on top.
|
||||
draw_rect(Rect2(c.x - 10, c.y - 8, 14, 11), stone)
|
||||
draw_rect(Rect2(c.x - 10, c.y - 8, 14, 3), stone_hi)
|
||||
# Chisel marks (3 short dark lines on the stone face).
|
||||
draw_line(Vector2(c.x - 6, c.y - 3), Vector2(c.x - 4, c.y - 3), stone_dark, 1.0)
|
||||
draw_line(Vector2(c.x - 2, c.y - 3), Vector2(c.x, c.y - 3), stone_dark, 1.0)
|
||||
draw_line(Vector2(c.x + 2, c.y - 3), Vector2(c.x - 0, c.y - 3), stone_dark, 1.0)
|
||||
# Pile of cut stones beside the block.
|
||||
draw_rect(Rect2(c.x + 6, c.y, 4, 3), stone)
|
||||
draw_rect(Rect2(c.x + 8, c.y - 3, 3, 3), stone_hi)
|
||||
# Chisel tool on top of block.
|
||||
draw_rect(Rect2(c.x - 4, c.y - 12, 2, 4), chisel_steel)
|
||||
draw_rect(Rect2(c.x - 5, c.y - 13, 4, 2), frame_top)
|
||||
&"plant_tree":
|
||||
# Green sapling sprout rising from dirt — signals manual tree planting.
|
||||
var dirt := Color(0.45, 0.32, 0.18)
|
||||
var dirt_hi := Color(0.62, 0.46, 0.28)
|
||||
var stem := Color(0.35, 0.22, 0.10)
|
||||
var leaf_a := Color(0.30, 0.65, 0.20)
|
||||
var leaf_b := Color(0.25, 0.55, 0.18)
|
||||
# Dirt base rectangle.
|
||||
draw_rect(Rect2(c.x - 14, c.y + 2, 28, 14), dirt)
|
||||
draw_line(Vector2(c.x - 14, c.y + 5), Vector2(c.x + 14, c.y + 5), dirt_hi, 1.0)
|
||||
# Stem.
|
||||
draw_line(Vector2(c.x, c.y + 2), Vector2(c.x, c.y - 8), stem, 2.0)
|
||||
# Three leaf dots at top of stem.
|
||||
draw_circle(Vector2(c.x, c.y - 10), 4.0, leaf_a)
|
||||
draw_circle(Vector2(c.x - 5, c.y - 6), 3.0, leaf_b)
|
||||
draw_circle(Vector2(c.x + 5, c.y - 5), 3.0, leaf_b)
|
||||
_:
|
||||
# Unknown tool — small grey placeholder.
|
||||
draw_rect(Rect2(c.x - 12, c.y - 12, 24, 24), Color(0.50, 0.50, 0.50))
|
||||
|
|
|
|||
1
scenes/ui/build_drawer_thumb.gd.uid
Normal file
1
scenes/ui/build_drawer_thumb.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://w802akkpbc6l
|
||||
|
|
@ -264,12 +264,36 @@ func _describe_wolf(w) -> String:
|
|||
|
||||
|
||||
func _describe_tree(t) -> String:
|
||||
# Growth stage label.
|
||||
var stage: int = int(t.get("growth_stage")) if "growth_stage" in t else 3
|
||||
var stage_key_map: Array[StringName] = [
|
||||
&"tree.stage.sapling",
|
||||
&"tree.stage.young",
|
||||
&"tree.stage.growing",
|
||||
&"tree.stage.mature",
|
||||
]
|
||||
var stage_label: String = Strings.t(stage_key_map[clamp(stage, 0, 3)])
|
||||
|
||||
# Pending-plant ghost indicator.
|
||||
var pending: bool = bool(t.get("pending_plant")) if "pending_plant" in t else false
|
||||
if pending:
|
||||
return "[b]%s[/b]\n[color=#aaa]awaiting pawn[/color]" % stage_label
|
||||
|
||||
# Growth progress for sub-mature trees.
|
||||
var is_mature: bool = (stage >= 3)
|
||||
if not is_mature:
|
||||
var progress: int = int(t.get("growth_progress")) if "growth_progress" in t else 0
|
||||
var stage_ticks: int = int(t.get("STAGE_TICKS")) if "STAGE_TICKS" in t else 1
|
||||
var pct_grow: int = int(100.0 * float(progress) / float(max(stage_ticks, 1)))
|
||||
return "[b]%s[/b]\nGrowing %d%%" % [stage_label, pct_grow]
|
||||
|
||||
# Mature tree — show chop progress if any.
|
||||
var pct: int = int(100.0 * float(t.chop_progress) / float(t.CHOP_TICKS))
|
||||
var designated: bool = bool(t.get("chop_designated"))
|
||||
var tag := " · [color=#fc6]marked[/color]" if designated else ""
|
||||
if pct > 0:
|
||||
return "[b]Tree[/b]\nChop %d%%%s" % [pct, tag]
|
||||
return "[b]Tree[/b]%s" % tag
|
||||
return "[b]%s[/b]\nChop %d%%%s" % [stage_label, pct, tag]
|
||||
return "[b]%s[/b]%s" % [stage_label, tag]
|
||||
|
||||
|
||||
func _describe_rock(r) -> String:
|
||||
|
|
|
|||
1
scenes/ui/medieval_theme.gd.uid
Normal file
1
scenes/ui/medieval_theme.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c26o807ldrrrx
|
||||
|
|
@ -40,6 +40,10 @@ const TOOL_BUILD_WORKBENCH_HEARTH: StringName = &"build_workbench_hearth"
|
|||
const TOOL_BUILD_WORKBENCH_CREMATION_PYRE: StringName = &"build_workbench_cremation_pyre"
|
||||
# Phase 17 — Stockpile tab.
|
||||
const TOOL_PAINT_STOCKPILE: StringName = &"paint_stockpile"
|
||||
# Tree planting — ghost sapling that ConstructionProvider will fulfil.
|
||||
const TOOL_PLANT_TREE: StringName = &"plant_tree"
|
||||
# Quarry — must paint on a BigRockNode tile; spawns a QuarryWorkbench ghost.
|
||||
const TOOL_PAINT_QUARRY: StringName = &"paint_quarry"
|
||||
|
||||
# ── tool → material override ─────────────────────────────────────────────────
|
||||
# For build_wall and build_floor the tool is shared but the material differs.
|
||||
|
|
@ -73,6 +77,8 @@ const _ATLAS_BY_TOOL: Dictionary = {
|
|||
&"build_workbench_hearth": Vector2i(1, 0),
|
||||
&"build_workbench_cremation_pyre":Vector2i(3, 0),
|
||||
&"paint_stockpile": Vector2i(0, 0),
|
||||
&"plant_tree": Vector2i(0, 0), # grass ghost — tinted green
|
||||
&"paint_quarry": Vector2i(2, 0), # stone-grey ghost
|
||||
}
|
||||
|
||||
# Placeholder source ID — mirrors World.PLACEHOLDER_SOURCE_ID.
|
||||
|
|
@ -120,6 +126,8 @@ func set_active_tool(tool: StringName) -> void:
|
|||
TOOL_BUILD_WORKBENCH_MILLSTONE, TOOL_BUILD_WORKBENCH_HEARTH,
|
||||
TOOL_BUILD_WORKBENCH_CREMATION_PYRE,
|
||||
TOOL_PAINT_STOCKPILE,
|
||||
TOOL_PLANT_TREE,
|
||||
TOOL_PAINT_QUARRY,
|
||||
],
|
||||
"Designation.set_active_tool: unknown tool '%s'" % tool
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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