Procedural shapes for unmapped item types (no more magenta squares)

Items without an atlas entry were rendering as a hue-hashed coloured
square — bread/grain/vegetable hashed to pink-magenta, indistinguish-
able from a missing-texture placeholder.

Added _draw_item_shape() dispatcher with category-appropriate silhouettes
for: bread (brown loaf), grain (wheat stalks), vegetable (root with
leaves), flour (cream sack), meal (wooden bowl with steam), meat (red
steak with marbling), cloth (blue pleated bolt), medicine (white phial
with red cross), tool (hammer), weapon (sword), armor (helmet),
stone_block (pale brick), copper_ore (copper chunks), silver (silver
nuggets), ash (grey pile with smoke).

Hue-hashed fallback retained for safety but should be unreachable now
that every ALL_TYPES entry is handled by either _ITEM_SPRITES or
_draw_item_shape.
This commit is contained in:
megaproxy 2026-05-16 15:38:45 +01:00
parent 4bb00a798b
commit ab4d62889b

View file

@ -176,20 +176,193 @@ static func from_dict(d: Dictionary) -> Dictionary:
# ── render ────────────────────────────────────────────────────────────────────
## Procedural shape per item type. All shapes draw inside a -6..+6 box so the
## quality border (half=6) wraps the result. Returns true if the type was
## handled; false sends the caller to the hue-hashed fallback.
##
## Shape language is loose silhouette + a category-appropriate colour: bread =
## brown loaf, grain = wheat-amber stalks, vegetable = green disc, meal = bowl,
## etc. Better than rendering 18 different magenta-pink squares; later art pass
## can lift these to atlas crops where the bundle has a fitting icon.
func _draw_item_shape(t: StringName) -> bool:
var dark := Color(0.10, 0.07, 0.05, 0.85) # shared outline
match t:
TYPE_BREAD:
# Two stacked brown loaves with a slash on the upper crust.
var crust := Color(0.62, 0.40, 0.18)
var glaze := Color(0.82, 0.58, 0.30)
draw_rect(Rect2(-6.0, -1.0, 12.0, 5.0), crust)
draw_rect(Rect2(-5.0, -5.0, 10.0, 4.0), glaze)
draw_line(Vector2(-3.0, -4.0), Vector2(0.0, -2.0), dark, 1.0)
draw_line(Vector2(0.0, -4.0), Vector2(3.0, -2.0), dark, 1.0)
draw_rect(Rect2(-6.0, -5.0, 12.0, 9.0), dark, false, 1.0)
return true
TYPE_GRAIN:
# Three vertical wheat stalks tied in the middle.
var stalk := Color(0.85, 0.70, 0.20)
var tie := Color(0.55, 0.35, 0.10)
draw_rect(Rect2(-4.0, -6.0, 1.5, 12.0), stalk)
draw_rect(Rect2(-0.75, -6.0, 1.5, 12.0), stalk)
draw_rect(Rect2(2.5, -6.0, 1.5, 12.0), stalk)
# Grain heads (small dots near top).
draw_circle(Vector2(-3.2, -5.0), 1.2, Color(0.95, 0.78, 0.25))
draw_circle(Vector2(0.0, -5.5), 1.2, Color(0.95, 0.78, 0.25))
draw_circle(Vector2(3.2, -5.0), 1.2, Color(0.95, 0.78, 0.25))
draw_rect(Rect2(-5.0, 0.0, 10.0, 2.0), tie)
return true
TYPE_FLOUR:
# Cream-white sack with a darker drawstring at the neck.
var sack := Color(0.93, 0.90, 0.78)
var shadow := Color(0.78, 0.74, 0.60)
draw_rect(Rect2(-5.0, -3.0, 10.0, 8.0), sack)
draw_rect(Rect2(3.0, -3.0, 2.0, 8.0), shadow)
draw_rect(Rect2(-4.0, -5.0, 8.0, 2.0), Color(0.45, 0.30, 0.10)) # drawstring band
draw_circle(Vector2(0.0, -5.5), 1.0, sack) # gathered top puff
draw_rect(Rect2(-5.0, -6.0, 10.0, 11.0), dark, false, 1.0)
return true
TYPE_VEGETABLE:
# Green-leafed root vegetable (think turnip).
var leaf := Color(0.25, 0.65, 0.20)
var root := Color(0.92, 0.88, 0.70)
draw_circle(Vector2(0.0, 1.0), 5.0, root)
draw_arc(Vector2(0.0, 1.0), 5.0, 0.0, TAU, 16, dark, 1.0)
# Leaves
draw_rect(Rect2(-3.0, -5.0, 2.0, 3.0), leaf)
draw_rect(Rect2(-1.0, -6.0, 2.0, 4.0), leaf)
draw_rect(Rect2( 1.0, -5.0, 2.0, 3.0), leaf)
return true
TYPE_MEAL:
# Wooden bowl with a steamy meal heap.
var bowl := Color(0.50, 0.30, 0.12)
var bowl_rim := Color(0.30, 0.18, 0.08)
var food := Color(0.85, 0.55, 0.20)
# Bowl as a half-disc.
draw_circle(Vector2(0.0, 1.0), 6.0, bowl)
draw_rect(Rect2(-6.0, -5.0, 12.0, 6.0), Color(0, 0, 0, 0)) # cover top half (no-op, rely on z)
draw_arc(Vector2(0.0, 1.0), 6.0, 0.0, PI, 16, bowl_rim, 1.0)
draw_line(Vector2(-6.0, 1.0), Vector2(6.0, 1.0), bowl_rim, 1.0)
# Food mound on top.
draw_circle(Vector2(0.0, 0.0), 3.5, food)
# Two steam wisps.
draw_line(Vector2(-2.0, -5.0), Vector2(-1.0, -3.0), Color(0.9, 0.9, 0.9, 0.7), 1.0)
draw_line(Vector2(2.0, -5.0), Vector2(1.0, -3.0), Color(0.9, 0.9, 0.9, 0.7), 1.0)
return true
TYPE_MEAT:
# Raw red steak with a pale fat marbling line.
var meat := Color(0.78, 0.20, 0.20)
var fat := Color(0.95, 0.85, 0.70)
draw_rect(Rect2(-5.0, -3.0, 10.0, 7.0), meat)
draw_line(Vector2(-5.0, 0.0), Vector2(5.0, 0.5), fat, 1.0)
draw_rect(Rect2(-5.0, -3.0, 10.0, 7.0), dark, false, 1.0)
return true
TYPE_CLOTH:
# Folded cloth bolt — light blue with horizontal pleats.
var cloth := Color(0.50, 0.65, 0.85)
var pleat := Color(0.32, 0.42, 0.58)
draw_rect(Rect2(-5.0, -4.0, 10.0, 8.0), cloth)
draw_line(Vector2(-5.0, -1.5), Vector2(5.0, -1.5), pleat, 1.0)
draw_line(Vector2(-5.0, 1.5), Vector2(5.0, 1.5), pleat, 1.0)
draw_rect(Rect2(-5.0, -4.0, 10.0, 8.0), dark, false, 1.0)
return true
TYPE_MEDICINE:
# White phial with a red cross — medieval-ish but reads instantly.
var phial := Color(0.95, 0.95, 0.95)
var cross := Color(0.80, 0.15, 0.15)
draw_rect(Rect2(-4.0, -5.0, 8.0, 10.0), phial)
draw_rect(Rect2(-1.0, -3.0, 2.0, 6.0), cross)
draw_rect(Rect2(-3.0, -1.0, 6.0, 2.0), cross)
draw_rect(Rect2(-4.0, -5.0, 8.0, 10.0), dark, false, 1.0)
return true
TYPE_TOOL:
# Brown-handled hammer.
var handle := Color(0.50, 0.30, 0.12)
var head := Color(0.45, 0.45, 0.48)
draw_rect(Rect2(-1.0, -2.0, 2.0, 8.0), handle)
draw_rect(Rect2(-5.0, -5.0, 10.0, 4.0), head)
draw_rect(Rect2(-5.0, -5.0, 10.0, 4.0), dark, false, 1.0)
return true
TYPE_WEAPON:
# Sword: triangular blade + brown grip + crossguard.
var blade := Color(0.78, 0.80, 0.85)
var guard := Color(0.45, 0.30, 0.10)
# Blade (pointing up)
var pts: PackedVector2Array = PackedVector2Array([Vector2(0.0, -6.0), Vector2(2.5, 1.0), Vector2(-2.5, 1.0)])
draw_colored_polygon(pts, blade)
# Crossguard
draw_rect(Rect2(-4.0, 1.0, 8.0, 2.0), guard)
# Grip
draw_rect(Rect2(-1.0, 3.0, 2.0, 3.0), guard)
return true
TYPE_ARMOR:
# Helmet silhouette — rounded grey dome with a nose-guard slit.
var steel := Color(0.65, 0.65, 0.70)
var visor := Color(0.20, 0.20, 0.25)
draw_circle(Vector2(0.0, 0.0), 5.5, steel)
draw_rect(Rect2(-1.0, -2.0, 2.0, 4.0), visor)
draw_rect(Rect2(-6.0, 4.0, 12.0, 2.0), steel)
draw_arc(Vector2(0.0, 0.0), 5.5, 0.0, TAU, 16, dark, 1.0)
return true
TYPE_STONE_BLOCK:
# Cleaned-up stone brick: pale grey rect with a chiseled corner.
var stone := Color(0.62, 0.60, 0.58)
var stone_hi := Color(0.78, 0.76, 0.72)
draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), stone)
draw_rect(Rect2(-6.0, -4.0, 12.0, 2.0), stone_hi)
draw_line(Vector2(-6.0, 0.0), Vector2(6.0, 0.0), Color(0.42, 0.40, 0.38), 1.0)
draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), dark, false, 1.0)
return true
TYPE_COPPER_ORE:
# Copper chunks — warm brown with bright highlights.
var copper := Color(0.65, 0.35, 0.18)
var hi := Color(0.92, 0.55, 0.20)
draw_circle(Vector2(-2.0, 1.0), 3.5, copper)
draw_circle(Vector2(2.0, -1.0), 2.8, copper)
draw_circle(Vector2(-2.0, 1.0), 1.5, hi)
draw_circle(Vector2(2.0, -1.0), 1.0, hi)
return true
TYPE_SILVER:
# Silver nugget — cool grey with a white highlight.
var silver := Color(0.78, 0.80, 0.85)
var hi := Color(0.98, 0.98, 1.00)
draw_circle(Vector2(-2.0, 1.0), 3.5, silver)
draw_circle(Vector2(2.0, -1.0), 2.8, silver)
draw_circle(Vector2(-2.0, 0.5), 1.2, hi)
return true
TYPE_ASH:
# Grey pile with faint smoke wisps.
var ash := Color(0.55, 0.55, 0.55)
var ash_hi := Color(0.78, 0.78, 0.78)
# Heap (low triangle)
var pts: PackedVector2Array = PackedVector2Array([Vector2(-6.0, 4.0), Vector2(6.0, 4.0), Vector2(0.0, -2.0)])
draw_colored_polygon(pts, ash)
draw_line(Vector2(-3.0, 1.0), Vector2(3.0, 1.0), ash_hi, 1.0)
# Smoke wisps
draw_line(Vector2(-1.0, -3.0), Vector2(0.0, -5.0), Color(0.85, 0.85, 0.85, 0.6), 1.0)
draw_line(Vector2(1.5, -3.0), Vector2(2.5, -5.0), Color(0.85, 0.85, 0.85, 0.6), 1.0)
return true
_:
return false
func _draw() -> void:
# Two render paths: types in _ITEM_SPRITES are painted by the Sprite2D child
# (built in setup()) — _draw() then only adds the quality border + stack
# count badge on top. Other types still use the procedural hue square so
# stockpile filtering remains visually unique while we expand the sprite set.
# Three render paths:
# 1. Atlas sprite (_ITEM_SPRITES): a Sprite2D child paints the icon;
# _draw() adds quality border + stack badge on top.
# 2. Procedural shape (_draw_item_shape returns true): bread/grain/
# vegetable/meal/flour/meat/cloth/medicine/etc. get a recognisable
# silhouette in their category colour.
# 3. Unknown fallback: hue-hashed coloured square. Should be unreachable
# once every ALL_TYPES entry is handled above — kept for safety.
var has_sprite: bool = _ITEM_SPRITES.has(item_type)
var half: int = 6 if not has_sprite else 8 # border hugs the 16×16 sprite
var square := Rect2(Vector2(-half, -half), Vector2(half * 2, half * 2))
if not has_sprite:
var hue := float(item_type.hash() % 360) / 360.0
var fill := Color.from_hsv(hue, 0.6, 0.85)
draw_rect(square, fill)
draw_rect(square, Color(0.0, 0.0, 0.0, 0.75), false, 1.0)
if not _draw_item_shape(item_type):
var hue := float(item_type.hash() % 360) / 360.0
var fill := Color.from_hsv(hue, 0.6, 0.85)
draw_rect(square, fill)
draw_rect(square, Color(0.0, 0.0, 0.0, 0.75), false, 1.0)
# Quality border — drawn over the dark outline (or sprite), colour per quality tier.
# NORMAL has no extra border.