Workbench sprites — swap procedural draw for tileset art per variant
- Carpenter → FG_Interior (24, 20) 16×32 wood cabinet w/ drawers - Smelter → FG_Marketplace (8, 30) anvil + hot metal - Hearth → FG_Interior (16, 32) stove w/ burners - Millstone → FG_Interior (17, 40) wood barrel + procedural wheel overlay Adds label_text setter so the sprite rebuilds idempotently whether the caller assigns label before or after setup() (world.gd assigns after, SaveSystem after). Setter also calls _maybe_build_light() to fix a pre-existing Phase 11 bug where Hearth never built its PointLight2D (label_text was still default when _ready fired). Unrecognised label_texts fall through to _draw_generic so ad-hoc workbench variants keep rendering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b09db9fd6
commit
c4f94fb543
3 changed files with 170 additions and 118 deletions
BIN
art/tiles/FG_Marketplace.png
Normal file
BIN
art/tiles/FG_Marketplace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
40
art/tiles/FG_Marketplace.png.import
Normal file
40
art/tiles/FG_Marketplace.png.import
Normal file
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
_:
|
||||
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)
|
||||
## 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 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)
|
||||
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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue