Procedural workbench redraws + 3-season tree variety

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.
This commit is contained in:
megaproxy 2026-05-15 20:22:55 +01:00
parent 840db55b44
commit c97ada80d7
6 changed files with 278 additions and 113 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -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

View file

@ -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.

View file

@ -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()