diff --git a/art/sprites/FG_Tree_Fall.png b/art/sprites/FG_Tree_Fall.png new file mode 100644 index 0000000..6f9c887 Binary files /dev/null and b/art/sprites/FG_Tree_Fall.png differ 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 0000000..7408581 Binary files /dev/null and b/art/sprites/FG_Tree_Summer.png differ 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()