Carry indicator: draw the actual item shape, scaled down

Pawns hauling items previously showed a 7×7 hue-coloured square — no
hint of what they were actually carrying. Now the carry indicator
calls Item.draw_item_shape(self, type) at 0.55× scale (~7px effective),
positioned at the pawn's upper-right (chest-height).

Refactor: _draw_item_shape (instance method) → Item.draw_item_shape
(static, takes a CanvasItem target). Item._draw() and Pawn._draw()
both call it. Also added shapes for the 5 atlas-backed types (wood /
stone / plank / iron_ore / gold) so the carry indicator works for
mining + carpenter outputs too — the on-floor visual still uses the
bundle atlas via the Sprite2D child.
This commit is contained in:
megaproxy 2026-05-16 15:46:13 +01:00
parent ab4d62889b
commit c7f97e2c7a
2 changed files with 127 additions and 102 deletions

View file

@ -176,169 +176,186 @@ 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.
## Procedural shape per item type, drawn onto an arbitrary CanvasItem. All
## shapes draw inside a -6..+6 box so the quality border (half=6) wraps the
## result, and the carry indicator (Pawn._draw) can scale the same shapes down
## via draw_set_transform. Returns true if the type was handled; false sends
## the caller to its 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:
## etc. Atlas-backed types (wood / stone / plank / iron_ore / gold) also have
## a shape here so the carry indicator works for them — the on-floor visual
## still uses the bundle icon via the Sprite2D child path.
static func draw_item_shape(target: CanvasItem, 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)
target.draw_rect(Rect2(-6.0, -1.0, 12.0, 5.0), crust)
target.draw_rect(Rect2(-5.0, -5.0, 10.0, 4.0), glaze)
target.draw_line(Vector2(-3.0, -4.0), Vector2(0.0, -2.0), dark, 1.0)
target.draw_line(Vector2(0.0, -4.0), Vector2(3.0, -2.0), dark, 1.0)
target.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)
target.draw_rect(Rect2(-4.0, -6.0, 1.5, 12.0), stalk)
target.draw_rect(Rect2(-0.75, -6.0, 1.5, 12.0), stalk)
target.draw_rect(Rect2(2.5, -6.0, 1.5, 12.0), stalk)
target.draw_circle(Vector2(-3.2, -5.0), 1.2, Color(0.95, 0.78, 0.25))
target.draw_circle(Vector2(0.0, -5.5), 1.2, Color(0.95, 0.78, 0.25))
target.draw_circle(Vector2(3.2, -5.0), 1.2, Color(0.95, 0.78, 0.25))
target.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)
target.draw_rect(Rect2(-5.0, -3.0, 10.0, 8.0), sack)
target.draw_rect(Rect2(3.0, -3.0, 2.0, 8.0), shadow)
target.draw_rect(Rect2(-4.0, -5.0, 8.0, 2.0), Color(0.45, 0.30, 0.10))
target.draw_circle(Vector2(0.0, -5.5), 1.0, sack)
target.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)
target.draw_circle(Vector2(0.0, 1.0), 5.0, root)
target.draw_arc(Vector2(0.0, 1.0), 5.0, 0.0, TAU, 16, dark, 1.0)
target.draw_rect(Rect2(-3.0, -5.0, 2.0, 3.0), leaf)
target.draw_rect(Rect2(-1.0, -6.0, 2.0, 4.0), leaf)
target.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)
target.draw_circle(Vector2(0.0, 1.0), 6.0, bowl)
target.draw_arc(Vector2(0.0, 1.0), 6.0, 0.0, PI, 16, bowl_rim, 1.0)
target.draw_line(Vector2(-6.0, 1.0), Vector2(6.0, 1.0), bowl_rim, 1.0)
target.draw_circle(Vector2(0.0, 0.0), 3.5, food)
target.draw_line(Vector2(-2.0, -5.0), Vector2(-1.0, -3.0), Color(0.9, 0.9, 0.9, 0.7), 1.0)
target.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)
target.draw_rect(Rect2(-5.0, -3.0, 10.0, 7.0), meat)
target.draw_line(Vector2(-5.0, 0.0), Vector2(5.0, 0.5), fat, 1.0)
target.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)
target.draw_rect(Rect2(-5.0, -4.0, 10.0, 8.0), cloth)
target.draw_line(Vector2(-5.0, -1.5), Vector2(5.0, -1.5), pleat, 1.0)
target.draw_line(Vector2(-5.0, 1.5), Vector2(5.0, 1.5), pleat, 1.0)
target.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)
target.draw_rect(Rect2(-4.0, -5.0, 8.0, 10.0), phial)
target.draw_rect(Rect2(-1.0, -3.0, 2.0, 6.0), cross)
target.draw_rect(Rect2(-3.0, -1.0, 6.0, 2.0), cross)
target.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)
target.draw_rect(Rect2(-1.0, -2.0, 2.0, 8.0), handle)
target.draw_rect(Rect2(-5.0, -5.0, 10.0, 4.0), head)
target.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)
target.draw_colored_polygon(pts, blade)
target.draw_rect(Rect2(-4.0, 1.0, 8.0, 2.0), guard)
target.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)
target.draw_circle(Vector2(0.0, 0.0), 5.5, steel)
target.draw_rect(Rect2(-1.0, -2.0, 2.0, 4.0), visor)
target.draw_rect(Rect2(-6.0, 4.0, 12.0, 2.0), steel)
target.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)
target.draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), stone)
target.draw_rect(Rect2(-6.0, -4.0, 12.0, 2.0), stone_hi)
target.draw_line(Vector2(-6.0, 0.0), Vector2(6.0, 0.0), Color(0.42, 0.40, 0.38), 1.0)
target.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)
target.draw_circle(Vector2(-2.0, 1.0), 3.5, copper)
target.draw_circle(Vector2(2.0, -1.0), 2.8, copper)
target.draw_circle(Vector2(-2.0, 1.0), 1.5, hi)
target.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)
target.draw_circle(Vector2(-2.0, 1.0), 3.5, silver)
target.draw_circle(Vector2(2.0, -1.0), 2.8, silver)
target.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)
target.draw_colored_polygon(pts, ash)
target.draw_line(Vector2(-3.0, 1.0), Vector2(3.0, 1.0), ash_hi, 1.0)
target.draw_line(Vector2(-1.0, -3.0), Vector2(0.0, -5.0), Color(0.85, 0.85, 0.85, 0.6), 1.0)
target.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
# Atlas-backed types — the on-floor visual is the bundle icon (Sprite2D
# child), but the carry indicator needs a simple shape since pawns can't
# draw an AtlasTexture inline. Shapes below approximate the atlas look.
TYPE_WOOD:
var wood := Color(0.55, 0.35, 0.18)
var wood_hi := Color(0.75, 0.50, 0.25)
target.draw_rect(Rect2(-6.0, -3.0, 12.0, 6.0), wood)
target.draw_line(Vector2(-6.0, -1.0), Vector2(6.0, -1.0), wood_hi, 1.0)
target.draw_line(Vector2(-6.0, 1.0), Vector2(6.0, 1.0), Color(0.40, 0.25, 0.10), 1.0)
target.draw_rect(Rect2(-6.0, -3.0, 12.0, 6.0), dark, false, 1.0)
return true
TYPE_PLANK:
var plank := Color(0.80, 0.60, 0.35)
var plank_grain := Color(0.55, 0.38, 0.20)
target.draw_rect(Rect2(-6.0, -3.0, 12.0, 6.0), plank)
target.draw_line(Vector2(-5.0, -1.0), Vector2(5.0, -1.0), plank_grain, 1.0)
target.draw_line(Vector2(-5.0, 1.0), Vector2(5.0, 1.0), plank_grain, 1.0)
target.draw_rect(Rect2(-6.0, -3.0, 12.0, 6.0), dark, false, 1.0)
return true
TYPE_STONE:
var stone := Color(0.60, 0.58, 0.55)
var stone_hi := Color(0.78, 0.76, 0.72)
target.draw_circle(Vector2(-1.5, 1.0), 4.0, stone)
target.draw_circle(Vector2(2.0, -0.5), 3.0, stone)
target.draw_circle(Vector2(-1.5, 1.0), 1.5, stone_hi)
return true
TYPE_IRON_ORE:
var ore := Color(0.42, 0.42, 0.50)
var ore_hi := Color(0.60, 0.62, 0.72)
target.draw_circle(Vector2(-1.5, 1.0), 4.0, ore)
target.draw_circle(Vector2(2.0, -0.5), 3.0, ore)
target.draw_circle(Vector2(2.0, -0.5), 1.2, ore_hi)
return true
TYPE_GOLD:
var gold := Color(0.92, 0.78, 0.20)
var gold_hi := Color(1.00, 0.95, 0.55)
target.draw_circle(Vector2(-1.5, 1.0), 4.0, gold)
target.draw_circle(Vector2(2.0, -0.5), 3.0, gold)
target.draw_circle(Vector2(-1.5, 0.5), 1.5, gold_hi)
return true
_:
return false
@ -358,7 +375,7 @@ func _draw() -> void:
var square := Rect2(Vector2(-half, -half), Vector2(half * 2, half * 2))
if not has_sprite:
if not _draw_item_shape(item_type):
if not Item.draw_item_shape(self, 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)

View file

@ -1176,12 +1176,20 @@ func _draw() -> void:
if _selected:
draw_arc(Vector2.ZERO, 10.0, 0.0, TAU, 32, Color(1.0, 0.9, 0.2, 0.85), 2.0)
# Phase 4 — carry indicator: small coloured square at upper-right of body.
# Carry indicator — draw a shrunk version of the actual item shape at the
# pawn's upper-right ("held out at chest height"). Uses Item.draw_item_shape
# at 0.55× scale (the shapes are authored for a 12×12 box; 0.55× → ~7×7).
# Falls back to a hue-hashed square if the item type isn't shape-registered.
if carried_item != null:
var ci_hue := float(carried_item.item_type.hash() % 360) / 360.0
var ci_color := Color.from_hsv(ci_hue, 0.6, 0.85)
draw_rect(Rect2(6, -10, 7, 7), ci_color)
draw_rect(Rect2(6, -10, 7, 7), Color(0, 0, 0, 0.7), false, 1.0)
var ci_center := Vector2(7.0, -12.0)
var ci_scale := 0.55
draw_set_transform(ci_center, 0.0, Vector2(ci_scale, ci_scale))
if not Item.draw_item_shape(self, carried_item.item_type):
var ci_hue := float(carried_item.item_type.hash() % 360) / 360.0
var ci_color := Color.from_hsv(ci_hue, 0.6, 0.85)
draw_rect(Rect2(-6, -6, 12, 12), ci_color)
draw_rect(Rect2(-6, -6, 12, 12), Color(0, 0, 0, 0.7), false, 1.0)
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
# ── helpers ─────────────────────────────────────────────────────────────────