From c97ada80d745e296b991a65c2239c9c31e10389d Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 15 May 2026 20:22:55 +0100 Subject: [PATCH] Procedural workbench redraws + 3-season tree variety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workbenches: replace atlas sprites (which read as chest-of-drawers, candle base, kitchen stove, cushion stack) with procedural _draw_ methods following CremationPyre._draw_pyre's pattern. Carpenter shows a wood bench with saw + log slabs; Smelter a stone furnace with smoking chimney; Hearth a tall h=2 stone fireplace with arched opening + log fire; Millstone a wood frame supporting a round grindstone wheel. Trees: add Summer + Fall atlases alongside Spring (12 visual variants from 4 silhouettes × 3 seasons). Selection hash mixes season independently so neighbouring tiles don't all share the same palette. --- art/sprites/FG_Tree_Fall.png | Bin 0 -> 3220 bytes art/sprites/FG_Tree_Fall.png.import | 40 ++++ art/sprites/FG_Tree_Summer.png | Bin 0 -> 3225 bytes art/sprites/FG_Tree_Summer.png.import | 40 ++++ scenes/entities/tree.gd | 37 ++-- scenes/entities/workbench.gd | 274 ++++++++++++++++---------- 6 files changed, 278 insertions(+), 113 deletions(-) create mode 100644 art/sprites/FG_Tree_Fall.png create mode 100644 art/sprites/FG_Tree_Fall.png.import create mode 100644 art/sprites/FG_Tree_Summer.png create mode 100644 art/sprites/FG_Tree_Summer.png.import diff --git a/art/sprites/FG_Tree_Fall.png b/art/sprites/FG_Tree_Fall.png new file mode 100644 index 0000000000000000000000000000000000000000..6f9c887f79b2865e7db1e693b6bf674f8d7ad890 GIT binary patch literal 3220 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 J_KZjTe*n0OIKKb@ literal 0 HcmV?d00001 diff --git a/art/sprites/FG_Tree_Fall.png.import b/art/sprites/FG_Tree_Fall.png.import new file mode 100644 index 0000000..c735e56 --- /dev/null +++ b/art/sprites/FG_Tree_Fall.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://qen8u4g1oee4" +path="res://.godot/imported/FG_Tree_Fall.png-63225671846c6354f20ce0b5e0a5e31e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/FG_Tree_Fall.png" +dest_files=["res://.godot/imported/FG_Tree_Fall.png-63225671846c6354f20ce0b5e0a5e31e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/art/sprites/FG_Tree_Summer.png b/art/sprites/FG_Tree_Summer.png new file mode 100644 index 0000000000000000000000000000000000000000..7408581a5dc9aea940382a664f3192b7c75ffd6f GIT binary patch literal 3225 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 TW1Q?;9tOB*X=_ntek1C?(i$UZ literal 0 HcmV?d00001 diff --git a/art/sprites/FG_Tree_Summer.png.import b/art/sprites/FG_Tree_Summer.png.import new file mode 100644 index 0000000..3691b91 --- /dev/null +++ b/art/sprites/FG_Tree_Summer.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cepguvswk8ecj" +path="res://.godot/imported/FG_Tree_Summer.png-1bc1b92f64d4677ada2e125e991dc553.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/FG_Tree_Summer.png" +dest_files=["res://.godot/imported/FG_Tree_Summer.png-1bc1b92f64d4677ada2e125e991dc553.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/scenes/entities/tree.gd b/scenes/entities/tree.gd index 52177bf..50b2a28 100644 --- a/scenes/entities/tree.gd +++ b/scenes/entities/tree.gd @@ -34,14 +34,23 @@ var chop_designated: bool = false # Preloaded scene for spawned wood items. const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") -## ElvGames Grasslands tree pack — 4 variants laid out left-to-right. -## Each variant is 64×80 px; trunk base sits in the bottom ~10 rows. We anchor -## the sprite center 32 px above tile origin so the trunk bottom lands at the -## tile's bottom edge and the canopy rises into the cells above. -const _TREE_TEX: Texture2D = preload("res://art/sprites/FG_Tree_Spring.png") +## ElvGames Grasslands tree pack — 4 silhouettes laid out left-to-right (64×80 +## each). Trunk base sits in the bottom ~10 rows; we anchor the sprite centre +## 32 px above tile origin so the trunk bottom lands at the tile's bottom edge +## and the canopy rises into the cells above. +## +## Three season palettes (Spring / Summer / Fall) give 12 visual variants from +## the same silhouette set. Winter is omitted — snowy trees look out of place +## in the current biome. When a season-cycle system lands later, swap the +## active texture by season globally instead of per-tree. +const _TREE_TEXES: Array[Texture2D] = [ + preload("res://art/sprites/FG_Tree_Spring.png"), + preload("res://art/sprites/FG_Tree_Summer.png"), + preload("res://art/sprites/FG_Tree_Fall.png"), +] const _TREE_VARIANT_W: int = 64 const _TREE_VARIANT_H: int = 80 -const _TREE_VARIANT_COUNT: int = 4 +const _TREE_SILHOUETTES: int = 4 # silhouettes per atlas (columns) # ── lifecycle ───────────────────────────────────────────────────────────────── @@ -55,16 +64,20 @@ func _ready() -> void: World.register_tree(self) -## Adds a Sprite2D child painted with one of the 4 ElvGames tree variants. -## Variant chosen deterministically from the tile coord so the same tile always -## gets the same tree silhouette across boots and load/save. +## 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: var sprite := Sprite2D.new() sprite.name = "Sprite" - sprite.texture = _TREE_TEX + var hash_seed: int = tile.x * 31 + tile.y * 17 + var silhouette: int = hash_seed % _TREE_SILHOUETTES + # Independent hash mix for season so neighbouring tiles don't all match. + var season: int = ((hash_seed / _TREE_SILHOUETTES) + tile.x * 7 + tile.y * 11) % _TREE_TEXES.size() + sprite.texture = _TREE_TEXES[season] sprite.region_enabled = true - var variant: int = (tile.x * 31 + tile.y * 17) % _TREE_VARIANT_COUNT - sprite.region_rect = Rect2(variant * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H) + sprite.region_rect = Rect2(silhouette * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H) sprite.centered = true # Lift the sprite up so its bottom edge sits at the tile's bottom row. # Sprite center is at offset.y; sprite half-height is _TREE_VARIANT_H/2 = 40. diff --git a/scenes/entities/workbench.gd b/scenes/entities/workbench.gd index 51af0fd..029ea00 100644 --- a/scenes/entities/workbench.gd +++ b/scenes/entities/workbench.gd @@ -1,13 +1,15 @@ 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. +## Rendered procedurally (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 +## "Carpenter" → wooden workbench with saw + log slabs on top +## "Smelter" → dark stone furnace with chimney and ember glow +## "Hearth" → tall stone fireplace with mantle + log fire (h=2 tiles) +## "Millstone" → wooden frame supporting a round grindstone wheel ## Other → generic warm-grey fallback ## ## Bill model (architecture.md "Production: workbenches, recipes, bills"): @@ -42,45 +44,34 @@ const HEARTH_LIGHT_RADIUS: int = 5 ## Pixel size of the procedural radial gradient used for PointLight2D. const LIGHT_TEXTURE_SIZE: int = 64 -# ── sprite atlas (replaces procedural _draw for the four named variants) ───── -## Variant → (texture, atlas top-left coord, height in tiles). Selected from -## the ElvGames House Interior + Marketplace tilesets in the 2026-05-12 visual -## pass; see /tmp/workbench_candidates_v2.png from that session for the diff. +# ── variant rendering ───────────────────────────────────────────────────────── +## All four named workbench variants render procedurally via _draw(). The +## atlas-sprite approach was abandoned in the 2026-05-15 polish pass after +## visual review: the chosen ElvGames atlas tiles read as a chest-of-drawers +## (Carpenter), a tiny candle base (Smelter), a 2-burner stove (Hearth), and +## a stack of cushions (Millstone). Procedural draws give us shape control to +## hit the silhouettes those names imply. See CremationPyre._draw_pyre() for +## precedent — same pattern, local coords centered at (0, 0) at the BOTTOM of +## the workbench tile, drawing UP into negative y. ## -## h_tiles = 2 sprites bottom-anchor and extend UP into the tile above (Bed -## pattern) so the carpenter's tall cabinet reads as a piece of furniture -## standing in the room rather than a flat decal. h_tiles = 1 sprites stay -## within the workbench tile (anvil, stove top, barrel — squat shapes). -## -## Unrecognised label_texts fall through to procedural _draw_generic, so -## ad-hoc workbench variants keep rendering until a sprite is picked for them. -const _INTERIOR_TEX: Texture2D = preload("res://art/tiles/FG_Interior.png") -const _MARKETPLACE_TEX: Texture2D = preload("res://art/tiles/FG_Marketplace.png") -const _VARIANT_SPRITES: Dictionary = { - "Carpenter": {"tex": _INTERIOR_TEX, "coord": Vector2i(24, 20), "h_tiles": 2}, - "Smelter": {"tex": _MARKETPLACE_TEX, "coord": Vector2i(8, 30), "h_tiles": 1}, - "Hearth": {"tex": _INTERIOR_TEX, "coord": Vector2i(16, 32), "h_tiles": 1}, - "Millstone": {"tex": _INTERIOR_TEX, "coord": Vector2i(17, 40), "h_tiles": 1}, -} +## Tall variants (Hearth, h=2 logically) draw above y=-16 into the tile north +## of the bench; pawns stand correctly behind them because y_sort_enabled is +## on and position.y is anchored at the bench-tile's bottom edge. # ── exports ─────────────────────────────────────────────────────────────────── ## Tile position of this workbench in world-tile coordinates. @export var tile: Vector2i = Vector2i.ZERO -## Player-visible label. Also drives the sprite variant (see _VARIANT_SPRITES) -## and procedural _draw fallback for unrecognised values. -## Setter rebuilds the sprite child idempotently — callers can assign -## label_text either before OR after setup() and end up with the right sprite. +## Player-visible label. Also drives the procedural _draw() variant dispatch. +## Setter triggers a redraw + lazy light build — callers can assign label_text +## either before OR after setup() and the visual catches up. ## (World.gd assigns it after setup(); SaveSystem._spawn_workbench too.) @export var label_text: String = "Workbench": set(value): label_text = value - # Setter fires from .tscn property initialisation BEFORE _ready, so - # guard the rebuild until the node is actually in the tree (children - # can't be added safely before then). if is_inside_tree(): - _build_sprite() + queue_redraw() # Hearth-light catch-up: _ready() builds the PointLight2D only when # label_text is already "Hearth", but the project's call pattern # (add_child first, then set label_text) means _ready always saw the @@ -165,8 +156,6 @@ func _exit_tree() -> void: ## One-shot initialiser. Call after add_child() so _ready() has fired. -## Builds the variant sprite using the current label_text — if the caller -## hasn't assigned label_text yet, the setter rebuilds the sprite on assignment. ## Idempotent (safe under save-load's instantiate → setup → from_dict → setup chain). func setup(p_tile: Vector2i) -> void: tile = p_tile @@ -174,52 +163,12 @@ func setup(p_tile: Vector2i) -> void: tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, tile.y * TILE_SIZE_PX + TILE_SIZE_PX ) - # Y-sort so a 16×32 Carpenter sprite (which rises into the tile north of - # the bench) occludes pawns standing behind it. Matches Bed / Wall. + # Y-sort so tall variants (Hearth) drawing into the tile north of the bench + # occlude pawns standing behind. Matches Bed / Wall. y_sort_enabled = true - _build_sprite() queue_redraw() -## Build the variant Sprite2D child (or no-op when label_text isn't in the -## sprite table — those fall through to procedural _draw rendering). -## Idempotent: frees any previous Sprite child first. Called from setup() AND -## from the label_text setter, so the sprite always matches the current variant. -func _build_sprite() -> void: - var prev := get_node_or_null("Sprite") - if prev != null: - prev.queue_free() - var data = _VARIANT_SPRITES.get(label_text) - if data == null: - # Generic / unknown variants keep procedural rendering. _draw_generic - # fires through the existing match in _draw(). - return - var sprite := Sprite2D.new() - sprite.name = "Sprite" - sprite.texture = data["tex"] - sprite.region_enabled = true - var coord: Vector2i = data["coord"] - var h_tiles: int = data["h_tiles"] - var pixels_h: int = TILE_SIZE_PX * h_tiles - sprite.region_rect = Rect2( - coord.x * TILE_SIZE_PX, - coord.y * TILE_SIZE_PX, - TILE_SIZE_PX, - pixels_h, - ) - sprite.centered = true - # Parent position.y is at the BOTTOM of the workbench tile (see setup()). - # Bottom-anchor the sprite by offsetting it up by half its height, so a - # 16×16 sprite spans local y −16..0 (within the bench tile) and a 16×32 - # sprite spans local y −32..0 (bench tile + the tile above it, like Bed - # but extending UPWARD — workbenches don't have a "foot tile"). - sprite.offset = Vector2(0.0, -float(pixels_h) / 2.0) - sprite.z_index = 0 - # Ghost state — translucent until built. Solidified in _complete(). - sprite.modulate.a = 1.0 if _completed else 0.4 - add_child(sprite) - - # ── BuildJob interface ──────────────────────────────────────────────────────── ## True while the workbench still needs construction work. @@ -367,29 +316,156 @@ func from_dict(d: Dictionary) -> void: # ── render ───────────────────────────────────────────────────────────────────── func _draw() -> void: - # Sprite-backed variants (Carpenter / Smelter / Hearth) render entirely - # through their Sprite2D child — no procedural fallback needed. Millstone - # also has a sprite but keeps a small dark-grey wheel overlay so the - # wood barrel below reads as "grinding station" rather than a plain barrel. - # Unrecognised label_texts fall through to _draw_generic so ad-hoc - # benches still render until a sprite is picked for them. var alpha: float = 1.0 if _completed else 0.4 - if label_text == "Millstone": - _draw_millstone_overlay(alpha) - return - if _VARIANT_SPRITES.has(label_text): - return - _draw_generic(alpha) + match label_text: + "Carpenter": _draw_carpenter(alpha) + "Smelter": _draw_smelter(alpha) + "Hearth": _draw_hearth(alpha) + "Millstone": _draw_millstone(alpha) + _: _draw_generic(alpha) -## Stone-wheel overlay drawn on top of the Millstone barrel sprite. Without -## this, the barrel reads as "water/grain storage" rather than a millstone. -## The circle sits inside the top half of the barrel's tile. -func _draw_millstone_overlay(alpha: float) -> void: - var wheel := Color(0.40, 0.40, 0.36, alpha) - var rim := Color(0.22, 0.22, 0.20, alpha) - draw_circle(Vector2(0.0, -10.0), 4.5, wheel) - draw_arc(Vector2(0.0, -10.0), 4.5, 0.0, TAU, 12, rim, 1.0) +## Carpenter — wooden plank top with two visible legs and a hand-saw + log +## slabs on top. Reads as a workshop bench at 16×16 thanks to the saw blade +## silhouette breaking the plain top. +func _draw_carpenter(alpha: float) -> void: + var plank_top := Color(0.70, 0.50, 0.30, alpha) + var plank_front := Color(0.55, 0.38, 0.22, alpha) + var plank_edge := Color(0.35, 0.22, 0.12, alpha) + var leg := Color(0.30, 0.20, 0.10, alpha) + var saw_blade := Color(0.78, 0.78, 0.82, alpha) + var saw_handle := Color(0.55, 0.30, 0.15, alpha) + var log_face := Color(0.62, 0.42, 0.24, alpha) + var log_ring := Color(0.42, 0.27, 0.14, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Two legs at the corners (front face). + draw_rect(Rect2(Vector2(-7.0, -10.0), Vector2(2.0, 10.0)), leg) + draw_rect(Rect2(Vector2( 5.0, -10.0), Vector2(2.0, 10.0)), leg) + # Plank front face — thick band. + draw_rect(Rect2(Vector2(-8.0, -12.0), Vector2(16.0, 5.0)), plank_front) + # Plank top — slimmer band above the front, suggesting depth. + draw_rect(Rect2(Vector2(-8.0, -15.0), Vector2(16.0, 3.0)), plank_top) + # Edge highlight between top and front. + draw_line(Vector2(-8.0, -12.0), Vector2(8.0, -12.0), plank_edge, 1.0) + # Two short log slabs sitting on the left side of the top. + draw_rect(Rect2(Vector2(-6.0, -17.0), Vector2(3.0, 2.0)), log_face) + draw_rect(Rect2(Vector2(-3.0, -17.0), Vector2(3.0, 2.0)), log_face) + draw_line(Vector2(-4.5, -17.0), Vector2(-4.5, -15.0), log_ring, 1.0) + # Saw on the right — handle + blade silhouette. + draw_rect(Rect2(Vector2(1.0, -16.0), Vector2(6.0, 1.5)), saw_blade) + draw_rect(Rect2(Vector2(5.5, -17.0), Vector2(2.0, 2.0)), saw_handle) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) + + +## Smelter — stone furnace block with a stubby chimney puffing smoke and a +## bright ember-glow opening on the front face. Stone-grey base separates it +## visually from the Carpenter's warm wood. +func _draw_smelter(alpha: float) -> void: + var stone_top := Color(0.55, 0.55, 0.55, alpha) + var stone_front := Color(0.42, 0.42, 0.43, alpha) + var stone_shad := Color(0.30, 0.30, 0.32, alpha) + var ember := Color(0.98, 0.55, 0.10, alpha) + var ember_core := Color(1.00, 0.85, 0.30, alpha) + var chimney := Color(0.32, 0.30, 0.30, alpha) + var smoke := Color(0.75, 0.73, 0.70, alpha * 0.7) + var outline := Color(0.15, 0.12, 0.10, 0.7 * alpha) + + # Stone front body. + draw_rect(Rect2(Vector2(-8.0, -12.0), Vector2(16.0, 12.0)), stone_front) + # Top face — slightly lighter band. + draw_rect(Rect2(Vector2(-8.0, -15.0), Vector2(16.0, 3.0)), stone_top) + # Furnace mouth — dark recess with bright ember inside. + draw_rect(Rect2(Vector2(-4.0, -9.0), Vector2(8.0, 5.0)), stone_shad) + draw_rect(Rect2(Vector2(-3.0, -8.0), Vector2(6.0, 3.0)), ember) + draw_rect(Rect2(Vector2(-2.0, -7.0), Vector2(4.0, 1.0)), ember_core) + # Mortar lines across the front for stone-block feel. + draw_line(Vector2(-8.0, -8.0), Vector2(-4.0, -8.0), stone_shad, 1.0) + draw_line(Vector2( 4.0, -8.0), Vector2( 8.0, -8.0), stone_shad, 1.0) + # Chimney + smoke wisps rising above. + draw_rect(Rect2(Vector2(2.0, -19.0), Vector2(3.0, 4.0)), chimney) + draw_rect(Rect2(Vector2(3.0, -22.0), Vector2(1.0, 3.0)), smoke) + draw_rect(Rect2(Vector2(2.0, -24.0), Vector2(1.0, 2.0)), smoke) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) + + +## Hearth — tall (h=2) stone fireplace with mantle, arched opening, log fire +## with embers, and a flame licking up. Draws above y=-16 into the tile north +## of the bench (y_sort handles occlusion). Light-emitting via _maybe_build_light. +func _draw_hearth(alpha: float) -> void: + var stone := Color(0.60, 0.58, 0.55, alpha) + var stone_dark := Color(0.42, 0.40, 0.38, alpha) + var mantle := Color(0.50, 0.34, 0.20, alpha) + var mantle_edge := Color(0.32, 0.20, 0.10, alpha) + var opening := Color(0.08, 0.04, 0.02, alpha) + var log_wood := Color(0.55, 0.32, 0.15, alpha) + var ember := Color(0.98, 0.55, 0.10, alpha) + var flame_inner := Color(1.00, 0.85, 0.30, alpha) + var flame_outer := Color(0.95, 0.40, 0.05, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Stone surround — fills the bench tile (y −16..0) and the tile above + # (y −32..-16) so the fireplace is a 16×32 silhouette. + draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), stone) + # Stone block mortar — a couple of horizontal seams. + draw_line(Vector2(-8.0, -22.0), Vector2(8.0, -22.0), stone_dark, 1.0) + draw_line(Vector2(-8.0, -28.0), Vector2(8.0, -28.0), stone_dark, 1.0) + draw_line(Vector2(-2.0, -32.0), Vector2(-2.0, -28.0), stone_dark, 1.0) + draw_line(Vector2( 3.0, -28.0), Vector2( 3.0, -22.0), stone_dark, 1.0) + # Wooden mantle — horizontal beam across the middle. + draw_rect(Rect2(Vector2(-8.0, -19.0), Vector2(16.0, 3.0)), mantle) + draw_line(Vector2(-8.0, -19.0), Vector2(8.0, -19.0), mantle_edge, 1.0) + draw_line(Vector2(-8.0, -16.0), Vector2(8.0, -16.0), mantle_edge, 1.0) + # Arched opening — dark recess in the lower stone block. + draw_rect(Rect2(Vector2(-6.0, -14.0), Vector2(12.0, 14.0)), opening) + # Two stacked logs sitting in the opening. + draw_rect(Rect2(Vector2(-5.0, -4.0), Vector2(10.0, 2.0)), log_wood) + draw_rect(Rect2(Vector2(-4.0, -6.0), Vector2(8.0, 2.0)), log_wood) + # Ember strip glowing under the logs. + draw_rect(Rect2(Vector2(-4.0, -2.0), Vector2(8.0, 2.0)), ember) + # Flame — tapered teardrop above the logs. + draw_rect(Rect2(Vector2(-3.0, -10.0), Vector2(6.0, 4.0)), flame_outer) + draw_rect(Rect2(Vector2(-2.0, -12.0), Vector2(4.0, 2.0)), flame_outer) + draw_rect(Rect2(Vector2(-1.0, -13.0), Vector2(2.0, 1.0)), flame_outer) + draw_rect(Rect2(Vector2(-2.0, -9.0), Vector2(4.0, 2.0)), flame_inner) + draw_rect(Rect2(Vector2(-1.0, -11.0), Vector2(2.0, 2.0)), flame_inner) + # Outline around the full 16×32 silhouette. + draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), outline, false, 1.0) + + +## Millstone — wooden frame supporting a large round grindstone, viewed +## 3/4-perspective so the wheel reads as both round (top) and solid (front). +func _draw_millstone(alpha: float) -> void: + var frame_top := Color(0.55, 0.36, 0.18, alpha) + var frame_front := Color(0.42, 0.26, 0.12, alpha) + var frame_edge := Color(0.25, 0.14, 0.06, alpha) + var wheel := Color(0.55, 0.53, 0.50, alpha) + var wheel_dark := Color(0.34, 0.32, 0.30, alpha) + var wheel_rim := Color(0.18, 0.16, 0.14, alpha) + var groove := Color(0.28, 0.26, 0.24, alpha) + var pin := Color(0.20, 0.18, 0.16, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Wooden frame base — front + top faces. + 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) + # Grindstone — large dark-grey disc, rim slightly darker. Centred over + # the top of the frame, sticking up into the tile above only slightly. + var c := Vector2(0.0, -12.0) + draw_circle(c, 7.0, wheel_rim) + draw_circle(c, 6.0, wheel) + # Front-face shadow band across the lower half of the disc. + draw_rect(Rect2(Vector2(-6.0, -12.0), Vector2(12.0, 5.0)), wheel_dark) + # Two radial grooves — pie-slice indicators that the stone spins. + draw_line(c, c + Vector2(5.0, -3.5), groove, 1.0) + draw_line(c, c + Vector2(-5.0, -3.5), groove, 1.0) + # Centre pin / spindle. + draw_circle(c, 1.2, pin) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -19.0), Vector2(16.0, 19.0)), outline, false, 1.0) func _draw_generic(alpha: float) -> void: @@ -410,11 +486,7 @@ func _draw_generic(alpha: float) -> void: func _complete() -> void: _completed = true - # Solidify the ghost: sprite child (if any) goes from 40% to full opacity. - # Procedural-only variants reread alpha through _draw() via queue_redraw. - var sprite: Sprite2D = get_node_or_null("Sprite") - if sprite != null: - sprite.modulate.a = 1.0 + # Procedural-only variants re-read alpha through _draw() via queue_redraw. # Phase 11: enable PointLight2D for light-emitting workbenches on completion. if _light != null: _light.enabled = is_on()