class_name Workbench extends Node2D ## Workbench entity — buildable structure where pawns craft items per bills. ## ## Rendered as a bottom-anchored sprite (Y-sorted) matching the 3/4-perspective ## convention from Wall/Door. Ghost state (40% alpha) while construction is ## in progress; solid once _completed. ## ## Variant appearance is driven by label_text: ## "Carpenter" → warm-brown wood bench with a vise detail ## "Smelter" → dark grey stone block with an orange ember glow ## Other → generic warm-grey fallback ## ## Bill model (architecture.md "Production: workbenches, recipes, bills"): ## bills[] — ordered queue of Bill objects (untyped; Bill class ## is authored by a sibling agent and may not be ## registered when this file compiles). ## current_bill — the Bill actively being worked; null when idle. ## current_work_progress — tick counter within the current craft cycle. ## JobRunner._tick_craft increments this each sim tick. ## Workbench resets it to 0 on cycle completion or ## when the pawn leaves mid-craft. ## ## Save/load: to_dict / from_dict capture all persistent fields, including ## each bill via bill.to_dict(). Mirrors the Crate save pattern. ## ## World registration: World.register_workbench / World.unregister_workbench ## are called from _ready / _exit_tree. const TILE_SIZE_PX: int = 16 ## Sim ticks to build a workbench (90 ticks ≈ 4.5 sim seconds at 1×). const BUILD_TICKS: int = 90 # ── exports ─────────────────────────────────────────────────────────────────── ## Tile position of this workbench in world-tile coordinates. @export var tile: Vector2i = Vector2i.ZERO ## Player-visible label. Also drives the _draw() variant. ## Recognised values: "Carpenter", "Smelter". Others render generic. @export var label_text: String = "Workbench" ## Which skill category this bench accepts. ## CraftingProvider filters by this before assigning a pawn. @export var accepted_skill: StringName = &"crafting" # ── state ───────────────────────────────────────────────────────────────────── ## Ticks of construction work applied so far. 0..BUILD_TICKS. var build_progress: int = 0 ## True once build_progress >= BUILD_TICKS. var _completed: bool = false ## Ordered queue of Bill objects. Untyped so this file compiles before the ## Bill class is registered by the sibling agent. CraftingProvider reads this. var bills: Array = [] ## The Bill being actively worked right now. null when idle. ## Set by JobRunner when it begins a craft toil; cleared on completion or ## when the pawn walks away (job cancelled / interrupted). var current_bill = null ## Sim-tick progress within the current craft cycle. Incremented by ## JobRunner._tick_craft once per sim tick. Reset to 0: ## - when a craft completes (on_craft_complete) ## - when no pawn is actively crafting (JobRunner cancel / pawn interruption) ## JobRunner reads this to decide whether the recipe's work_ticks are done. var current_work_progress: int = 0 # ── lifecycle ───────────────────────────────────────────────────────────────── func _ready() -> void: # Position is bottom-anchored so Y-sort occludes pawns correctly. position = Vector2( tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, tile.y * TILE_SIZE_PX + TILE_SIZE_PX ) World.register_workbench(self) queue_redraw() func _exit_tree() -> void: World.unregister_workbench(self) ## 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_SIZE_PX / 2.0, tile.y * TILE_SIZE_PX + TILE_SIZE_PX ) queue_redraw() # ── BuildJob interface ──────────────────────────────────────────────────────── ## True while the workbench still needs construction work. ## JobRunner's BUILD toil checks this to decide when the toil is done. func is_buildable() -> bool: return not _completed ## Human-readable label for job descriptions and Audit logs. func label() -> String: return label_text ## Called by the BUILD toil in JobRunner once per sim tick. ## Advances build_progress; completes the workbench at BUILD_TICKS. func on_build_tick() -> void: if _completed: return build_progress += 1 queue_redraw() if build_progress >= BUILD_TICKS: _complete() ## True once the workbench has been fully built. func is_completed() -> bool: return _completed # ── Bills ───────────────────────────────────────────────────────────────────── ## Append a bill to the queue. func add_bill(b) -> void: bills.append(b) Audit.log("workbench", "%s: bill added — recipe '%s'" % [label_text, b.recipe.id]) ## Return the first bill that is active and whose required_skill matches ## this bench's accepted_skill. Returns null when none qualify. ## CraftingProvider calls this; JobRunner also calls it when the current_bill ## becomes inactive (UNTIL_N threshold reached, paused, etc.). func find_active_bill(): for b in bills: if not b.is_active(): continue if b.recipe.required_skill != accepted_skill: continue return b return null # ── Craft-cycle hooks (called by JobRunner) ─────────────────────────────────── ## JobRunner calls this when it starts working a bill on this bench. ## Stores the active bill and resets the tick counter. func begin_craft(b) -> void: current_bill = b current_work_progress = 0 ## JobRunner calls this once per sim tick while a pawn is actively crafting. ## Returns true when the recipe's work_ticks have been reached (craft done). func tick_craft() -> bool: if current_bill == null: return false current_work_progress += 1 return current_work_progress >= current_bill.recipe.work_ticks ## JobRunner calls this on craft completion before spawning the output item. ## Records the completion on the bill, resets state. func on_craft_complete() -> void: if current_bill != null: current_bill.record_completion() current_bill = null current_work_progress = 0 ## JobRunner calls this when the craft is interrupted (pawn leaves, cancel, etc.). ## Resets in-progress state so another pawn can start fresh. func on_craft_interrupted() -> void: current_bill = null current_work_progress = 0 # ── save / load ─────────────────────────────────────────────────────────────── ## Serialise workbench state for World save (wired in Phase 16). func to_dict() -> Dictionary: var bills_data: Array = [] for b in bills: bills_data.append(b.to_dict()) return { "tile_x": tile.x, "tile_y": tile.y, "label_text": label_text, "accepted_skill": String(accepted_skill), "build_progress": build_progress, "completed": _completed, "current_work_progress": current_work_progress, "bills": bills_data, } ## Restore from a dict produced by to_dict(). ## Bill objects are reconstructed by the caller using Bill.from_dict() once the ## Bill class is registered. current_bill is left null — JobRunner reconnects ## from its own saved state on the next sim tick. func from_dict(d: Dictionary) -> void: tile = Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))) label_text = str(d.get("label_text", "Workbench")) accepted_skill = StringName(d.get("accepted_skill", "crafting")) build_progress = int(d.get("build_progress", 0)) _completed = bool(d.get("completed", false)) current_work_progress = int(d.get("current_work_progress", 0)) # Bills are re-populated by World.load_workbenches() after Bill class loads. # Raw dicts are kept in the dict; the caller handles reconstruction. setup(tile) # ── render ───────────────────────────────────────────────────────────────────── func _draw() -> void: # 3/4-perspective bench rendering — fits within the tile (16×16 local box). # Origin (0,0) = tile bottom-centre. Tile spans local Y: -16 to 0. # Two-band look (matches Wall): lit top band + shaded front face. # Ghost (not yet built) draws at 0.4 alpha. var alpha: float = 1.0 if _completed else 0.4 match label_text: "Carpenter": _draw_carpenter(alpha) "Smelter": _draw_smelter(alpha) _: _draw_generic(alpha) func _draw_carpenter(alpha: float) -> void: # Warm-brown wood bench. Top band lit, front face darker. # Vise/saw detail: a small darker square at the top-right corner of the # front face to suggest a mounted tool. var top_face := Color(0.62, 0.45, 0.25, alpha) var front_face := Color(0.52, 0.36, 0.18, alpha) var plank := Color(0.34, 0.22, 0.10, alpha) var vise := Color(0.28, 0.18, 0.08, alpha) var outline := Color(0.20, 0.12, 0.04, 0.7 * alpha) # Top face — lit strip at upper-third of tile. draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 5.0)), top_face) # Front face — lower body. draw_rect(Rect2(Vector2(-8.0, -11.0), Vector2(16.0, 11.0)), front_face) # Horizontal plank seam across the front face. draw_line(Vector2(-8.0, -6.0), Vector2(8.0, -6.0), plank, 1.0) # Vise detail: 4×4 px darker square at top-right of front face. draw_rect(Rect2(Vector2(3.0, -11.0), Vector2(4.0, 4.0)), vise) # Top/front edge horizon line. draw_line(Vector2(-8.0, -11.0), Vector2(8.0, -11.0), plank, 1.0) # Tile outline. draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) func _draw_smelter(alpha: float) -> void: # Dark grey stone block. Top band slightly lighter. # Ember glow: orange rect centred at the bottom of the front face. var top_face := Color(0.42, 0.42, 0.40, alpha) var front_face := Color(0.32, 0.32, 0.30, alpha) var mortar := Color(0.20, 0.20, 0.18, alpha) var ember := Color(0.95, 0.45, 0.10, alpha) var outline := Color(0.14, 0.14, 0.12, 0.7 * alpha) # Top face. draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 5.0)), top_face) # Front face. draw_rect(Rect2(Vector2(-8.0, -11.0), Vector2(16.0, 11.0)), front_face) # Mortar line. draw_line(Vector2(-8.0, -6.0), Vector2(8.0, -6.0), mortar, 1.0) # Ember glow: 6×3 px orange rect, horizontally centred, at bottom of front face. draw_rect(Rect2(Vector2(-3.0, -3.0), Vector2(6.0, 3.0)), ember) # Top/front edge horizon line. draw_line(Vector2(-8.0, -11.0), Vector2(8.0, -11.0), mortar, 1.0) # Tile outline. draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) func _draw_generic(alpha: float) -> void: # Warm-grey fallback bench. Simple two-band block with a single seam. var top_face := Color(0.58, 0.55, 0.50, alpha) var front_face := Color(0.42, 0.40, 0.36, alpha) var seam := Color(0.30, 0.28, 0.25, alpha) var outline := Color(0.20, 0.18, 0.16, 0.7 * alpha) draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 5.0)), top_face) draw_rect(Rect2(Vector2(-8.0, -11.0), Vector2(16.0, 11.0)), front_face) draw_line(Vector2(-8.0, -6.0), Vector2(8.0, -6.0), seam, 1.0) draw_line(Vector2(-8.0, -11.0), Vector2(8.0, -11.0), seam, 1.0) draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) # ── internal ────────────────────────────────────────────────────────────────── func _complete() -> void: _completed = true queue_redraw() Audit.log("workbench", "%s built at %s" % [label_text, tile])