rimlike/scenes/entities/item.gd
megaproxy d9638a4ea4 fix six critical bugs from audit sprint
save/load round-trip: workbench bills, crop static-method, bed owner,
wolf target now all survive reload via Bill.from_dict reconstruction,
_spawn_crop using setup(), and a new _post_load_resolve_references pass.

PlantProvider: sow path added; consumes 1 grain on a TILLED crop tile.

CraftingProvider: ingredient2 supported via new KIND_DEPOSIT_AT_WB toil
and Workbench.deposited_inputs buffer. Cremation pyre now actually
consumes wood.

HaulingProvider: per-item haul_retry_count + haul_rejected after 3
orphan passes; new EventBus.stockpile_layout_changed resets rejects on
any player stockpile edit.

Storyteller: 14 stubbed event effects implemented. New buff registry
(add_buff/get_buff_multiplier/has_buff, day-prune, save/load) drives
seasonal/resource events. New request_pawn_spawn signal + WANDERER
table for arrivals. New SICK status + 3 mood thoughts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:06:55 +01:00

528 lines
23 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Dropped item entity — a single stack of one item type lying on the world floor.
##
## Render model:
## For types in _ITEM_SPRITES (stone, wood, plank, iron_ore, gold, …) a Sprite2D
## child draws the ElvGames icon. For all other types _draw() falls back to a
## hue-hashed coloured square — the original Phase 4 placeholder kept around
## for the long tail of items (cloth, meals, corpses, ash, etc.).
## Quality border + stack-count badge are always drawn procedurally on top of
## whichever base layer renders, so the sprite swap is purely additive.
##
## Item type constants mirror the 16 filter chips in docs/design.md. They are
## used by StockpileZone filter bitmasks and pawn-carry typing.
##
## World registration (World.register_item / World.unregister_item) is called
## here but the methods land in World during Opus integration. The script will
## parse cleanly; the call will fail at runtime until then.
class_name Item extends Node2D
const TILE_SIZE_PX: int = 16
## ElvGames FG_Abandoned_Mines icons used for the most-spawned item types.
## Picked in the 2026-05-12 visual pass — see /tmp/item_finals.png for the
## side-by-side that produced these atlas coords. Other types still fall back
## to the procedural hue-hashed square in _draw().
const _MINES_TEX: Texture2D = preload("res://art/tiles/FG_Abandoned_Mines.png")
const _ITEM_SPRITES: Dictionary = {
&"stone": {"tex": _MINES_TEX, "coord": Vector2i(5, 33)},
&"iron_ore": {"tex": _MINES_TEX, "coord": Vector2i(9, 33)},
&"gold": {"tex": _MINES_TEX, "coord": Vector2i(13, 33)},
&"wood": {"tex": _MINES_TEX, "coord": Vector2i(20, 13)},
&"plank": {"tex": _MINES_TEX, "coord": Vector2i(28, 18)},
}
# ── canonical type registry — matches docs/design.md "16 filter chips" ───────
const TYPE_WOOD: StringName = &"wood" # Wd
const TYPE_STONE: StringName = &"stone" # St
const TYPE_IRON_ORE: StringName = &"iron_ore" # Ir
const TYPE_COPPER_ORE: StringName = &"copper_ore" # Cu
const TYPE_SILVER: StringName = &"silver" # Ag
const TYPE_GOLD: StringName = &"gold" # Au
const TYPE_CLOTH: StringName = &"cloth" # Cl
const TYPE_VEGETABLE: StringName = &"vegetable" # Veg
const TYPE_MEAT: StringName = &"meat" # Mt
const TYPE_GRAIN: StringName = &"grain" # Gr
const TYPE_MEAL: StringName = &"meal" # Ck (cooked)
const TYPE_MEDICINE: StringName = &"medicine" # Md
const TYPE_TOOL: StringName = &"tool" # Tl
const TYPE_WEAPON: StringName = &"weapon" # Wp
const TYPE_ARMOR: StringName = &"armor" # Ar
const TYPE_CORPSE: StringName = &"corpse" # Co
# Phase 6 — intermediate crafted goods (carpenter bench + smelter outputs).
const TYPE_PLANK: StringName = &"plank"
const TYPE_STONE_BLOCK: StringName = &"stone_block"
# Phase 7 — cooking chain. Grain → Flour (millstone) → Bread (hearth).
# TYPE_MEAL (&"meal") is the generic cooked-dish output and already lives above
# in the 16-chip base set.
const TYPE_FLOUR: StringName = &"flour"
const TYPE_BREAD: StringName = &"bread"
# Phase 14 — cremation output. One ash item drops per cremated corpse.
const TYPE_ASH: StringName = &"ash"
const ALL_TYPES: Array[StringName] = [
TYPE_WOOD, TYPE_STONE, TYPE_IRON_ORE, TYPE_COPPER_ORE,
TYPE_SILVER, TYPE_GOLD, TYPE_CLOTH, TYPE_VEGETABLE,
TYPE_MEAT, TYPE_GRAIN, TYPE_MEAL, TYPE_MEDICINE,
TYPE_TOOL, TYPE_WEAPON, TYPE_ARMOR, TYPE_CORPSE,
TYPE_PLANK, TYPE_STONE_BLOCK,
TYPE_FLOUR, TYPE_BREAD,
TYPE_ASH,
]
# ── quality system (docs/architecture.md "Quality system") ───────────────────
# Rolled at craft-completion; stored per item; drives border colour in _draw().
enum Quality { SHODDY, NORMAL, EXCELLENT, MASTERWORK, LEGENDARY }
# ── state ────────────────────────────────────────────────────────────────────
@export var item_type: StringName = TYPE_WOOD
@export var stack_size: int = 1
@export var quality: Quality = Quality.NORMAL
## Visual subtype within the broader item_type. Lets multiple harvested produce
## share one storage-filter category (TYPE_GRAIN matches both wheat AND corn)
## while still rendering distinctly. Empty string = use item_type's default
## shape. Set by the spawning code (Crop.on_harvest_tick assigns "wheat"/"corn"
## /"potato"/"strawberry" based on crop_kind).
@export var subtype: StringName = &""
var tile: Vector2i = Vector2i.ZERO
## When true the on-floor visual is suppressed; the carrying pawn renders the
## carry indicator instead.
var being_carried: bool = false
## Hauling-retry fallback (memory.md 2026-05-10 design lock).
## Incremented each sim-tick pass in which HaulingProvider finds no valid
## destination for this item. After MAX_HAUL_RETRIES failures the provider
## stops offering the item and sets haul_rejected = true, surfacing the
## "no_stockpile_accepts" alert. Both fields reset when stockpile layout changes.
const MAX_HAUL_RETRIES: int = 3
var haul_retry_count: int = 0
var haul_rejected: bool = false
# ── lifecycle ─────────────────────────────────────────────────────────────────
func _ready() -> void:
position = _tile_to_world(tile)
visible = not being_carried
func _exit_tree() -> void:
World.unregister_item(self)
# ── public API ────────────────────────────────────────────────────────────────
## One-shot initialiser called by the spawning code (Tree.fell, Rock.mined, etc.)
## Sets all fields, syncs position, and registers with World.
func setup(p_type: StringName, p_stack: int, p_tile: Vector2i) -> void:
item_type = p_type
stack_size = p_stack
tile = p_tile
position = _tile_to_world(tile)
visible = not being_carried
_build_sprite()
queue_redraw()
World.register_item(self)
Audit.log("item", "spawned %s×%d at %s" % [item_type, stack_size, tile])
## Hide/show the on-floor sprite when the pawn picks up or drops this item.
func set_being_carried(value: bool) -> void:
being_carried = value
visible = not being_carried
## Build the Sprite2D child for item_type, if a tileset icon is registered.
## Idempotent: re-running drops the previous sprite first. Types without an
## entry in _ITEM_SPRITES render via the procedural fallback in _draw().
func _build_sprite() -> void:
var prev := get_node_or_null("Sprite")
if prev != null:
prev.queue_free()
var data = _ITEM_SPRITES.get(item_type)
if data == null:
return
var sprite := Sprite2D.new()
sprite.name = "Sprite"
sprite.texture = data["tex"]
sprite.region_enabled = true
var coord: Vector2i = data["coord"]
sprite.region_rect = Rect2(coord.x * TILE_SIZE_PX, coord.y * TILE_SIZE_PX, TILE_SIZE_PX, TILE_SIZE_PX)
sprite.centered = true
# Item position is the tile centre — Sprite2D centred at (0, 0) fills the
# 16×16 cell exactly. No bottom-anchoring offset (items are flat on the floor).
sprite.offset = Vector2.ZERO
sprite.z_index = 0
add_child(sprite)
# ── save / load ───────────────────────────────────────────────────────────────
func to_dict() -> Dictionary:
return {
"class_id": &"item",
"type": String(item_type),
"subtype": String(subtype),
"stack_size": stack_size,
"tile_x": tile.x,
"tile_y": tile.y,
"quality": int(quality),
"haul_retry_count": haul_retry_count,
"haul_rejected": haul_rejected,
}
## Returns a plain Dictionary spec for World.load_items() to instantiate from.
## Items cannot reconstruct themselves standalone — they need a parent in the
## scene tree. World adds the node, then calls setup() from the returned dict.
static func from_dict(d: Dictionary) -> Dictionary:
return {
"type": StringName(d.get("type", "wood")),
"subtype": StringName(d.get("subtype", "")),
"stack_size": int(d.get("stack_size", 1)),
"tile_x": int(d.get("tile_x", 0)),
"tile_y": int(d.get("tile_y", 0)),
"quality": int(d.get("quality", Quality.NORMAL)),
# Hauling-retry fields — default 0/false so older v2 saves load cleanly.
"haul_retry_count": int(d.get("haul_retry_count", 0)),
"haul_rejected": bool(d.get("haul_rejected", false)),
}
# ── render ────────────────────────────────────────────────────────────────────
## 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. 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, sub: StringName = &"") -> bool:
var dark := Color(0.10, 0.07, 0.05, 0.85) # shared outline
# Subtype dispatch — lets wheat / corn / potato / strawberry render
# distinctly even though they share TYPE_GRAIN or TYPE_VEGETABLE for
# storage-filter purposes. Falls through to the type dispatch if subtype
# is unrecognised so existing items don't blank out.
match sub:
&"wheat":
# Yellow stalks with grain heads — same as default TYPE_GRAIN shape.
var stalk := Color(0.85, 0.70, 0.20)
var tie := Color(0.55, 0.35, 0.10)
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
&"corn":
# Corn cob — yellow body with rows of kernel dots + green husk.
var husk := Color(0.30, 0.65, 0.20)
var cob := Color(0.95, 0.82, 0.30)
var kernel_dark := Color(0.70, 0.55, 0.15)
# Husk leaves splayed out at the top.
var husk_l: PackedVector2Array = PackedVector2Array([Vector2(-4.0, -5.0), Vector2(-2.0, -2.0), Vector2(-5.0, -1.0)])
var husk_r: PackedVector2Array = PackedVector2Array([Vector2(4.0, -5.0), Vector2(2.0, -2.0), Vector2(5.0, -1.0)])
target.draw_colored_polygon(husk_l, husk)
target.draw_colored_polygon(husk_r, husk)
# Cob body — vertical oval/rect with rounded corners.
target.draw_rect(Rect2(-3.0, -4.0, 6.0, 10.0), cob)
target.draw_circle(Vector2(0.0, 5.5), 3.0, cob)
# Kernel dots — 2 columns × 3 rows for texture.
for y in [-2.0, 0.0, 2.0, 4.0]:
target.draw_rect(Rect2(-2.0, y, 1.5, 1.5), kernel_dark)
target.draw_rect(Rect2(0.5, y, 1.5, 1.5), kernel_dark)
# Cob outline (open at top because of husk).
target.draw_arc(Vector2(0.0, 5.5), 3.0, 0.0, PI, 12, dark, 1.0)
return true
&"potato":
# Two brown lumps with sprout-eye dots — pile of potatoes.
var skin := Color(0.62, 0.45, 0.25)
var skin_dark := Color(0.42, 0.28, 0.15)
var eye := Color(0.25, 0.15, 0.05)
# Two overlapping potato ovals.
target.draw_circle(Vector2(-2.0, 1.0), 4.0, skin)
target.draw_circle(Vector2(2.5, -0.5), 3.5, skin)
# Outline.
target.draw_arc(Vector2(-2.0, 1.0), 4.0, 0.0, TAU, 16, skin_dark, 1.0)
target.draw_arc(Vector2(2.5, -0.5), 3.5, 0.0, TAU, 12, skin_dark, 1.0)
# Eye dots.
target.draw_circle(Vector2(-3.0, 0.0), 0.7, eye)
target.draw_circle(Vector2(-0.5, 2.0), 0.6, eye)
target.draw_circle(Vector2(3.5, -1.5), 0.7, eye)
return true
&"strawberry":
# Classic red strawberry — heart-shape body with green calyx on top
# and tiny yellow seed dots scattered on the surface.
var berry := Color(0.88, 0.18, 0.20)
var berry_dark := Color(0.62, 0.10, 0.10)
var leaf := Color(0.25, 0.60, 0.20)
var seed := Color(0.95, 0.85, 0.30)
# Body — wider top tapering to a point at the bottom.
var body: PackedVector2Array = PackedVector2Array([
Vector2(-5.0, -1.0), Vector2(-3.5, -3.0), Vector2(3.5, -3.0),
Vector2(5.0, -1.0), Vector2(3.0, 4.0), Vector2(0.0, 6.0), Vector2(-3.0, 4.0),
])
target.draw_colored_polygon(body, berry)
# Body outline.
target.draw_polyline(body + PackedVector2Array([body[0]]), berry_dark, 1.0)
# Green calyx (leaves) on top.
target.draw_rect(Rect2(-3.0, -5.0, 6.0, 2.0), leaf)
var leaf_top: PackedVector2Array = PackedVector2Array([Vector2(-2.0, -5.0), Vector2(0.0, -7.0), Vector2(2.0, -5.0)])
target.draw_colored_polygon(leaf_top, leaf)
# Seed speckles.
target.draw_circle(Vector2(-2.0, 1.0), 0.5, seed)
target.draw_circle(Vector2(2.0, 1.0), 0.5, seed)
target.draw_circle(Vector2(0.0, 3.0), 0.5, seed)
return true
# Fall through to type dispatch.
match t:
TYPE_BREAD:
var crust := Color(0.62, 0.40, 0.18)
var glaze := Color(0.82, 0.58, 0.30)
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:
var stalk := Color(0.85, 0.70, 0.20)
var tie := Color(0.55, 0.35, 0.10)
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:
var sack := Color(0.93, 0.90, 0.78)
var shadow := Color(0.78, 0.74, 0.60)
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:
var leaf := Color(0.25, 0.65, 0.20)
var root := Color(0.92, 0.88, 0.70)
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:
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)
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:
var meat := Color(0.78, 0.20, 0.20)
var fat := Color(0.95, 0.85, 0.70)
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:
var cloth := Color(0.50, 0.65, 0.85)
var pleat := Color(0.32, 0.42, 0.58)
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:
var phial := Color(0.95, 0.95, 0.95)
var cross := Color(0.80, 0.15, 0.15)
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:
var handle := Color(0.50, 0.30, 0.12)
var head := Color(0.45, 0.45, 0.48)
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:
var blade := Color(0.78, 0.80, 0.85)
var guard := Color(0.45, 0.30, 0.10)
var pts: PackedVector2Array = PackedVector2Array([Vector2(0.0, -6.0), Vector2(2.5, 1.0), Vector2(-2.5, 1.0)])
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:
var steel := Color(0.65, 0.65, 0.70)
var visor := Color(0.20, 0.20, 0.25)
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:
var stone := Color(0.62, 0.60, 0.58)
var stone_hi := Color(0.78, 0.76, 0.72)
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:
var copper := Color(0.65, 0.35, 0.18)
var hi := Color(0.92, 0.55, 0.20)
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:
var silver := Color(0.78, 0.80, 0.85)
var hi := Color(0.98, 0.98, 1.00)
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:
var ash := Color(0.55, 0.55, 0.55)
var ash_hi := Color(0.78, 0.78, 0.78)
var pts: PackedVector2Array = PackedVector2Array([Vector2(-6.0, 4.0), Vector2(6.0, 4.0), Vector2(0.0, -2.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
func _draw() -> void:
# 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:
if not Item.draw_item_shape(self, item_type, subtype):
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.
match quality:
Quality.SHODDY:
draw_rect(square, Color(0.40, 0.40, 0.40), false, 1.0)
Quality.EXCELLENT:
draw_rect(square, Color(0.20, 0.55, 0.95), false, 1.0)
Quality.MASTERWORK:
draw_rect(square, Color(0.85, 0.55, 0.10), false, 1.0)
Quality.LEGENDARY:
draw_rect(square, Color(0.85, 0.10, 0.80), false, 2.0)
_:
pass # NORMAL — no extra border
# Stack count badge — bottom-right corner of the square, font_size 7.
if stack_size > 1:
var label := Strings.t(&"item.stack_count").format({"n": stack_size})
draw_string(
ThemeDB.fallback_font,
Vector2(half - 1, half - 1),
label,
HORIZONTAL_ALIGNMENT_RIGHT,
-1,
7,
Color(0.0, 0.0, 0.0, 0.6) # drop-shadow offset below
)
draw_string(
ThemeDB.fallback_font,
Vector2(half - 2, half - 2),
label,
HORIZONTAL_ALIGNMENT_RIGHT,
-1,
7,
Color(1.0, 1.0, 1.0, 1.0)
)
# ── helpers ───────────────────────────────────────────────────────────────────
func _tile_to_world(t: Vector2i) -> Vector2:
return Vector2(
t.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0,
t.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0
)