diff --git a/art/tiles/FG_Marketplace.png b/art/tiles/FG_Marketplace.png new file mode 100644 index 0000000..6893f8b Binary files /dev/null and b/art/tiles/FG_Marketplace.png differ diff --git a/art/tiles/FG_Marketplace.png.import b/art/tiles/FG_Marketplace.png.import new file mode 100644 index 0000000..8182d25 --- /dev/null +++ b/art/tiles/FG_Marketplace.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d24k3glxf7xt4" +path="res://.godot/imported/FG_Marketplace.png-44bb1301a91d0a5f6f9519ce316c9fed.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/tiles/FG_Marketplace.png" +dest_files=["res://.godot/imported/FG_Marketplace.png-44bb1301a91d0a5f6f9519ce316c9fed.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/workbench.gd b/scenes/entities/workbench.gd index b294b34..76dc924 100644 --- a/scenes/entities/workbench.gd +++ b/scenes/entities/workbench.gd @@ -42,14 +42,64 @@ 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. +## +## 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}, +} + # ── 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", "Hearth", "Millstone". Others render generic. -@export var label_text: String = "Workbench" +## 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. +## (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() + # 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 + # default "Workbench" and skipped the light. Build it lazily here + # so Hearth workbenches actually glow. Pre-existing bug since Phase 11. + _maybe_build_light() + + +## Build the PointLight2D for light-emitting workbenches if it doesn't exist +## yet. Idempotent — safe to call from both _ready() and the label_text setter. +## Enabled state is decided by is_on() (false until _complete fires). +func _maybe_build_light() -> void: + if _light != null: + return + if not label_text in LIGHT_EMITTING_LABELS: + return + _light = _build_point_light_2d() + add_child(_light) + _light.enabled = is_on() ## Which skill category this bench accepts. ## CraftingProvider filters by this before assigning a pawn. @@ -98,10 +148,12 @@ func _ready() -> void: # All workbenches register; non-emitters return false from is_on() so # World.is_tile_lit() skips them at zero cost. World.register_light_source(self) - if label_text in LIGHT_EMITTING_LABELS: - _light = _build_point_light_2d() - add_child(_light) - _light.enabled = false # dark until built + # Builds the PointLight2D for light-emitting workbenches. Usually a no-op + # here because the standard call pattern is add_child → setup → set label + # AFTER _ready, so label_text is still the default. The label_text setter + # calls _maybe_build_light() again when the real label lands — that's the + # one that actually wires the light. Idempotent. + _maybe_build_light() queue_redraw() @@ -111,15 +163,61 @@ 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 position = Vector2( 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_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. @@ -267,120 +365,29 @@ func from_dict(d: Dictionary) -> void: # ── 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. + # 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 - - match label_text: - "Carpenter": - _draw_carpenter(alpha) - "Smelter": - _draw_smelter(alpha) - "Hearth": - _draw_hearth(alpha) - "Millstone": - _draw_millstone(alpha) - _: - _draw_generic(alpha) + if label_text == "Millstone": + _draw_millstone_overlay(alpha) + return + if _VARIANT_SPRITES.has(label_text): + return + _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_hearth(alpha: float) -> void: - # Dark grey stone block with a large orange flame at centre of front face and - # a thin smoke wisp poking above the top face. Visually heavier than the - # Smelter (which has a small ember) to signal open-fire cooking. - var top_face := Color(0.35, 0.30, 0.25, alpha) - var front_face := Color(0.28, 0.24, 0.20, alpha) - var mortar := Color(0.18, 0.14, 0.12, alpha) - var flame := Color(0.95, 0.55, 0.10, alpha) - var smoke := Color(0.72, 0.70, 0.68, alpha * 0.6) - var outline := Color(0.14, 0.10, 0.08, 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 seam. - draw_line(Vector2(-8.0, -6.0), Vector2(8.0, -6.0), mortar, 1.0) - # Flame: 6×4 px orange rect, horizontally centred in the front face. - draw_rect(Rect2(Vector2(-3.0, -8.0), Vector2(6.0, 4.0)), flame) - # Smoke wisp: 1×2 px vertical light-grey rect rising above the top face. - draw_rect(Rect2(Vector2(-0.5, -18.0), Vector2(1.0, 2.0)), smoke) - # 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_millstone(alpha: float) -> void: - # Very light grey stone block with a circular dark-grey stone wheel inset - # at the centre of the front face. Suggests a grinding wheel. - var top_face := Color(0.78, 0.78, 0.72, alpha) - var front_face := Color(0.65, 0.65, 0.60, alpha) - var seam := Color(0.45, 0.45, 0.42, alpha) - var wheel := Color(0.40, 0.40, 0.36, alpha) - var outline := Color(0.28, 0.28, 0.26, 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) - # Seam. - draw_line(Vector2(-8.0, -6.0), Vector2(8.0, -6.0), seam, 1.0) - # Stone wheel: filled circle radius 5 px, centred on the front face. - draw_circle(Vector2(0.0, -5.5), 5.0, wheel) - # Top/front edge horizon line. - draw_line(Vector2(-8.0, -11.0), Vector2(8.0, -11.0), seam, 1.0) - # Tile outline. - draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) +## 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) func _draw_generic(alpha: float) -> void: @@ -401,6 +408,11 @@ 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 # Phase 11: enable PointLight2D for light-emitting workbenches on completion. if _light != null: _light.enabled = is_on()