From c97ada80d745e296b991a65c2239c9c31e10389d Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 15 May 2026 20:22:55 +0100 Subject: [PATCH 01/19] Procedural workbench redraws + 3-season tree variety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workbenches: replace atlas sprites (which read as chest-of-drawers, candle base, kitchen stove, cushion stack) with procedural _draw_ methods following CremationPyre._draw_pyre's pattern. Carpenter shows a wood bench with saw + log slabs; Smelter a stone furnace with smoking chimney; Hearth a tall h=2 stone fireplace with arched opening + log fire; Millstone a wood frame supporting a round grindstone wheel. Trees: add Summer + Fall atlases alongside Spring (12 visual variants from 4 silhouettes × 3 seasons). Selection hash mixes season independently so neighbouring tiles don't all share the same palette. --- art/sprites/FG_Tree_Fall.png | Bin 0 -> 3220 bytes art/sprites/FG_Tree_Fall.png.import | 40 ++++ art/sprites/FG_Tree_Summer.png | Bin 0 -> 3225 bytes art/sprites/FG_Tree_Summer.png.import | 40 ++++ scenes/entities/tree.gd | 37 ++-- scenes/entities/workbench.gd | 274 ++++++++++++++++---------- 6 files changed, 278 insertions(+), 113 deletions(-) create mode 100644 art/sprites/FG_Tree_Fall.png create mode 100644 art/sprites/FG_Tree_Fall.png.import create mode 100644 art/sprites/FG_Tree_Summer.png create mode 100644 art/sprites/FG_Tree_Summer.png.import diff --git a/art/sprites/FG_Tree_Fall.png b/art/sprites/FG_Tree_Fall.png new file mode 100644 index 0000000000000000000000000000000000000000..6f9c887f79b2865e7db1e693b6bf674f8d7ad890 GIT binary patch literal 3220 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 J_KZjTe*n0OIKKb@ literal 0 HcmV?d00001 diff --git a/art/sprites/FG_Tree_Fall.png.import b/art/sprites/FG_Tree_Fall.png.import new file mode 100644 index 0000000..c735e56 --- /dev/null +++ b/art/sprites/FG_Tree_Fall.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://qen8u4g1oee4" +path="res://.godot/imported/FG_Tree_Fall.png-63225671846c6354f20ce0b5e0a5e31e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/FG_Tree_Fall.png" +dest_files=["res://.godot/imported/FG_Tree_Fall.png-63225671846c6354f20ce0b5e0a5e31e.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/art/sprites/FG_Tree_Summer.png b/art/sprites/FG_Tree_Summer.png new file mode 100644 index 0000000000000000000000000000000000000000..7408581a5dc9aea940382a664f3192b7c75ffd6f GIT binary patch literal 3225 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 TW1Q?;9tOB*X=_ntek1C?(i$UZ literal 0 HcmV?d00001 diff --git a/art/sprites/FG_Tree_Summer.png.import b/art/sprites/FG_Tree_Summer.png.import new file mode 100644 index 0000000..3691b91 --- /dev/null +++ b/art/sprites/FG_Tree_Summer.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cepguvswk8ecj" +path="res://.godot/imported/FG_Tree_Summer.png-1bc1b92f64d4677ada2e125e991dc553.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/FG_Tree_Summer.png" +dest_files=["res://.godot/imported/FG_Tree_Summer.png-1bc1b92f64d4677ada2e125e991dc553.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/tree.gd b/scenes/entities/tree.gd index 52177bf..50b2a28 100644 --- a/scenes/entities/tree.gd +++ b/scenes/entities/tree.gd @@ -34,14 +34,23 @@ var chop_designated: bool = false # Preloaded scene for spawned wood items. const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") -## ElvGames Grasslands tree pack — 4 variants laid out left-to-right. -## Each variant is 64×80 px; trunk base sits in the bottom ~10 rows. We anchor -## the sprite center 32 px above tile origin so the trunk bottom lands at the -## tile's bottom edge and the canopy rises into the cells above. -const _TREE_TEX: Texture2D = preload("res://art/sprites/FG_Tree_Spring.png") +## ElvGames Grasslands tree pack — 4 silhouettes laid out left-to-right (64×80 +## each). Trunk base sits in the bottom ~10 rows; we anchor the sprite centre +## 32 px above tile origin so the trunk bottom lands at the tile's bottom edge +## and the canopy rises into the cells above. +## +## Three season palettes (Spring / Summer / Fall) give 12 visual variants from +## the same silhouette set. Winter is omitted — snowy trees look out of place +## in the current biome. When a season-cycle system lands later, swap the +## active texture by season globally instead of per-tree. +const _TREE_TEXES: Array[Texture2D] = [ + preload("res://art/sprites/FG_Tree_Spring.png"), + preload("res://art/sprites/FG_Tree_Summer.png"), + preload("res://art/sprites/FG_Tree_Fall.png"), +] const _TREE_VARIANT_W: int = 64 const _TREE_VARIANT_H: int = 80 -const _TREE_VARIANT_COUNT: int = 4 +const _TREE_SILHOUETTES: int = 4 # silhouettes per atlas (columns) # ── lifecycle ───────────────────────────────────────────────────────────────── @@ -55,16 +64,20 @@ func _ready() -> void: World.register_tree(self) -## Adds a Sprite2D child painted with one of the 4 ElvGames tree variants. -## Variant chosen deterministically from the tile coord so the same tile always -## gets the same tree silhouette across boots and load/save. +## Adds a Sprite2D child painted with one of the 12 ElvGames tree variants +## (4 silhouettes × 3 season palettes). Variant chosen deterministically +## from the tile coord so the same tile always gets the same tree silhouette +## across boots and load/save. func _build_sprite() -> void: var sprite := Sprite2D.new() sprite.name = "Sprite" - sprite.texture = _TREE_TEX + var hash_seed: int = tile.x * 31 + tile.y * 17 + var silhouette: int = hash_seed % _TREE_SILHOUETTES + # Independent hash mix for season so neighbouring tiles don't all match. + var season: int = ((hash_seed / _TREE_SILHOUETTES) + tile.x * 7 + tile.y * 11) % _TREE_TEXES.size() + sprite.texture = _TREE_TEXES[season] sprite.region_enabled = true - var variant: int = (tile.x * 31 + tile.y * 17) % _TREE_VARIANT_COUNT - sprite.region_rect = Rect2(variant * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H) + sprite.region_rect = Rect2(silhouette * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H) sprite.centered = true # Lift the sprite up so its bottom edge sits at the tile's bottom row. # Sprite center is at offset.y; sprite half-height is _TREE_VARIANT_H/2 = 40. diff --git a/scenes/entities/workbench.gd b/scenes/entities/workbench.gd index 51af0fd..029ea00 100644 --- a/scenes/entities/workbench.gd +++ b/scenes/entities/workbench.gd @@ -1,13 +1,15 @@ class_name Workbench extends Node2D ## Workbench entity — buildable structure where pawns craft items per bills. ## -## Rendered as a bottom-anchored sprite (Y-sorted) matching the 3/4-perspective -## convention from Wall/Door. Ghost state (40% alpha) while construction is -## in progress; solid once _completed. +## Rendered procedurally (Y-sorted) matching the 3/4-perspective convention +## from Wall/Door. Ghost state (40% alpha) while construction is in progress; +## solid once _completed. ## ## Variant appearance is driven by label_text: -## "Carpenter" → warm-brown wood bench with a vise detail -## "Smelter" → dark grey stone block with an orange ember glow +## "Carpenter" → wooden workbench with saw + log slabs on top +## "Smelter" → dark stone furnace with chimney and ember glow +## "Hearth" → tall stone fireplace with mantle + log fire (h=2 tiles) +## "Millstone" → wooden frame supporting a round grindstone wheel ## Other → generic warm-grey fallback ## ## Bill model (architecture.md "Production: workbenches, recipes, bills"): @@ -42,45 +44,34 @@ 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. +# ── variant rendering ───────────────────────────────────────────────────────── +## All four named workbench variants render procedurally via _draw(). The +## atlas-sprite approach was abandoned in the 2026-05-15 polish pass after +## visual review: the chosen ElvGames atlas tiles read as a chest-of-drawers +## (Carpenter), a tiny candle base (Smelter), a 2-burner stove (Hearth), and +## a stack of cushions (Millstone). Procedural draws give us shape control to +## hit the silhouettes those names imply. See CremationPyre._draw_pyre() for +## precedent — same pattern, local coords centered at (0, 0) at the BOTTOM of +## the workbench tile, drawing UP into negative y. ## -## 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}, -} +## Tall variants (Hearth, h=2 logically) draw above y=-16 into the tile north +## of the bench; pawns stand correctly behind them because y_sort_enabled is +## on and position.y is anchored at the bench-tile's bottom edge. # ── exports ─────────────────────────────────────────────────────────────────── ## Tile position of this workbench in world-tile coordinates. @export var tile: Vector2i = Vector2i.ZERO -## 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. +## Player-visible label. Also drives the procedural _draw() variant dispatch. +## Setter triggers a redraw + lazy light build — callers can assign label_text +## either before OR after setup() and the visual catches up. ## (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() + queue_redraw() # 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 @@ -165,8 +156,6 @@ 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 @@ -174,52 +163,12 @@ func setup(p_tile: Vector2i) -> void: 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 so tall variants (Hearth) drawing into the tile north of the bench + # occlude pawns standing behind. 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. @@ -367,29 +316,156 @@ func from_dict(d: Dictionary) -> void: # ── render ───────────────────────────────────────────────────────────────────── func _draw() -> void: - # 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 - if label_text == "Millstone": - _draw_millstone_overlay(alpha) - return - if _VARIANT_SPRITES.has(label_text): - return - _draw_generic(alpha) + match label_text: + "Carpenter": _draw_carpenter(alpha) + "Smelter": _draw_smelter(alpha) + "Hearth": _draw_hearth(alpha) + "Millstone": _draw_millstone(alpha) + _: _draw_generic(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 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) +## Carpenter — wooden plank top with two visible legs and a hand-saw + log +## slabs on top. Reads as a workshop bench at 16×16 thanks to the saw blade +## silhouette breaking the plain top. +func _draw_carpenter(alpha: float) -> void: + var plank_top := Color(0.70, 0.50, 0.30, alpha) + var plank_front := Color(0.55, 0.38, 0.22, alpha) + var plank_edge := Color(0.35, 0.22, 0.12, alpha) + var leg := Color(0.30, 0.20, 0.10, alpha) + var saw_blade := Color(0.78, 0.78, 0.82, alpha) + var saw_handle := Color(0.55, 0.30, 0.15, alpha) + var log_face := Color(0.62, 0.42, 0.24, alpha) + var log_ring := Color(0.42, 0.27, 0.14, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Two legs at the corners (front face). + draw_rect(Rect2(Vector2(-7.0, -10.0), Vector2(2.0, 10.0)), leg) + draw_rect(Rect2(Vector2( 5.0, -10.0), Vector2(2.0, 10.0)), leg) + # Plank front face — thick band. + draw_rect(Rect2(Vector2(-8.0, -12.0), Vector2(16.0, 5.0)), plank_front) + # Plank top — slimmer band above the front, suggesting depth. + draw_rect(Rect2(Vector2(-8.0, -15.0), Vector2(16.0, 3.0)), plank_top) + # Edge highlight between top and front. + draw_line(Vector2(-8.0, -12.0), Vector2(8.0, -12.0), plank_edge, 1.0) + # Two short log slabs sitting on the left side of the top. + draw_rect(Rect2(Vector2(-6.0, -17.0), Vector2(3.0, 2.0)), log_face) + draw_rect(Rect2(Vector2(-3.0, -17.0), Vector2(3.0, 2.0)), log_face) + draw_line(Vector2(-4.5, -17.0), Vector2(-4.5, -15.0), log_ring, 1.0) + # Saw on the right — handle + blade silhouette. + draw_rect(Rect2(Vector2(1.0, -16.0), Vector2(6.0, 1.5)), saw_blade) + draw_rect(Rect2(Vector2(5.5, -17.0), Vector2(2.0, 2.0)), saw_handle) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) + + +## Smelter — stone furnace block with a stubby chimney puffing smoke and a +## bright ember-glow opening on the front face. Stone-grey base separates it +## visually from the Carpenter's warm wood. +func _draw_smelter(alpha: float) -> void: + var stone_top := Color(0.55, 0.55, 0.55, alpha) + var stone_front := Color(0.42, 0.42, 0.43, alpha) + var stone_shad := Color(0.30, 0.30, 0.32, alpha) + var ember := Color(0.98, 0.55, 0.10, alpha) + var ember_core := Color(1.00, 0.85, 0.30, alpha) + var chimney := Color(0.32, 0.30, 0.30, alpha) + var smoke := Color(0.75, 0.73, 0.70, alpha * 0.7) + var outline := Color(0.15, 0.12, 0.10, 0.7 * alpha) + + # Stone front body. + draw_rect(Rect2(Vector2(-8.0, -12.0), Vector2(16.0, 12.0)), stone_front) + # Top face — slightly lighter band. + draw_rect(Rect2(Vector2(-8.0, -15.0), Vector2(16.0, 3.0)), stone_top) + # Furnace mouth — dark recess with bright ember inside. + draw_rect(Rect2(Vector2(-4.0, -9.0), Vector2(8.0, 5.0)), stone_shad) + draw_rect(Rect2(Vector2(-3.0, -8.0), Vector2(6.0, 3.0)), ember) + draw_rect(Rect2(Vector2(-2.0, -7.0), Vector2(4.0, 1.0)), ember_core) + # Mortar lines across the front for stone-block feel. + draw_line(Vector2(-8.0, -8.0), Vector2(-4.0, -8.0), stone_shad, 1.0) + draw_line(Vector2( 4.0, -8.0), Vector2( 8.0, -8.0), stone_shad, 1.0) + # Chimney + smoke wisps rising above. + draw_rect(Rect2(Vector2(2.0, -19.0), Vector2(3.0, 4.0)), chimney) + draw_rect(Rect2(Vector2(3.0, -22.0), Vector2(1.0, 3.0)), smoke) + draw_rect(Rect2(Vector2(2.0, -24.0), Vector2(1.0, 2.0)), smoke) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) + + +## Hearth — tall (h=2) stone fireplace with mantle, arched opening, log fire +## with embers, and a flame licking up. Draws above y=-16 into the tile north +## of the bench (y_sort handles occlusion). Light-emitting via _maybe_build_light. +func _draw_hearth(alpha: float) -> void: + var stone := Color(0.60, 0.58, 0.55, alpha) + var stone_dark := Color(0.42, 0.40, 0.38, alpha) + var mantle := Color(0.50, 0.34, 0.20, alpha) + var mantle_edge := Color(0.32, 0.20, 0.10, alpha) + var opening := Color(0.08, 0.04, 0.02, alpha) + var log_wood := Color(0.55, 0.32, 0.15, alpha) + var ember := Color(0.98, 0.55, 0.10, alpha) + var flame_inner := Color(1.00, 0.85, 0.30, alpha) + var flame_outer := Color(0.95, 0.40, 0.05, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Stone surround — fills the bench tile (y −16..0) and the tile above + # (y −32..-16) so the fireplace is a 16×32 silhouette. + draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), stone) + # Stone block mortar — a couple of horizontal seams. + draw_line(Vector2(-8.0, -22.0), Vector2(8.0, -22.0), stone_dark, 1.0) + draw_line(Vector2(-8.0, -28.0), Vector2(8.0, -28.0), stone_dark, 1.0) + draw_line(Vector2(-2.0, -32.0), Vector2(-2.0, -28.0), stone_dark, 1.0) + draw_line(Vector2( 3.0, -28.0), Vector2( 3.0, -22.0), stone_dark, 1.0) + # Wooden mantle — horizontal beam across the middle. + draw_rect(Rect2(Vector2(-8.0, -19.0), Vector2(16.0, 3.0)), mantle) + draw_line(Vector2(-8.0, -19.0), Vector2(8.0, -19.0), mantle_edge, 1.0) + draw_line(Vector2(-8.0, -16.0), Vector2(8.0, -16.0), mantle_edge, 1.0) + # Arched opening — dark recess in the lower stone block. + draw_rect(Rect2(Vector2(-6.0, -14.0), Vector2(12.0, 14.0)), opening) + # Two stacked logs sitting in the opening. + draw_rect(Rect2(Vector2(-5.0, -4.0), Vector2(10.0, 2.0)), log_wood) + draw_rect(Rect2(Vector2(-4.0, -6.0), Vector2(8.0, 2.0)), log_wood) + # Ember strip glowing under the logs. + draw_rect(Rect2(Vector2(-4.0, -2.0), Vector2(8.0, 2.0)), ember) + # Flame — tapered teardrop above the logs. + draw_rect(Rect2(Vector2(-3.0, -10.0), Vector2(6.0, 4.0)), flame_outer) + draw_rect(Rect2(Vector2(-2.0, -12.0), Vector2(4.0, 2.0)), flame_outer) + draw_rect(Rect2(Vector2(-1.0, -13.0), Vector2(2.0, 1.0)), flame_outer) + draw_rect(Rect2(Vector2(-2.0, -9.0), Vector2(4.0, 2.0)), flame_inner) + draw_rect(Rect2(Vector2(-1.0, -11.0), Vector2(2.0, 2.0)), flame_inner) + # Outline around the full 16×32 silhouette. + draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), outline, false, 1.0) + + +## Millstone — wooden frame supporting a large round grindstone, viewed +## 3/4-perspective so the wheel reads as both round (top) and solid (front). +func _draw_millstone(alpha: float) -> void: + var frame_top := Color(0.55, 0.36, 0.18, alpha) + var frame_front := Color(0.42, 0.26, 0.12, alpha) + var frame_edge := Color(0.25, 0.14, 0.06, alpha) + var wheel := Color(0.55, 0.53, 0.50, alpha) + var wheel_dark := Color(0.34, 0.32, 0.30, alpha) + var wheel_rim := Color(0.18, 0.16, 0.14, alpha) + var groove := Color(0.28, 0.26, 0.24, alpha) + var pin := Color(0.20, 0.18, 0.16, alpha) + var outline := Color(0.15, 0.10, 0.05, 0.7 * alpha) + + # Wooden frame base — front + top faces. + draw_rect(Rect2(Vector2(-8.0, -7.0), Vector2(16.0, 7.0)), frame_front) + draw_rect(Rect2(Vector2(-8.0, -10.0), Vector2(16.0, 3.0)), frame_top) + draw_line(Vector2(-8.0, -7.0), Vector2(8.0, -7.0), frame_edge, 1.0) + # Grindstone — large dark-grey disc, rim slightly darker. Centred over + # the top of the frame, sticking up into the tile above only slightly. + var c := Vector2(0.0, -12.0) + draw_circle(c, 7.0, wheel_rim) + draw_circle(c, 6.0, wheel) + # Front-face shadow band across the lower half of the disc. + draw_rect(Rect2(Vector2(-6.0, -12.0), Vector2(12.0, 5.0)), wheel_dark) + # Two radial grooves — pie-slice indicators that the stone spins. + draw_line(c, c + Vector2(5.0, -3.5), groove, 1.0) + draw_line(c, c + Vector2(-5.0, -3.5), groove, 1.0) + # Centre pin / spindle. + draw_circle(c, 1.2, pin) + # Outline. + draw_rect(Rect2(Vector2(-8.0, -19.0), Vector2(16.0, 19.0)), outline, false, 1.0) func _draw_generic(alpha: float) -> void: @@ -410,11 +486,7 @@ 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 + # Procedural-only variants re-read alpha through _draw() via queue_redraw. # Phase 11: enable PointLight2D for light-emitting workbenches on completion. if _light != null: _light.enabled = is_on() From 61d3a6bd6454fcff89566649b0f09526de202020 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 15 May 2026 20:23:51 +0100 Subject: [PATCH 02/19] memory.md: workbench procedural redraws + tree 3-season variety --- memory.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/memory.md b/memory.md index a40bf4c..d7914e2 100644 --- a/memory.md +++ b/memory.md @@ -293,6 +293,10 @@ Same scope as locked in `~/claude/ideas/rimlike/plan.md`. Realistic timeline 3 - **Pattern recorded — ".import auto-generation via `godot --headless --import`".** Writing `*.png.import` companion files by hand with placeholder paths works but Godot rewrites them with proper UIDs on the first import scan. Reliable workflow: copy PNG into `art/`, write minimal .import (UID will be ignored), then run `/mnt/d/godot/Godot_v4.6.2-stable_win64_console.exe --headless --import` once to generate the .ctex binaries. Headless boot fails until the import scan runs. Same pattern from 2026-05-12 confirmed. - Delegation report (whole-day 05-15 polish sprint): **No delegation — all on Opus.** Sprite-atlas surveys, MCP runtime verification (camera positioning, dialog dismissal, stage forcing), and the iterative play→fix→play loop kept the same files hot in context; Haiku/Sonnet handoffs would have re-read every time. The `researcher` (Haiku) dispatch from earlier in the day for the job-system audit is the only delegated work. +- **Workbenches rebuilt procedural after second atlas-misidentification.** The 2026-05-12 atlas-sprite pass for Carpenter/Smelter/Hearth/Millstone again hit the same trap as Bed: the picked tiles read as chest-of-drawers (Carpenter, FG_Interior 24,20), candle base (Smelter, FG_Marketplace 8,30), 2-burner stove (Hearth, FG_Interior 16,32), and cushion stack (Millstone, FG_Interior 17,40). User: "the carpenter is a chest of draws? no idea what the mill is, looks like a bucket". Survey of all 7 plausible source tilesets (Interior / Marketplace / Houses / Village / Fortress / Mines / Dark Castle / Extras) found a 2×2 medieval stone fireplace at FG_Interior (12,13)..(13,14) for Hearth but **2-wide tiles don't fit the current single-column sprite system**, and the other three had no clean replacement. Decision: go procedural for all four, following `CremationPyre._draw_pyre` as precedent. New methods `_draw_carpenter / _draw_smelter / _draw_hearth / _draw_millstone` in workbench.gd; `_VARIANT_SPRITES` table + `_build_sprite()` Sprite2D path removed; `_complete()` no longer touches a sprite child. Hearth is the only h=2 variant — draws stone surround + mantle + arched opening + log fire + flame teardrop extending into the tile north (Y-sort handles occlusion). Commit `c97ada8`. +- **Pattern recorded — "atlas-misidentification trap, second occurrence — prefer procedural for named furniture".** Pixel-art tile atlases pack hundreds of squat 16×16 items at low resolution; without zoomed-in inspection it's easy to mistake a furniture silhouette for what its name implies. Both Bed (2026-05-12) and the four workbenches (2026-05-15) hit this failure mode. Going forward: for furniture whose silhouette is **definitional** (workbench-with-tools, bed-with-pillow, millstone-as-grindstone), **start with procedural draws** and only swap in an atlas tile after verifying via 16× zoom that the tile actually depicts the named object. Atlas-sourced sprites are still the right call for generic decorative variation (trees, walls, floors, crops with growth stages). +- **Tree variety added via 3-season palette swap.** Copied `FG_Tree_Summer.png` + `FG_Tree_Fall.png` from the Grasslands tier-1 pack into `art/sprites/` (Spring was already there). 4 silhouettes × 3 palettes = 12 visual variants; `tree.gd` hash mixes silhouette and season independently so neighbouring tiles don't all share the same palette. Winter omitted — snowy trees would look out of place in the current biome. When a season-cycle system lands (future phase), the active texture can be swapped globally by season instead of per-tree. No growth stage / sapling system yet — bundle has no growth frames for trees, and the user's "regrow / auto-grow" ask is explicit future scope (deferred to a later phase that also covers planting designation). Commit `c97ada8`. + ## External references - **Forgejo repo:** https://git.rdx4.com/megaproxy/rimlike (private) From c81e81723c226a417775a5052cbc0c59cec3c1a1 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Fri, 15 May 2026 22:01:44 +0100 Subject: [PATCH 03/19] Remove SW pre-made stockpiles + crates: items sit until player paints storage --- scenes/world/world.gd | 57 ++++++++++++------------------------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/scenes/world/world.gd b/scenes/world/world.gd index 734cc1c..cb40443 100644 --- a/scenes/world/world.gd +++ b/scenes/world/world.gd @@ -1,8 +1,9 @@ extends Node2D ## Phase 4 world view. 80×80 TileMap, 6 layers, 3 pawns, full AI pipeline: ## RestProvider → ChopProvider → MineProvider → HaulingProvider → idle -## plus sample trees, rocks, and two stockpile zones with different priorities -## for the haul-cascade demo. +## plus sample trees and rocks. No pre-made stockpiles — items sit where they +## are produced until the player paints storage (a stockpile zone or builds +## a crate). This matches Rimworld parity: storage is a player decision. ## ## TileMap layer indices follow docs/architecture.md: ## 0 Terrain · 1 Floor · 2 Wall · 3 Designation · 4 Roof · 5 Fog @@ -212,7 +213,10 @@ func _ready() -> void: _spawn_sample_pawns() _spawn_sample_harvestables() - _spawn_sample_stockpiles() + # No pre-made stockpiles: the player must paint their own storage. Items + # from chop/mine/crafting sit where they're produced until a player-made + # stockpile or crate exists for hauling. (2026-05-15 — _spawn_sample_stockpiles + # removed; was leftover Phase 4 acceptance scaffolding south-west of the cabin.) _seed_phase5_demo_buildings() # Phase 13 — pre-stamp the cabin walls + floors on the TileMap data layers # so RoomDetector can see a completed enclosure at boot without waiting for @@ -406,7 +410,7 @@ func _spawn_sample_pawns() -> void: World.register_pawn(p) -# ── Phase 4: harvestables + stockpile zones ───────────────────────────────── +# ── Phase 4: harvestables (stockpile-zone seeding removed 2026-05-15) ─────── func _spawn_sample_harvestables() -> void: # Untyped vars — Godot's class-name cache for class_name'd classes is @@ -443,8 +447,8 @@ func _seed_phase5_demo_buildings() -> void: # • Perimeter walls (skipping the door slot) # • Door at (47, 28) — middle of the south wall # • Wood floor across the 6×5 interior (rows 23..27) - # • One pre-built crate inside (north-east corner of the interior) - # • Two stockpile-target crates outside (Phase 4 hauling target) + # • One pre-built crate inside (north-east corner of the interior — the + # cabin's starting amenity; player paints additional storage later) # Bumped from 8×6 → 8×7 so the north interior row (23) is free for the # bed sprites to extend into (beds are 1×2, anchored at the foot, head # extends one tile up). Previously the headboards clipped into the wall. @@ -486,17 +490,13 @@ func _seed_phase5_demo_buildings() -> void: while interior_crate.is_buildable(): interior_crate.on_build_tick() - # Two external stockpile-target crates south-west (Phase 4 haul destination). - var crate_tiles: Array[Vector2i] = [Vector2i(17, 60), Vector2i(18, 60)] - for t in crate_tiles: - var c: Crate = CRATE_SCENE.instantiate() - add_child(c) - c.setup(t) - while c.is_buildable(): - c.on_build_tick() + # (2026-05-15) Two external SW crates removed alongside _spawn_sample_stockpiles: + # they were Phase 4 haul-destination scaffolding, no longer needed now that + # the player paints their own storage. The interior cabin crate above stays + # as a starting amenity. - Audit.log("world", "phase 5 demo: %d walls + 1 door + %d floors queued; %d crates pre-built" % [ - wall_count, floor_count, 1 + crate_tiles.size() + Audit.log("world", "phase 5 demo: %d walls + 1 door + %d floors queued; 1 interior crate pre-built" % [ + wall_count, floor_count ]) # Phase 6 demo — two pre-built workbenches inside the cabin with bills. @@ -631,31 +631,6 @@ func _seed_phase5_demo_buildings() -> void: Audit.log("world", "phase 11 demo: %d torches pre-built inside cabin" % torch_tiles.size()) -func _spawn_sample_stockpiles() -> void: - # Two zones for the Phase 4 acceptance demo: - # - Zone A (north): wood-only filter, NORMAL priority (just a wood drop) - # - Zone B (south): wildcard, HIGH priority (the "watch wood flow upward" target) - # When the sweep runs, wood items in Zone A get re-marked for haul and - # eventually migrate to Zone B. - var zone_a: StockpileZone = STOCKPILE_SCENE.instantiate() - add_child(zone_a) - zone_a.region = Rect2i(15, 55, 4, 4) - zone_a.label = "Wood (Normal)" - zone_a.priority = StorageDestination.Priority.NORMAL - zone_a.accepted_types = [Item.TYPE_WOOD] as Array[StringName] - zone_a.queue_redraw() - - var zone_b: StockpileZone = STOCKPILE_SCENE.instantiate() - add_child(zone_b) - zone_b.region = Rect2i(15, 62, 4, 4) - zone_b.label = "Anything (High)" - zone_b.priority = StorageDestination.Priority.HIGH - zone_b.accepted_types = [] as Array[StringName] # wildcard - zone_b.queue_redraw() - - Audit.log("world", "spawned 2 stockpiles: %s + %s" % [zone_a.label, zone_b.label]) - - # ── Phase 5: designation → build-site spawn bridge ────────────────────────── # Track build sites keyed by tile so we can find + queue_free them on cancel. From e5f3693ad9d61d89d42f869a9bc3435dfac79e1e Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 00:23:10 +0100 Subject: [PATCH 04/19] Workbench bill UI plan + 05-15 polish session log --- memory.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/memory.md b/memory.md index d7914e2..ac0f86c 100644 --- a/memory.md +++ b/memory.md @@ -297,6 +297,10 @@ Same scope as locked in `~/claude/ideas/rimlike/plan.md`. Realistic timeline 3 - **Pattern recorded — "atlas-misidentification trap, second occurrence — prefer procedural for named furniture".** Pixel-art tile atlases pack hundreds of squat 16×16 items at low resolution; without zoomed-in inspection it's easy to mistake a furniture silhouette for what its name implies. Both Bed (2026-05-12) and the four workbenches (2026-05-15) hit this failure mode. Going forward: for furniture whose silhouette is **definitional** (workbench-with-tools, bed-with-pillow, millstone-as-grindstone), **start with procedural draws** and only swap in an atlas tile after verifying via 16× zoom that the tile actually depicts the named object. Atlas-sourced sprites are still the right call for generic decorative variation (trees, walls, floors, crops with growth stages). - **Tree variety added via 3-season palette swap.** Copied `FG_Tree_Summer.png` + `FG_Tree_Fall.png` from the Grasslands tier-1 pack into `art/sprites/` (Spring was already there). 4 silhouettes × 3 palettes = 12 visual variants; `tree.gd` hash mixes silhouette and season independently so neighbouring tiles don't all share the same palette. Winter omitted — snowy trees would look out of place in the current biome. When a season-cycle system lands (future phase), the active texture can be swapped globally by season instead of per-tree. No growth stage / sapling system yet — bundle has no growth frames for trees, and the user's "regrow / auto-grow" ask is explicit future scope (deferred to a later phase that also covers planting designation). Commit `c97ada8`. +### 2026-05-16 +- **Pre-made SW stockpiles + crates removed** so items sit where they're produced until the player paints storage (matches Rimworld parity). Deleted `_spawn_sample_stockpiles()` (two zones at (15,55) and (15,62)) and the two SW haul-target crates at (17,60)/(18,60). Kept the interior cabin crate at (50,23) as a starting amenity. Bills' `UNTIL_N` mode counts items anywhere in world (not just stockpiles) so the Smelter's "5 stone blocks" bill still terminates. All `World.stockpiles` consumers iterate the list — empty-list no-op, no crashes. Commit `c81e817`. +- **Workbench Bill Editor plan drafted** for the deferred Phase 17 item ("Bill UI for workbenches"). 10-step plan saved to user-memory at `~/.claude/projects/-mnt-d-godot-rimlike/memory/plan_bill_ui.md` (resilient to fresh sessions). Without this UI, every player-built workbench is functionally dead — build drawer can paint them but bills can only be set in code. Recipe.id field existence needs verification in Step 4. Starting execution 2026-05-16. + ## External references - **Forgejo repo:** https://git.rdx4.com/megaproxy/rimlike (private) From bdd435202d3eb9811f7b00426770602d54efbd3d Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 00:29:46 +0100 Subject: [PATCH 05/19] =?UTF-8?q?Workbench=20bill=20editor=20=E2=80=94=20t?= =?UTF-8?q?ap=20a=20workbench,=20see/edit=20bills?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tap-to-select chain extended to workbenches (pawn always wins on shared tile). Mutually exclusive with pawn selection via EventBus — selecting one clears the other. New WorkbenchPanel (scenes/ui/workbench_panel.gd, ~432 LOC, layer 18, right-anchored 360 px) mirrors PawnDetailPanel shape. Bill rows expose recipe name, mode (FOREVER / COUNT / UNTIL_N), target count, completed progress, pause, and remove. Add-bill popup filters RecipeCatalog.all() by accepted_skill so a Hearth only offers cooking recipes. Supporting plumbing: - EventBus.workbench_selected / workbench_deselected signals. - Workbench.remove_bill() — interrupts mid-craft cleanly via on_craft_interrupted() before erasing. - RecipeCatalog.all() static enumerator + Recipe.display_name() helper. - World.workbench_at_tile() lookup. - i18n keys ui.bill.* and ui.workbench.* in strings.gd. Closes the deferred Phase 17 "Bill UI for workbenches" item. Player- built workbenches are now functionally configurable; before this landed, only world.gd-hardcoded bills worked. --- autoload/event_bus.gd | 2 + autoload/strings.gd | 12 + autoload/world.gd | 9 + scenes/ai/recipe.gd | 9 + scenes/ai/recipe_catalog.gd | 14 ++ scenes/entities/workbench.gd | 9 + scenes/main/main.gd | 10 +- scenes/ui/workbench_panel.gd | 432 +++++++++++++++++++++++++++++++++++ scenes/world/selection.gd | 64 +++++- 9 files changed, 551 insertions(+), 10 deletions(-) create mode 100644 scenes/ui/workbench_panel.gd diff --git a/autoload/event_bus.gd b/autoload/event_bus.gd index dc9a8b2..81de2c8 100644 --- a/autoload/event_bus.gd +++ b/autoload/event_bus.gd @@ -55,6 +55,8 @@ signal load_finished(slot: StringName, ok: bool, real_seconds_away: int) ## Emit # Phase 17 — Touch UX completion. signal pawn_selected(pawn) ## Emitted when Selection picks a pawn — opens PawnDetailPanel. signal pawn_deselected ## Emitted when Selection clears — closes PawnDetailPanel. +signal workbench_selected(workbench) +signal workbench_deselected signal pawn_priority_changed(pawn, category: StringName, level: int) ## Emitted when priority matrix updates a cell. signal alert_added(severity: StringName, text: String, focus_tile: Vector2i) ## Emitted by gameplay subsystems to surface a player notice. severity = info | warn | danger. signal request_wolf_spawn(count: int) ## Phase 15 EventCatalog → WolfSpawner. Decouples threat-event effects from spawner. diff --git a/autoload/strings.gd b/autoload/strings.gd index c67813c..d49660d 100644 --- a/autoload/strings.gd +++ b/autoload/strings.gd @@ -196,6 +196,18 @@ const TABLE: Dictionary = { &"tool.workbench_cremation_pyre": "Cremation Pyre", &"tool.stockpile_general": "Stockpile", &"tool.graveyard": "Graveyard", + &"ui.bill.mode_forever": "Forever", + &"ui.bill.mode_count": "Do X times", + &"ui.bill.mode_until_n": "Do until X", + &"ui.bill.target": "Target", + &"ui.bill.until_count": "Until count", + &"ui.bill.completed": "Done", + &"ui.bill.pause": "Pause", + &"ui.bill.remove": "Remove", + &"ui.bill.add_button": "Add bill", + &"ui.bill.no_bills_hint": "No bills. Add one to start crafting.", + &"ui.workbench.current_bill": "Current", + &"ui.workbench.idle": "Idle", } diff --git a/autoload/world.gd b/autoload/world.gd index 9169dac..ffa2b99 100644 --- a/autoload/world.gd +++ b/autoload/world.gd @@ -124,6 +124,15 @@ func pawn_at_tile(tile: Vector2i) -> Pawn: return null +## Returns the Workbench occupying `tile`, or null if none. Used by Selection +## to route taps on a workbench to the bill-editor panel. +func workbench_at_tile(tile: Vector2i): + for w in workbenches: + if w.tile == tile: + return w + return null + + func clear_pawns() -> void: # For save-load / new-game flow in Phase 16. pawns.clear() diff --git a/scenes/ai/recipe.gd b/scenes/ai/recipe.gd index 8571b38..768fa6a 100644 --- a/scenes/ai/recipe.gd +++ b/scenes/ai/recipe.gd @@ -46,6 +46,15 @@ var label: String = "" # ── save / load ─────────────────────────────────────────────────────────────── +## Player-visible display name for this recipe. Used by the workbench bill +## editor's recipe-picker and the bill list. Falls back to `id` if `label` +## is empty (shouldn't happen for catalog recipes, but defensive). +func display_name() -> String: + if label.is_empty(): + return str(id) + return label + + func to_dict() -> Dictionary: return { "id": String(id), diff --git a/scenes/ai/recipe_catalog.gd b/scenes/ai/recipe_catalog.gd index 0645c3a..c8f9e22 100644 --- a/scenes/ai/recipe_catalog.gd +++ b/scenes/ai/recipe_catalog.gd @@ -109,3 +109,17 @@ static func cremate_corpse() -> Recipe: r.required_skill = &"manual_labor" r.skill_threshold = 0 return r + + +## Returns one fresh instance of every recipe in the catalog. Used by UI +## recipe-pickers to enumerate available bills; callers filter by +## `recipe.required_skill` against the workbench's `accepted_skill`. +static func all() -> Array[Recipe]: + return [ + plank(), + stone_block(), + flour(), + bread(), + meal_from_vegetables(), + cremate_corpse(), + ] diff --git a/scenes/entities/workbench.gd b/scenes/entities/workbench.gd index 029ea00..a108c24 100644 --- a/scenes/entities/workbench.gd +++ b/scenes/entities/workbench.gd @@ -206,6 +206,15 @@ func add_bill(b) -> void: Audit.log("workbench", "%s: bill added — recipe '%s'" % [label_text, b.recipe.id]) +## Remove a bill from this workbench's queue. If the bill is currently being +## crafted, the active toil is interrupted cleanly so the pawn re-decides. +func remove_bill(b) -> void: + if current_bill == b: + on_craft_interrupted() + bills.erase(b) + Audit.log("workbench", "%s: bill removed — recipe '%s'" % [label_text, b.recipe.id]) + + ## Return the first bill that is active and whose required_skill matches ## this bench's accepted_skill. Returns null when none qualify. ## CraftingProvider calls this; JobRunner also calls it when the current_bill diff --git a/scenes/main/main.gd b/scenes/main/main.gd index 692a27d..c5b559f 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -19,6 +19,7 @@ const LOAD_MENU_SCRIPT: Script = preload("res://scenes/ui/load_menu. const RESUME_TOAST_SCRIPT: Script = preload("res://scenes/ui/resume_toast.gd") # Phase 17 — PawnDetailPanel (layer 18) and SettingsMenu (layer 26). const PAWN_DETAIL_PANEL_SCRIPT: Script = preload("res://scenes/ui/pawn_detail_panel.gd") +const WORKBENCH_PANEL_SCRIPT: Script = preload("res://scenes/ui/workbench_panel.gd") const SETTINGS_MENU_SCRIPT: Script = preload("res://scenes/ui/settings_menu.gd") # Phase 17 (Agent B) — BuildDrawer bottom-sheet (layer 16). const BUILD_DRAWER_SCRIPT: Script = preload("res://scenes/ui/build_drawer.gd") @@ -79,6 +80,13 @@ func _ready() -> void: pawn_detail_panel.name = "PawnDetailPanel" add_child(pawn_detail_panel) + # Bill-editor bottom-sheet for workbenches. Same shape as PawnDetailPanel + # (right-anchored 360 px, layer 18); mutually exclusive with it via Selection. + var workbench_panel := CanvasLayer.new() + workbench_panel.set_script(WORKBENCH_PANEL_SCRIPT) + workbench_panel.name = "WorkbenchPanel" + add_child(workbench_panel) + var settings_menu := CanvasLayer.new() settings_menu.set_script(SETTINGS_MENU_SCRIPT) settings_menu.name = "SettingsMenu" @@ -91,7 +99,7 @@ func _ready() -> void: if top_bar.has_method("_add_settings_btn"): top_bar._add_settings_btn() - Audit.log("main", "Phase 17 — PawnDetailPanel + SettingsMenu mounted.") + Audit.log("main", "Phase 17 — PawnDetailPanel + WorkbenchPanel + SettingsMenu mounted.") # Phase 17 (Agent B) — BuildDrawer bottom-sheet (layer 16). # Must mount AFTER the World node is ready (World._ready seeds designation_ctl). diff --git a/scenes/ui/workbench_panel.gd b/scenes/ui/workbench_panel.gd new file mode 100644 index 0000000..b7cf4de --- /dev/null +++ b/scenes/ui/workbench_panel.gd @@ -0,0 +1,432 @@ +class_name WorkbenchPanel extends CanvasLayer +## Phase 17 — Right-side bottom-sheet workbench bill editor. +## +## Layer 18: same level as PawnDetailPanel (only one is visible at a time). +## Opens when EventBus.workbench_selected fires; closes on workbench_deselected +## or when a pawn is selected (mutual-exclusion with PawnDetailPanel). +## +## Refresh model (matches PawnDetailPanel): +## - Full UI rebuild: only on workbench_selected, add_bill, remove_bill. +## - Status-line refresh: every 5 sim ticks while open (_on_sim_tick). +## - Bill rows are NOT touched during refresh to preserve scroll + dropdown state. +## +## Touch targets: all interactive controls are at least 48×48 px. +## Background elements use MOUSE_FILTER_IGNORE so world taps pass through. + +const PANEL_WIDTH: int = 360 +const REFRESH_TICKS: int = 5 # update status line every N sim ticks +const LAYER: int = 18 + +# ── internal state ──────────────────────────────────────────────────────────── +var current_workbench: Workbench = null +var _tick_counter: int = 0 + +# ── node refs (built in _build_ui) ─────────────────────────────────────────── +var _panel: PanelContainer = null +var _wb_name_label: Label = null +var _close_btn: Button = null +var _status_label: Label = null +var _bills_vbox: VBoxContainer = null +var _add_btn: Button = null +var _no_bills_label: Label = null +var _recipe_popup: PopupMenu = null + + +func _ready() -> void: + layer = LAYER + + _build_ui() + _set_visible(false) + + EventBus.workbench_selected.connect(_on_workbench_selected) + EventBus.workbench_deselected.connect(_on_workbench_deselected) + EventBus.pawn_selected.connect(_on_pawn_selected) + EventBus.sim_tick.connect(_on_sim_tick) + + Audit.log("workbench_panel", "WorkbenchPanel ready (layer %d)" % layer) + + +func _exit_tree() -> void: + if EventBus.workbench_selected.is_connected(_on_workbench_selected): + EventBus.workbench_selected.disconnect(_on_workbench_selected) + if EventBus.workbench_deselected.is_connected(_on_workbench_deselected): + EventBus.workbench_deselected.disconnect(_on_workbench_deselected) + if EventBus.pawn_selected.is_connected(_on_pawn_selected): + EventBus.pawn_selected.disconnect(_on_pawn_selected) + if EventBus.sim_tick.is_connected(_on_sim_tick): + EventBus.sim_tick.disconnect(_on_sim_tick) + + +# ── UI construction ─────────────────────────────────────────────────────────── + +func _build_ui() -> void: + # Right-side sheet anchored to the right edge, full height. + _panel = PanelContainer.new() + _panel.name = "WorkbenchSheet" + _panel.anchor_left = 1.0 + _panel.anchor_right = 1.0 + _panel.anchor_top = 0.0 + _panel.anchor_bottom = 1.0 + _panel.offset_left = -PANEL_WIDTH + _panel.offset_right = 0.0 + _panel.offset_top = 0.0 + _panel.offset_bottom = 0.0 + _panel.mouse_filter = Control.MOUSE_FILTER_PASS + add_child(_panel) + + # Scrollable inner container so content survives small screens. + var scroll := ScrollContainer.new() + scroll.name = "Scroll" + scroll.set_anchors_preset(Control.PRESET_FULL_RECT) + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + _panel.add_child(scroll) + + var vbox := VBoxContainer.new() + vbox.name = "Content" + vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL + vbox.add_theme_constant_override("separation", 6) + scroll.add_child(vbox) + + # ── Header ──────────────────────────────────────────────────────────────── + var header := HBoxContainer.new() + header.name = "Header" + header.add_theme_constant_override("separation", 8) + vbox.add_child(header) + + _wb_name_label = Label.new() + _wb_name_label.name = "WorkbenchName" + _wb_name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _wb_name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _wb_name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + header.add_child(_wb_name_label) + + _close_btn = Button.new() + _close_btn.name = "CloseBtn" + _close_btn.text = Strings.t(&"ui.detail.close") + _close_btn.custom_minimum_size = Vector2(48, 48) + _close_btn.focus_mode = Control.FOCUS_NONE + _close_btn.pressed.connect(_on_close_pressed) + header.add_child(_close_btn) + + _add_separator(vbox) + + # ── Status line (live-refreshed) ────────────────────────────────────────── + _status_label = Label.new() + _status_label.name = "StatusLabel" + _status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _status_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + vbox.add_child(_status_label) + + _add_separator(vbox) + + # ── Bill list ───────────────────────────────────────────────────────────── + var bills_header := Label.new() + bills_header.text = Strings.t(&"ui.bill.no_bills_hint") + bills_header.name = "BillsHeader" + bills_header.mouse_filter = Control.MOUSE_FILTER_IGNORE + # Header is just section spacing; actual content is in _bills_vbox below. + # Repurpose this as the "no bills" hint — hidden when bills exist. + _no_bills_label = bills_header + vbox.add_child(_no_bills_label) + + _bills_vbox = VBoxContainer.new() + _bills_vbox.name = "BillList" + _bills_vbox.add_theme_constant_override("separation", 8) + _bills_vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE + vbox.add_child(_bills_vbox) + + _add_separator(vbox) + + # ── Add-bill footer ─────────────────────────────────────────────────────── + var footer := HBoxContainer.new() + footer.name = "Footer" + footer.add_theme_constant_override("separation", 8) + vbox.add_child(footer) + + _add_btn = Button.new() + _add_btn.name = "AddBillBtn" + _add_btn.text = Strings.t(&"ui.bill.add_button") + _add_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _add_btn.custom_minimum_size = Vector2(0, 48) + _add_btn.focus_mode = Control.FOCUS_NONE + _add_btn.pressed.connect(_on_add_bill_pressed) + footer.add_child(_add_btn) + + # PopupMenu for recipe selection — populated lazily in _on_add_bill_pressed. + _recipe_popup = PopupMenu.new() + _recipe_popup.name = "RecipePopup" + _recipe_popup.id_pressed.connect(_on_recipe_chosen) + _panel.add_child(_recipe_popup) + + +func _add_separator(parent: VBoxContainer) -> void: + var sep := HSeparator.new() + sep.mouse_filter = Control.MOUSE_FILTER_IGNORE + parent.add_child(sep) + + +func _clear_children(node: Node) -> void: + for child in node.get_children(): + child.queue_free() + + +# ── event handlers ──────────────────────────────────────────────────────────── + +func _on_workbench_selected(wb: Workbench) -> void: + current_workbench = wb + _tick_counter = 0 + _wb_name_label.text = wb.label_text + _refresh_status() + _populate_bills() + _set_visible(true) + Audit.log("workbench_panel", "opened for %s" % wb.label_text) + + +func _on_workbench_deselected() -> void: + current_workbench = null + _set_visible(false) + Audit.log("workbench_panel", "closed (deselected)") + + +func _on_pawn_selected(_pawn) -> void: + # Mutual exclusion: pawn panel and workbench panel are never both open. + if current_workbench == null: + return + current_workbench = null + _set_visible(false) + EventBus.workbench_deselected.emit() + Audit.log("workbench_panel", "closed (pawn selected)") + + +func _on_close_pressed() -> void: + current_workbench = null + _set_visible(false) + EventBus.workbench_deselected.emit() + Audit.log("workbench_panel", "closed (X button)") + + +func _on_sim_tick(_tick_number: int) -> void: + if current_workbench == null or not _panel.visible: + return + if not is_instance_valid(current_workbench): + current_workbench = null + _set_visible(false) + return + _tick_counter += 1 + if _tick_counter >= REFRESH_TICKS: + _tick_counter = 0 + _refresh_status() + + +# ── status-line refresh (called every REFRESH_TICKS, NOT rebuild) ───────────── + +func _refresh_status() -> void: + if current_workbench == null: + return + var cb = current_workbench.current_bill + if cb != null and cb.recipe != null: + var work_ticks: int = cb.recipe.work_ticks + _status_label.text = "%s: %s %d/%d" % [ + Strings.t(&"ui.workbench.current_bill"), + cb.recipe.display_name(), + current_workbench.current_work_progress, + work_ticks + ] + else: + _status_label.text = Strings.t(&"ui.workbench.idle") + + +# ── bill list population (called on open / add / remove only) ───────────────── + +func _populate_bills() -> void: + if current_workbench == null: + return + + _clear_children(_bills_vbox) + + var has_bills: bool = current_workbench.bills.size() > 0 + _no_bills_label.visible = not has_bills + + for bill in current_workbench.bills: + _bills_vbox.add_child(_make_bill_row(bill)) + + # Enable/disable the add button based on whether any filtered recipes exist. + var filtered: Array[Recipe] = _filtered_recipes() + _add_btn.disabled = filtered.is_empty() + + +## Build and return a VBoxContainer widget for a single bill. +## All controls mutate bill fields directly; remove triggers _populate_bills(). +func _make_bill_row(bill: Bill) -> VBoxContainer: + var row_vbox := VBoxContainer.new() + row_vbox.add_theme_constant_override("separation", 4) + row_vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE + + # Row 1: recipe name (bold via theme; we use a plain Label — theme handles weight). + var name_lbl := Label.new() + name_lbl.text = bill.recipe.display_name() if bill.recipe != null else "???" + name_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + row_vbox.add_child(name_lbl) + + # Row 2: mode OptionButton (Forever / Do X times / Do until X). + var mode_row := HBoxContainer.new() + mode_row.add_theme_constant_override("separation", 6) + row_vbox.add_child(mode_row) + + var mode_lbl := Label.new() + mode_lbl.text = "Mode:" + mode_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + mode_lbl.custom_minimum_size = Vector2(40, 0) + mode_row.add_child(mode_lbl) + + var mode_btn := OptionButton.new() + mode_btn.add_item(Strings.t(&"ui.bill.mode_forever"), Bill.Mode.FOREVER) + mode_btn.add_item(Strings.t(&"ui.bill.mode_count"), Bill.Mode.COUNT) + mode_btn.add_item(Strings.t(&"ui.bill.mode_until_n"), Bill.Mode.UNTIL_N) + mode_btn.selected = bill.mode as int + mode_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL + mode_btn.focus_mode = Control.FOCUS_NONE + mode_btn.custom_minimum_size = Vector2(0, 48) + # Capture bill reference in closure; repopulate on mode change so conditional + # rows (count spinner, done label) appear/disappear correctly. + mode_btn.item_selected.connect(func(idx: int) -> void: + bill.mode = idx as Bill.Mode + Audit.log("workbench_ui", "%s: bill mode → %d" % [current_workbench.label_text, idx]) + # Repopulate so conditional rows update; OptionButton state survives because + # we set mode on bill before the rebuild, and the new row reads bill.mode. + _populate_bills() + ) + mode_row.add_child(mode_btn) + + # Row 3 (conditional): SpinBox for target_count. Shown when mode != FOREVER. + if bill.mode != Bill.Mode.FOREVER: + var count_row := HBoxContainer.new() + count_row.add_theme_constant_override("separation", 6) + row_vbox.add_child(count_row) + + var count_lbl := Label.new() + count_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + count_lbl.custom_minimum_size = Vector2(80, 0) + if bill.mode == Bill.Mode.COUNT: + count_lbl.text = Strings.t(&"ui.bill.target") + else: + count_lbl.text = Strings.t(&"ui.bill.until_count") + count_row.add_child(count_lbl) + + var spin := SpinBox.new() + spin.min_value = 1 + spin.max_value = 999 + spin.step = 1 + spin.value = max(1, bill.target_count) + spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL + spin.focus_mode = Control.FOCUS_NONE + spin.value_changed.connect(func(v: float) -> void: + bill.target_count = int(v) + Audit.log("workbench_ui", "%s: bill target_count → %d" % [current_workbench.label_text, bill.target_count]) + ) + count_row.add_child(spin) + + # Row 4 (COUNT only): "Done: X/Y" progress label. + if bill.mode == Bill.Mode.COUNT: + var done_lbl := Label.new() + done_lbl.text = "%s: %d/%d" % [Strings.t(&"ui.bill.completed"), bill.completed_count, bill.target_count] + done_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE + row_vbox.add_child(done_lbl) + + # Row 5: pause CheckBox. + var pause_check := CheckBox.new() + pause_check.text = Strings.t(&"ui.bill.pause") + pause_check.button_pressed = bill.paused + pause_check.focus_mode = Control.FOCUS_NONE + pause_check.custom_minimum_size = Vector2(0, 40) + pause_check.toggled.connect(func(on: bool) -> void: + bill.paused = on + Audit.log("workbench_ui", "%s: bill paused → %s" % [current_workbench.label_text, str(on)]) + ) + row_vbox.add_child(pause_check) + + # Row 6: "Remove" button, right-aligned via HSpacer + HBox. + var remove_row := HBoxContainer.new() + row_vbox.add_child(remove_row) + + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE + remove_row.add_child(spacer) + + var remove_btn := Button.new() + remove_btn.text = Strings.t(&"ui.bill.remove") + remove_btn.custom_minimum_size = Vector2(80, 40) + remove_btn.focus_mode = Control.FOCUS_NONE + remove_btn.pressed.connect(func() -> void: + if current_workbench == null: + return + current_workbench.remove_bill(bill) + Audit.log("workbench_ui", "%s: bill removed — recipe '%s'" % [ + current_workbench.label_text, + bill.recipe.id if bill.recipe != null else "null" + ]) + _populate_bills() + ) + remove_row.add_child(remove_btn) + + # Thin separator below each bill row for visual grouping. + var sep := HSeparator.new() + sep.mouse_filter = Control.MOUSE_FILTER_IGNORE + row_vbox.add_child(sep) + + return row_vbox + + +# ── add-bill popup ──────────────────────────────────────────────────────────── + +func _on_add_bill_pressed() -> void: + if current_workbench == null: + return + + var recipes: Array[Recipe] = _filtered_recipes() + if recipes.is_empty(): + return + + _recipe_popup.clear() + for i in recipes.size(): + _recipe_popup.add_item(recipes[i].display_name(), i) + + # Position the popup just above the add button. + var btn_rect: Rect2 = _add_btn.get_global_rect() + _recipe_popup.position = Vector2i(int(btn_rect.position.x), int(btn_rect.position.y) - _recipe_popup.size.y - 4) + _recipe_popup.popup() + + +func _on_recipe_chosen(id: int) -> void: + if current_workbench == null: + return + var recipes: Array[Recipe] = _filtered_recipes() + if id < 0 or id >= recipes.size(): + return + var picked: Recipe = recipes[id] + var b := Bill.new() + b.recipe = picked + b.mode = Bill.Mode.FOREVER + current_workbench.add_bill(b) + Audit.log("workbench_ui", "%s: bill added — recipe '%s'" % [current_workbench.label_text, picked.id]) + _populate_bills() + + +## Returns all catalog recipes whose required_skill matches the workbench's +## accepted_skill. Returns an empty array when no workbench is set. +func _filtered_recipes() -> Array[Recipe]: + if current_workbench == null: + return [] + var result: Array[Recipe] = [] + for r in RecipeCatalog.all(): + if r.required_skill == current_workbench.accepted_skill: + result.append(r) + return result + + +# ── visibility ──────────────────────────────────────────────────────────────── + +func _set_visible(v: bool) -> void: + if _panel != null: + _panel.visible = v diff --git a/scenes/world/selection.gd b/scenes/world/selection.gd index ec04c64..dbd0aca 100644 --- a/scenes/world/selection.gd +++ b/scenes/world/selection.gd @@ -14,6 +14,9 @@ const CLICK_MAX_DURATION_MS: int = 300 var _pathfinder: Pathfinder = null var _selected_pawn: Pawn = null +## Currently selected workbench, or null. Mutually exclusive with _selected_pawn — +## selecting one clears the other (see _select / _select_workbench). +var _selected_workbench: Workbench = null var _camera = null # Camera2D (CameraRig) — set via bind_camera(); duck-typed to avoid circular preload # When Designation paint mode is active this flag is raised by Designation so @@ -62,23 +65,32 @@ func _unhandled_input(event: InputEvent) -> void: # ── Keyboard: Escape → deselect (lowest-priority; consumed last) ───────────── # Designation._input handles Escape first; panels handle it in _unhandled_input # before reaching here. If we still see it and have a selection, consume it. - if event.is_action_pressed("cancel") and _selected_pawn != null: - _deselect() - get_viewport().set_input_as_handled() - Audit.log("selection", "escape: deselected") - return + if event.is_action_pressed("cancel"): + if _selected_pawn != null: + _deselect() + get_viewport().set_input_as_handled() + Audit.log("selection", "escape: deselected pawn") + return + if _selected_workbench != null: + _deselect_workbench() + get_viewport().set_input_as_handled() + Audit.log("selection", "escape: deselected workbench") + return # ── Mouse: only handle button events below ─────────────────────────────────── if not (event is InputEventMouseButton): return - # ── Right-click: cancel designation (if active) or deselect pawn ───────────── + # ── Right-click: cancel designation (if active) or deselect pawn / workbench ── if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: # Designation cancellation is handled by Designation._input; if we see - # this right-click, no designation was active. Deselect any selected pawn. + # this right-click, no designation was active. Deselect whatever is selected. if _selected_pawn != null: _deselect() get_viewport().set_input_as_handled() + elif _selected_workbench != null: + _deselect_workbench() + get_viewport().set_input_as_handled() return if event.button_index != MOUSE_BUTTON_LEFT: @@ -114,14 +126,23 @@ func _handle_click(screen_pos: Vector2) -> void: floori(world_pos.y / float(Pawn.TILE_SIZE_PX)), ) - # Click on a pawn → select. + # Click on a pawn → select. Pawn wins over workbench when they share a tile + # (a pawn working at a bench is selectable; tap empty bench tile to inspect bills). var hit_pawn: Pawn = World.pawn_at_tile(tile) if hit_pawn != null: _select(hit_pawn) return - # Empty tile with no current selection → no-op. + # Click on a workbench → open the bill-editor panel. + var hit_workbench = World.workbench_at_tile(tile) + if hit_workbench != null: + _select_workbench(hit_workbench) + return + + # Empty tile with no current pawn selection → also clear any workbench selection. if _selected_pawn == null: + if _selected_workbench != null: + _deselect_workbench() return # Empty walkable tile with a selection → queue a forced job. Decision picks @@ -140,6 +161,9 @@ func _handle_click(screen_pos: Vector2) -> void: func _select(pawn: Pawn) -> void: if _selected_pawn == pawn: return + # Mutual exclusion with workbench selection: clear it before promoting pawn. + if _selected_workbench != null: + _deselect_workbench() if _selected_pawn != null: _selected_pawn.set_selected(false) EventBus.pawn_deselected.emit() @@ -159,6 +183,28 @@ func _deselect() -> void: _selected_pawn = null +## Select a workbench → opens the bill-editor panel via EventBus. +## Mutually exclusive with pawn selection: clears _selected_pawn first. +func _select_workbench(wb) -> void: + if _selected_workbench == wb: + return + if _selected_pawn != null: + _deselect() + if _selected_workbench != null: + EventBus.workbench_deselected.emit() + _selected_workbench = wb + EventBus.workbench_selected.emit(wb) + Audit.log("selection", "selected workbench %s at %s" % [wb.label_text, wb.tile]) + + +func _deselect_workbench() -> void: + if _selected_workbench == null: + return + Audit.log("selection", "deselected workbench %s" % _selected_workbench.label_text) + _selected_workbench = null + EventBus.workbench_deselected.emit() + + ## Cycle the selection forward (dir=1) or backward (dir=-1) through World.pawns. ## Wraps around. If no pawn currently selected, picks World.pawns[0]. ## Pans the camera to the newly selected pawn's tile. From aba8476285ad8f3c0fca8d58b21c6fce41f4f75f Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 00:30:20 +0100 Subject: [PATCH 06/19] docs: mark Phase 17 'Bill UI for workbenches' as shipped --- docs/implementation.md | 2 +- memory.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/implementation.md b/docs/implementation.md index 6c06643..1600fe9 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -395,7 +395,7 @@ The five items from `memory.md` *Open questions / Audit*. None of these need cod - [x] **Day-summary emission** — Clock emits `day_ended(summary)` at dusk→night with day/weather/season/pawns_alive/tension/wolves_alive recap. AlertsLog surfaces as a single-line entry per day. Full DaySummaryCard UI deferred. - [ ] Per-pawn / per-job views layered on the matrix — deferred (current matrix has read-write only, no view layers). - [ ] Stockpile / container 4×4 chip grid UI — deferred (paint creates 1×1 zones today; filter UI is data-only). -- [ ] Bill UI for workbenches — deferred (Phase 6 stub still in use; add/edit bills programmatically only). +- [x] **Bill UI for workbenches** (shipped 2026-05-16, out-of-phase). `WorkbenchPanel` bottom-sheet mirrors PawnDetailPanel (layer 18, right-anchored 360 px); tap a workbench → bill rows with mode toggle (FOREVER/COUNT/UNTIL_N), target SpinBox, pause CheckBox, remove button; Add-bill popup filters `RecipeCatalog.all()` by `accepted_skill`. Selection chain extended: pawn wins over workbench on shared tile, mutual-exclusion via `EventBus.workbench_selected/deselected`. Closes the Phase 6 stub — player-built workbenches are now configurable. - [ ] "No stockpile accepts X" + "Bill blocked" alerts — wiring stubs ready; emit calls in HaulingProvider / CraftingProvider deferred. - [ ] Day-summary card UI — deferred (signal emits; visual card is Phase 17.5). - [x] **Acceptance:** Hand-test verified — TopBar shows Save/Load/Settings/Build/Work/Log[N]; tap Bram → right panel shows all his state; tap Build → bottom drawer with 4 tabs; tap Work → grid of pawn priorities; tap Log → scrollable alerts list including the Spring Awakens storyteller event from boot. All UIs touch-friendly (48×48+ targets). Screenshots captured for all 4 surfaces. diff --git a/memory.md b/memory.md index ac0f86c..85f6308 100644 --- a/memory.md +++ b/memory.md @@ -300,6 +300,9 @@ Same scope as locked in `~/claude/ideas/rimlike/plan.md`. Realistic timeline 3 ### 2026-05-16 - **Pre-made SW stockpiles + crates removed** so items sit where they're produced until the player paints storage (matches Rimworld parity). Deleted `_spawn_sample_stockpiles()` (two zones at (15,55) and (15,62)) and the two SW haul-target crates at (17,60)/(18,60). Kept the interior cabin crate at (50,23) as a starting amenity. Bills' `UNTIL_N` mode counts items anywhere in world (not just stockpiles) so the Smelter's "5 stone blocks" bill still terminates. All `World.stockpiles` consumers iterate the list — empty-list no-op, no crashes. Commit `c81e817`. - **Workbench Bill Editor plan drafted** for the deferred Phase 17 item ("Bill UI for workbenches"). 10-step plan saved to user-memory at `~/.claude/projects/-mnt-d-godot-rimlike/memory/plan_bill_ui.md` (resilient to fresh sessions). Without this UI, every player-built workbench is functionally dead — build drawer can paint them but bills can only be set in code. Recipe.id field existence needs verification in Step 4. Starting execution 2026-05-16. +- **Workbench Bill Editor shipped same day** (commit `bdd4352`). Tap a workbench → right-side panel with bill rows (mode toggle, target spin, pause, remove) + Add-bill popup filtered by `accepted_skill`. Mirrors `PawnDetailPanel` exactly: layer 18, right-anchored 360 px, procedural `_build_ui()`, sim_tick refresh of the current-bill status only (bill list rebuilt on add/remove/select to preserve scroll). Selection chain extended in `selection.gd`: pawn-first then workbench, mutual-exclusion via new `EventBus.workbench_selected/deselected` signals. Closes Phase 17 deferred item. Code-level verification: headless boot clean, no runtime errors. Visual MCP verification still pending (need editor running). +- **Delegation report — bill UI sprint.** Steps 1, 3, 4, 6 (~5 mechanical edits across 5 files) → `quick-edit` (Haiku, 1 dispatch). Step 5 (new ~432-LOC WorkbenchPanel script needing full mirror of PawnDetailPanel + 6 source files) → `gdscript-refactor` (Sonnet, 1 dispatch). Steps 2, 7 (selection chain + main mount, needed design judgment about mutual-exclusion and main.gd's typed-var pattern) handled on Opus. Strings table follow-up (`ui.bill.until_count` key missed by Sonnet, fallback was a hardcoded literal) caught and fixed on Opus before commit. +- **Pattern recorded — "main.gd typed-var pattern requires CanvasLayer.new() + set_script(), not SCRIPT.new()".** First mount attempt used `WORKBENCH_PANEL_SCRIPT.new()` — Godot 4's parser refused with "Cannot infer the type" because `Script.new()` returns generic `Object`. Switched to `var x := CanvasLayer.new(); x.set_script(SCRIPT)` matching the rest of main.gd. Cheap parse error to surface via `--headless --quit`, but worth noting: subagents writing UI mount glue need this idiom explicitly. ## External references From 4e09dea03aff33f6490f01a99026063860f01544 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 00:35:42 +0100 Subject: [PATCH 07/19] Fix bill-editor crash on mode-change and remove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mode OptionButton and remove Button both called _populate_bills() from inside their own signal callbacks. _populate_bills() rebuilds the bill list — freeing the very widget mid-emit, which crashes Godot. Defer the rebuild via call_deferred("_populate_bills") so it runs after the signal frame completes. Reproduced by user changing a bill from FOREVER → "Do until 10". --- scenes/ui/workbench_panel.gd | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scenes/ui/workbench_panel.gd b/scenes/ui/workbench_panel.gd index b7cf4de..0bfe0b5 100644 --- a/scenes/ui/workbench_panel.gd +++ b/scenes/ui/workbench_panel.gd @@ -292,9 +292,10 @@ func _make_bill_row(bill: Bill) -> VBoxContainer: mode_btn.item_selected.connect(func(idx: int) -> void: bill.mode = idx as Bill.Mode Audit.log("workbench_ui", "%s: bill mode → %d" % [current_workbench.label_text, idx]) - # Repopulate so conditional rows update; OptionButton state survives because - # we set mode on bill before the rebuild, and the new row reads bill.mode. - _populate_bills() + # Defer the rebuild — we must NOT free mode_btn while its item_selected + # signal is still emitting (instant crash). call_deferred runs the + # repopulate after the signal frame completes. + call_deferred("_populate_bills") ) mode_row.add_child(mode_btn) @@ -366,7 +367,8 @@ func _make_bill_row(bill: Bill) -> VBoxContainer: current_workbench.label_text, bill.recipe.id if bill.recipe != null else "null" ]) - _populate_bills() + # Defer — same reason as mode_btn: don't free this button mid-emit. + call_deferred("_populate_bills") ) remove_row.add_child(remove_btn) From da55bf312c98e471d0f38077db01cb318d735b9c Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 00:35:59 +0100 Subject: [PATCH 08/19] memory.md: record 'never free widget from own signal callback' pattern --- memory.md | 1 + 1 file changed, 1 insertion(+) diff --git a/memory.md b/memory.md index 85f6308..7f7157e 100644 --- a/memory.md +++ b/memory.md @@ -303,6 +303,7 @@ Same scope as locked in `~/claude/ideas/rimlike/plan.md`. Realistic timeline 3 - **Workbench Bill Editor shipped same day** (commit `bdd4352`). Tap a workbench → right-side panel with bill rows (mode toggle, target spin, pause, remove) + Add-bill popup filtered by `accepted_skill`. Mirrors `PawnDetailPanel` exactly: layer 18, right-anchored 360 px, procedural `_build_ui()`, sim_tick refresh of the current-bill status only (bill list rebuilt on add/remove/select to preserve scroll). Selection chain extended in `selection.gd`: pawn-first then workbench, mutual-exclusion via new `EventBus.workbench_selected/deselected` signals. Closes Phase 17 deferred item. Code-level verification: headless boot clean, no runtime errors. Visual MCP verification still pending (need editor running). - **Delegation report — bill UI sprint.** Steps 1, 3, 4, 6 (~5 mechanical edits across 5 files) → `quick-edit` (Haiku, 1 dispatch). Step 5 (new ~432-LOC WorkbenchPanel script needing full mirror of PawnDetailPanel + 6 source files) → `gdscript-refactor` (Sonnet, 1 dispatch). Steps 2, 7 (selection chain + main mount, needed design judgment about mutual-exclusion and main.gd's typed-var pattern) handled on Opus. Strings table follow-up (`ui.bill.until_count` key missed by Sonnet, fallback was a hardcoded literal) caught and fixed on Opus before commit. - **Pattern recorded — "main.gd typed-var pattern requires CanvasLayer.new() + set_script(), not SCRIPT.new()".** First mount attempt used `WORKBENCH_PANEL_SCRIPT.new()` — Godot 4's parser refused with "Cannot infer the type" because `Script.new()` returns generic `Object`. Switched to `var x := CanvasLayer.new(); x.set_script(SCRIPT)` matching the rest of main.gd. Cheap parse error to surface via `--headless --quit`, but worth noting: subagents writing UI mount glue need this idiom explicitly. +- **Pattern recorded — "never free a widget from within its own signal callback".** Bill editor crashed when user changed mode FOREVER → UNTIL_N. Root cause: OptionButton.item_selected lambda called `_populate_bills()` directly, which clears + rebuilds all bill rows — freeing the very OptionButton whose signal was still emitting. Same pattern in the Remove button. Fix: `call_deferred("_populate_bills")` so the rebuild runs on the next idle frame after the signal frame completes. Commit `4e09dea`. Applies to any UI where a child Control's signal handler mutates a parent container — always defer rebuilds. ## External references From b4c9541eae366eabebe44dd0f62a2989cf5b3076 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 15:23:18 +0100 Subject: [PATCH 09/19] =?UTF-8?q?Pawn=20reskin=20Slice=201=20=E2=80=94=20p?= =?UTF-8?q?easant=20sprites=20replace=20coloured=20disc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pawns now render as AnimatedSprite2D children sourced from ElvGames "Farming Characters Pack" atlases (Pack 1, characters 001-015). Each pawn picks one of 15 peasants deterministically from name hash: Bram=004, Cora=013, Edda=001. Animations: idle_down/left/right/up + walk_down/left/right/up (4 fps idle, 8 fps walk, looped) + dead (single frame, no loop). Pawn picks animation each _process tick from (is_downed, is_walking, facing). Facing is now a Vector2i field updated in _advance_walk; round-trips through save/load. Sprite mounting is deferred from _ready() to setup() / from_dict() because the atlas pick depends on pawn_name, which isn't assigned at _ready time. _mount_sprite() is idempotent for the save-load chain. _atlas_for_pawn(pawn) is the single Slice-2 extension point — swapping atlases based on equipped armor in a future sprint is a one-function change. _draw() stripped of body disc + downed-rotation; now overlay-only (selection ring + carry indicator). AnimatedSprite2D child uses z_index=-1 so the overlays stay on top. 45 PNGs copied into art/sprites/characters/ + 45 .import companions. --- art/sprites/characters/Character_001_Dead.png | Bin 0 -> 956 bytes .../characters/Character_001_Dead.png.import | 40 ++++++ art/sprites/characters/Character_001_Idle.png | Bin 0 -> 2866 bytes .../characters/Character_001_Idle.png.import | 40 ++++++ art/sprites/characters/Character_001_Walk.png | Bin 0 -> 3462 bytes .../characters/Character_001_Walk.png.import | 40 ++++++ art/sprites/characters/Character_002_Dead.png | Bin 0 -> 876 bytes .../characters/Character_002_Dead.png.import | 40 ++++++ art/sprites/characters/Character_002_Idle.png | Bin 0 -> 2555 bytes .../characters/Character_002_Idle.png.import | 40 ++++++ art/sprites/characters/Character_002_Walk.png | Bin 0 -> 3020 bytes .../characters/Character_002_Walk.png.import | 40 ++++++ art/sprites/characters/Character_003_Dead.png | Bin 0 -> 846 bytes .../characters/Character_003_Dead.png.import | 40 ++++++ art/sprites/characters/Character_003_Idle.png | Bin 0 -> 2649 bytes .../characters/Character_003_Idle.png.import | 40 ++++++ art/sprites/characters/Character_003_Walk.png | Bin 0 -> 3189 bytes .../characters/Character_003_Walk.png.import | 40 ++++++ art/sprites/characters/Character_004_Dead.png | Bin 0 -> 860 bytes .../characters/Character_004_Dead.png.import | 40 ++++++ art/sprites/characters/Character_004_Idle.png | Bin 0 -> 2651 bytes .../characters/Character_004_Idle.png.import | 40 ++++++ art/sprites/characters/Character_004_Walk.png | Bin 0 -> 2994 bytes .../characters/Character_004_Walk.png.import | 40 ++++++ art/sprites/characters/Character_005_Dead.png | Bin 0 -> 922 bytes .../characters/Character_005_Dead.png.import | 40 ++++++ art/sprites/characters/Character_005_Idle.png | Bin 0 -> 2633 bytes .../characters/Character_005_Idle.png.import | 40 ++++++ art/sprites/characters/Character_005_Walk.png | Bin 0 -> 3080 bytes .../characters/Character_005_Walk.png.import | 40 ++++++ art/sprites/characters/Character_006_Dead.png | Bin 0 -> 952 bytes .../characters/Character_006_Dead.png.import | 40 ++++++ art/sprites/characters/Character_006_Idle.png | Bin 0 -> 3002 bytes .../characters/Character_006_Idle.png.import | 40 ++++++ art/sprites/characters/Character_006_Walk.png | Bin 0 -> 3490 bytes .../characters/Character_006_Walk.png.import | 40 ++++++ art/sprites/characters/Character_007_Dead.png | Bin 0 -> 891 bytes .../characters/Character_007_Dead.png.import | 40 ++++++ art/sprites/characters/Character_007_Idle.png | Bin 0 -> 2600 bytes .../characters/Character_007_Idle.png.import | 40 ++++++ art/sprites/characters/Character_007_Walk.png | Bin 0 -> 3159 bytes .../characters/Character_007_Walk.png.import | 40 ++++++ art/sprites/characters/Character_008_Dead.png | Bin 0 -> 870 bytes .../characters/Character_008_Dead.png.import | 40 ++++++ art/sprites/characters/Character_008_Idle.png | Bin 0 -> 2265 bytes .../characters/Character_008_Idle.png.import | 40 ++++++ art/sprites/characters/Character_008_Walk.png | Bin 0 -> 2804 bytes .../characters/Character_008_Walk.png.import | 40 ++++++ art/sprites/characters/Character_009_Dead.png | Bin 0 -> 918 bytes .../characters/Character_009_Dead.png.import | 40 ++++++ art/sprites/characters/Character_009_Idle.png | Bin 0 -> 2527 bytes .../characters/Character_009_Idle.png.import | 40 ++++++ art/sprites/characters/Character_009_Walk.png | Bin 0 -> 2939 bytes .../characters/Character_009_Walk.png.import | 40 ++++++ art/sprites/characters/Character_010_Dead.png | Bin 0 -> 859 bytes .../characters/Character_010_Dead.png.import | 40 ++++++ art/sprites/characters/Character_010_Idle.png | Bin 0 -> 2557 bytes .../characters/Character_010_Idle.png.import | 40 ++++++ art/sprites/characters/Character_010_Walk.png | Bin 0 -> 2992 bytes .../characters/Character_010_Walk.png.import | 40 ++++++ art/sprites/characters/Character_011_Dead.png | Bin 0 -> 799 bytes .../characters/Character_011_Dead.png.import | 40 ++++++ art/sprites/characters/Character_011_Idle.png | Bin 0 -> 2152 bytes .../characters/Character_011_Idle.png.import | 40 ++++++ art/sprites/characters/Character_011_Walk.png | Bin 0 -> 2639 bytes .../characters/Character_011_Walk.png.import | 40 ++++++ art/sprites/characters/Character_012_Dead.png | Bin 0 -> 866 bytes .../characters/Character_012_Dead.png.import | 40 ++++++ art/sprites/characters/Character_012_Idle.png | Bin 0 -> 2658 bytes .../characters/Character_012_Idle.png.import | 40 ++++++ art/sprites/characters/Character_012_Walk.png | Bin 0 -> 3163 bytes .../characters/Character_012_Walk.png.import | 40 ++++++ art/sprites/characters/Character_013_Dead.png | Bin 0 -> 893 bytes .../characters/Character_013_Dead.png.import | 40 ++++++ art/sprites/characters/Character_013_Idle.png | Bin 0 -> 2732 bytes .../characters/Character_013_Idle.png.import | 40 ++++++ art/sprites/characters/Character_013_Walk.png | Bin 0 -> 3211 bytes .../characters/Character_013_Walk.png.import | 40 ++++++ art/sprites/characters/Character_014_Dead.png | Bin 0 -> 859 bytes .../characters/Character_014_Dead.png.import | 40 ++++++ art/sprites/characters/Character_014_Idle.png | Bin 0 -> 2528 bytes .../characters/Character_014_Idle.png.import | 40 ++++++ art/sprites/characters/Character_014_Walk.png | Bin 0 -> 3057 bytes .../characters/Character_014_Walk.png.import | 40 ++++++ art/sprites/characters/Character_015_Dead.png | Bin 0 -> 845 bytes .../characters/Character_015_Dead.png.import | 40 ++++++ art/sprites/characters/Character_015_Idle.png | Bin 0 -> 2639 bytes .../characters/Character_015_Idle.png.import | 40 ++++++ art/sprites/characters/Character_015_Walk.png | Bin 0 -> 2911 bytes .../characters/Character_015_Walk.png.import | 40 ++++++ scenes/pawn/pawn.gd | 126 +++++++++++++++--- scenes/pawn/pawn_sprite_frames.gd | 51 +++++++ scenes/pawn/pawn_sprite_frames.gd.uid | 1 + scenes/ui/workbench_panel.gd.uid | 1 + 94 files changed, 1960 insertions(+), 19 deletions(-) create mode 100644 art/sprites/characters/Character_001_Dead.png create mode 100644 art/sprites/characters/Character_001_Dead.png.import create mode 100644 art/sprites/characters/Character_001_Idle.png create mode 100644 art/sprites/characters/Character_001_Idle.png.import create mode 100644 art/sprites/characters/Character_001_Walk.png create mode 100644 art/sprites/characters/Character_001_Walk.png.import create mode 100644 art/sprites/characters/Character_002_Dead.png create mode 100644 art/sprites/characters/Character_002_Dead.png.import create mode 100644 art/sprites/characters/Character_002_Idle.png create mode 100644 art/sprites/characters/Character_002_Idle.png.import create mode 100644 art/sprites/characters/Character_002_Walk.png create mode 100644 art/sprites/characters/Character_002_Walk.png.import create mode 100644 art/sprites/characters/Character_003_Dead.png create mode 100644 art/sprites/characters/Character_003_Dead.png.import create mode 100644 art/sprites/characters/Character_003_Idle.png create mode 100644 art/sprites/characters/Character_003_Idle.png.import create mode 100644 art/sprites/characters/Character_003_Walk.png create mode 100644 art/sprites/characters/Character_003_Walk.png.import create mode 100644 art/sprites/characters/Character_004_Dead.png create mode 100644 art/sprites/characters/Character_004_Dead.png.import create mode 100644 art/sprites/characters/Character_004_Idle.png create mode 100644 art/sprites/characters/Character_004_Idle.png.import create mode 100644 art/sprites/characters/Character_004_Walk.png create mode 100644 art/sprites/characters/Character_004_Walk.png.import create mode 100644 art/sprites/characters/Character_005_Dead.png create mode 100644 art/sprites/characters/Character_005_Dead.png.import create mode 100644 art/sprites/characters/Character_005_Idle.png create mode 100644 art/sprites/characters/Character_005_Idle.png.import create mode 100644 art/sprites/characters/Character_005_Walk.png create mode 100644 art/sprites/characters/Character_005_Walk.png.import create mode 100644 art/sprites/characters/Character_006_Dead.png create mode 100644 art/sprites/characters/Character_006_Dead.png.import create mode 100644 art/sprites/characters/Character_006_Idle.png create mode 100644 art/sprites/characters/Character_006_Idle.png.import create mode 100644 art/sprites/characters/Character_006_Walk.png create mode 100644 art/sprites/characters/Character_006_Walk.png.import create mode 100644 art/sprites/characters/Character_007_Dead.png create mode 100644 art/sprites/characters/Character_007_Dead.png.import create mode 100644 art/sprites/characters/Character_007_Idle.png create mode 100644 art/sprites/characters/Character_007_Idle.png.import create mode 100644 art/sprites/characters/Character_007_Walk.png create mode 100644 art/sprites/characters/Character_007_Walk.png.import create mode 100644 art/sprites/characters/Character_008_Dead.png create mode 100644 art/sprites/characters/Character_008_Dead.png.import create mode 100644 art/sprites/characters/Character_008_Idle.png create mode 100644 art/sprites/characters/Character_008_Idle.png.import create mode 100644 art/sprites/characters/Character_008_Walk.png create mode 100644 art/sprites/characters/Character_008_Walk.png.import create mode 100644 art/sprites/characters/Character_009_Dead.png create mode 100644 art/sprites/characters/Character_009_Dead.png.import create mode 100644 art/sprites/characters/Character_009_Idle.png create mode 100644 art/sprites/characters/Character_009_Idle.png.import create mode 100644 art/sprites/characters/Character_009_Walk.png create mode 100644 art/sprites/characters/Character_009_Walk.png.import create mode 100644 art/sprites/characters/Character_010_Dead.png create mode 100644 art/sprites/characters/Character_010_Dead.png.import create mode 100644 art/sprites/characters/Character_010_Idle.png create mode 100644 art/sprites/characters/Character_010_Idle.png.import create mode 100644 art/sprites/characters/Character_010_Walk.png create mode 100644 art/sprites/characters/Character_010_Walk.png.import create mode 100644 art/sprites/characters/Character_011_Dead.png create mode 100644 art/sprites/characters/Character_011_Dead.png.import create mode 100644 art/sprites/characters/Character_011_Idle.png create mode 100644 art/sprites/characters/Character_011_Idle.png.import create mode 100644 art/sprites/characters/Character_011_Walk.png create mode 100644 art/sprites/characters/Character_011_Walk.png.import create mode 100644 art/sprites/characters/Character_012_Dead.png create mode 100644 art/sprites/characters/Character_012_Dead.png.import create mode 100644 art/sprites/characters/Character_012_Idle.png create mode 100644 art/sprites/characters/Character_012_Idle.png.import create mode 100644 art/sprites/characters/Character_012_Walk.png create mode 100644 art/sprites/characters/Character_012_Walk.png.import create mode 100644 art/sprites/characters/Character_013_Dead.png create mode 100644 art/sprites/characters/Character_013_Dead.png.import create mode 100644 art/sprites/characters/Character_013_Idle.png create mode 100644 art/sprites/characters/Character_013_Idle.png.import create mode 100644 art/sprites/characters/Character_013_Walk.png create mode 100644 art/sprites/characters/Character_013_Walk.png.import create mode 100644 art/sprites/characters/Character_014_Dead.png create mode 100644 art/sprites/characters/Character_014_Dead.png.import create mode 100644 art/sprites/characters/Character_014_Idle.png create mode 100644 art/sprites/characters/Character_014_Idle.png.import create mode 100644 art/sprites/characters/Character_014_Walk.png create mode 100644 art/sprites/characters/Character_014_Walk.png.import create mode 100644 art/sprites/characters/Character_015_Dead.png create mode 100644 art/sprites/characters/Character_015_Dead.png.import create mode 100644 art/sprites/characters/Character_015_Idle.png create mode 100644 art/sprites/characters/Character_015_Idle.png.import create mode 100644 art/sprites/characters/Character_015_Walk.png create mode 100644 art/sprites/characters/Character_015_Walk.png.import create mode 100644 scenes/pawn/pawn_sprite_frames.gd create mode 100644 scenes/pawn/pawn_sprite_frames.gd.uid create mode 100644 scenes/ui/workbench_panel.gd.uid diff --git a/art/sprites/characters/Character_001_Dead.png b/art/sprites/characters/Character_001_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..2340c331ab2907a3181803bdc806d0d036a8f135 GIT binary patch literal 956 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU~cwwaSW-L^LEa`tji1xZU0Y| z6|7mxk-Ek2WUffLG^-#RUwneXn+L%h)qDq+zerU0!DQrbbR$%9ch3UjoxK~TeUadw zb&1bhcy`2x|NEkEuZfqqe@j=pUiq|9qyG14wM-jgI1b1#8MHH;QDi6vGt-~u@Bgr2 zzim0=!?QP^AA7XC`G5Rx=~R7|r;oz_s&lztpR{Db%}XENoOV)gs<-?X#3=J^`{u}x zzMq6wzMkE%zpDNr1K;o4k?ZexDmDf2{wW(Nma$ocl4lt2A`^{3?w^|k+kt}_-LjQwQ4*Ik$C z->bh%nNGw;<6Z9z3snC)0?W_ew$MWj zHBA2O{dQeRNlA^Cr_~KacnfJYC}R o95CUM;tia3J`{Gu@Z356kH?q)N|!Ew0L+yPp00i_>zopr0C1AU0ssI2 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_001_Dead.png.import b/art/sprites/characters/Character_001_Dead.png.import new file mode 100644 index 0000000..4fdb88a --- /dev/null +++ b/art/sprites/characters/Character_001_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dl5678nw0ak4q" +path="res://.godot/imported/Character_001_Dead.png-53f8a2c7c9d21301ad6ecc32b2e18e90.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_001_Dead.png" +dest_files=["res://.godot/imported/Character_001_Dead.png-53f8a2c7c9d21301ad6ecc32b2e18e90.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/art/sprites/characters/Character_001_Idle.png b/art/sprites/characters/Character_001_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..9d86ee8cc056e17e3cfa8a6a75953766ae0514c0 GIT binary patch literal 2866 zcma)8c{JPG7XBqM&s5rnx|EAvp;X$KN>Zh*;RZ#tN=+A?R4WZFA;G1l8ahx_lHLxg zf>w(f!zH2>!>v~oxoIV8h-T1M5|kJBt+&?ubuojc4~Lb0bS4KvOlikW*ApdWXZ_-Q1VcXBy}ifqDl3U;OUVB z&@Ikl1{rxzM=x#yhZhP;kIvET?eOolpoCujE@E00edE{o*lj>jjzRZykGwvVLl$(* zhAkoj{ntCS!lKVlCuDR)$liad@*aaHA4-YKCdi{L7aUke6ri!0vIxeG{!@HFYCb zC^8Vt$iF_d@Taqvnp*hlU^mm$hxZN7C%q1)aXx6g?MQKf9Yh!ul@o6Xu_3Z|rnXB> zyW`#!<!1RI&Tjd;%*vFxOeZ;)A6z}(cj8m!iXN4JvOCBq&1z%K$%uxGSR~=*2dN#uI;|qT zVh)cK_N3ZgH56mamALcaU!67w-J4z*JOsMRoYRtwPjKsxz5)Kuj)^w|`FJ0o3bB09 z_GMajOJ}1ayS(@@Yw(+;1t;(KL~4V?ONx<+*R+jc*=@F$zleKQWOd`7cy9}vM6IB& ze{O%laQbYU8fuoNEQ#RoxJnnl%Pmcs#B_$V$plkDFsMoGdPSPW3Y)L;i~9uM1>CTT zggazcD_2!W{C1$(jQKF~s_2^qY8>ZSI`EMmx0wNdkfA!j+YxAeNn);)3EsYAM`j=p z2{}C=j|$2z+ymvH8ap^e2$fxDMe5$7rKoj6Tz?afG4);Cn?Jyq3VYy zKD}yBM!w9$r-VA05gc<2r=Jy0_StP%I&g6GWfrb|VC(jP-`o1*#sK-fsdY6(DZnmF zYla0eUAbQ={BKz=E{vIj9%-p{T_|L2H=1%$J`FBS3FVcXaZ_cndj@sq|M-xQE?`D?eW zEz4fgv$6m{oirYo+*(zwixQp@cWxs>!9>Uxw4bMtCWM#}->w|DvQT$VkR zt#?r6%^tT_OBz3aI7Q(O44ETk^3vrv61>nppd(D_=0hn;3R7`Or=T0()W}5=lvB$V zyPv-)camLs5lfq!BOvxG0=q3XCib#KyFR?AcBiYU0`@6jQl))nX*KTv#%KL?)&g46 zT`7x)gYyxrcbC}-rza#7D`My84t-zo>>9c}CTl?~EbDBe=11PP4N*KL{MjqSjyZxv znc-RL8FdwoQHAPN_ZC7c11>JlKo6{rQ`?qZy@a%7h|VGD?(F(JhXxzMKP}MFCKVXW zGr}Ki8sBTbBCGe^V1HMe32g^meceR?_!@V>ZER5ik*~P2a5)O>U2#FOBB!wk#*in>jpsB1=rSyLJa=-dKWLO*;{|U!1k1o_QkQ` z8a&it#3`%u=^A7SLDATug=F$@M2l}b*>826*PIeU5 zQ#kbnyDAgE>S~xujsaQuIf=hAe&T#p1TJRziIT>vABwNz9~0J!p;wBR6hrfhpDSptWcV6b6n@hG$mwwTA%2>P}h<}Gf%|F6}{$m#ZTapKU*Z@sffAV6k&<8#Zlsplqu#}?1MAn}e zCDZN))s$E#)gE8Ky3zP;1R{n#-lpubP+0mpCJNyEXy~>+t=Q#YHZfd2lw}PK(QJ@V zR2sC3%u{(cQ7h;qSZHgqPNpLULE4Y1>60f)#XiXB`JBMe#b1au>0(U0s_;A)o3g;W zw1hqL^J$a*PmuU+MK;o+3Lp|vV4)5~r-Z2D+lJ-*SmKujHtsQZCqVSu(;f@>UIV`o zx2S^-H%`7S<$a_0sXW+obJ%>83CvEKzB$>rTGiB20I^ z4p^4qo8jXFUrOZ35{M6tzjgxE7O58{I^7^?*aWj(} zmO}Ah%#GVbf8p9BL@JM(Flemq6KR9u;pPq_#2Ds%7dt{uXsxsr5w*YZVW7()&1QQ- zaS7LWg|~AfW|jlyThS5w!kXs4i(7&VC@MDFG|1HiX=SXp3XzJ@Y*x4(DW*v}~C9-=bA)bPgc%a%1UL z@rzr6W$-FPfz0!i2SocC1A?}SWTFOWY4vT#=-;cTG|T( N?rvz;I+w8Ye*j+vjOYLW literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_001_Idle.png.import b/art/sprites/characters/Character_001_Idle.png.import new file mode 100644 index 0000000..1ee7f2a --- /dev/null +++ b/art/sprites/characters/Character_001_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vdtgxefdrv6h" +path="res://.godot/imported/Character_001_Idle.png-949c25bb7badfcc54ea7b50733e40589.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_001_Idle.png" +dest_files=["res://.godot/imported/Character_001_Idle.png-949c25bb7badfcc54ea7b50733e40589.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/art/sprites/characters/Character_001_Walk.png b/art/sprites/characters/Character_001_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..317724981feb52686cc43a0ebbd4527345bf8d83 GIT binary patch literal 3462 zcmai%c{o&UAIHy(v1f^d2&t?UZ)D3dS!$38sXW3=gwbS=$ToH&sV7^Et+I=;WGtDn zXDJfKmNmvU7}=5;@9BN7_s{2g|M;DA-S>5$bMEW9zrXMIbK=aA*Lb*2Z~*|oV~jAg z1n;;%#lZ$%TXpaCfb(X?hF7foGii2HSg|`NbSkk9VqKP2hW&%ywuD}wu2XA#s=+9~f+Ga$w;f@Z7;aZNRR#Ef(M>g?`67Ak zD;J{SVw=NJI{ZfKZX1gZf4mW*p>k*@eW<(v$h=No-G~*SEqC~P95Hs~1{VqOZEDH8 z-D^~j#@wl$H%8wgy@z^gLGDL5KkknG&4Mtw`zfXN#drBT67w`JrBGlL?>%&A4N2Kv z(K_M5Itclw$B=4$C^%}$xZy*dS2XXY4$hnW68HSY^#j2^ zG;(fPJ?Q*&%hD;QkmWuASK@sFQ2vH5EN$2kE}qiOq9z+khe^H#JJ$j+PeaCEn5{bt?cVgQEsSQ3GQ0IDoyGF_&8_o%#7gR8>ulBE1)g!g8@Css=AN zH+Q|}q6F*#XM&lTV-Y0GP#Tg&6|$MqPn=6KdFRQD7%iU9lat0H4H{mQ8kQ`|p1^Jw zSjuPW1P*Udmrk%Gc}SiD0IFVE*%J(fuFaQ4Et+`nL)g+k^euZ)-Hkz~`{i)%vbWjr z63t7yoRgAEVJL(sQ`{~Pw-fVtTG&zqmNd*it+58AU-=7wL1=vIA~HTAF+s~ddv$Vc zLGw>1vcH@MfB{Lr)ADYoq~aEjbYnt^t^upJ`={hTag;J6$#YPdJS8c53|GdW_@LX% z8^P`f(pol-$3kCH*a*G71A&gg(Ej3LdG`%7E`X;JB#h*fH%)&^+$_iMog_hk*yz^j zyn>|O0}jn9V<)MOs6eTOTHhsV%d>Kk*Q{Sdu??}46#Mq%mcFFDgGn37CL+Oh+x}yA zNg%)f%vKTRg=Cle$DQQVNDxH3v!un%SBEHWzpj_BPQl?hF{~ADr?%{x zB}Q;FSct?(_#NOv%s*5oVVw`Y*80Xv%wMrpfpKk(5B(U5n4nLl)hB2;xc-8?rfaP2 zo9(iDN<%)cj<%&;={fO!9;kRTRmlXZ|LYCvPjvWsU5($qKH(lkxSvd(YqG;*jsG{al=Y zIlV)Dqy1Y&v|vRsYWH4FwPFN!RMM*t#VHhf&lBiW7Btr|3xQ)Unm?QY?I{~fYs-4DY%VY zd!Fr8@9}(xu(#74ul%h`f7^AkV@&ekAp3l8?qEvegrJAscS57{ZsnD=Fwjc2vL z4ly|IkZA(Y7qF>1PpctyCKCtbL*MAo0$fsm zx^On04+=yM1Osskhj553kv$#@m5~ruCR8T&WRD4t8#B6S(h{sJ6WfTW~W0yneidjDYGy5JZ@Ed!l67~nwhQ7?2+14R9 zq%_|D*H$oU!3`d zG|IDwqhr3Rz3HXP6gx2Q(SH7h#)ftIP5w01wKzGK00HZv@)95u35@30-1Ym=_-qwf zw<+f#S@Ga*I_!V6<(X@4fND$;KmR~cv0LrEc_n(d=%Z*^xVl}U(<9mkZx{f=zkm*?(?!8dSY>Gh+3^#@fNkq0BoR+ zH@|UE_(jAD`4MLXrUUGA2}i5>jV!)%*wCxNG_%u#fV$T1V;R})z(%~J-apy*GiCj0 zmlX})@>zkmCiwzxYq;R{XvuKzH*MsuOx7Dlw2SQpa;^LU3gszQRIEXdO4PGr)(XgRxet%m4r@)}y z@L3eob=&jd`7SWrX++rkzU#{gB*V5W9we)!+R*=m&FU&M9K;#xgPMf)hC^0!LbQ); zE5bP>@;Y6)-N8s3 z#nnn3-%Q#RGSD_A+itSihiW>cMOt9+bpRZ+L$|TAJQUUp6>1(lHhMhJUkB=UW?C7E x(@~?{?)P|S|BXkw8n)`*Kdov#@PECW9@+pCLtjHUJAwaKfUyzM@V!1d{NFkzqMQH# literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_001_Walk.png.import b/art/sprites/characters/Character_001_Walk.png.import new file mode 100644 index 0000000..0f2c40a --- /dev/null +++ b/art/sprites/characters/Character_001_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bx7dgdvihp3kj" +path="res://.godot/imported/Character_001_Walk.png-84dbf748230688d019f76cc700c71c17.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_001_Walk.png" +dest_files=["res://.godot/imported/Character_001_Walk.png-84dbf748230688d019f76cc700c71c17.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/art/sprites/characters/Character_002_Dead.png b/art/sprites/characters/Character_002_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..f1be9a01734a82e3dfbbef115480e3d1ffe9c1a1 GIT binary patch literal 876 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU^e!2aSW-L^LEZzuMkECH+$uR zrB9ExuzAjIinN?95VON0YKJJ(-7TM4qkc))x@arDU_CUgX-lU1l)nnXOMDiui@Gaj z=9FeI`MX-lxtnME=bSD`b9+BEe~Hd%j_KLqwG2gDB^YFw8a%?<5eY<>o;wnC$|NB4k%Q{r2ZObu#KacCX-JE;h zKbHZyYks?4h+kWuGGldP?;?%)HZfk$qrX2ouHI!+zjvzN_UO~y3-(w2Kft&pJ?(U~ zdA^FL+@0O?Rnpln&Cr;nl7DZ%_avDweOmu5uEh7(KVXn@4fN~lU0yFx|7bz^l*5xB zpSsWb>-9(eFX0aH_vT31PL5IWoAm5ThT8FCmo>Uh{k!Z^U;D3-=|#zI<+Cd<>TFq7 ze8KwEv0F*krawQoT|`2W+t#(MUM znfX&aH+}#5`=dpKL7K#w3g-7`fK)AW;*=*{~=2OS5#n)AE4kJ_J-egNyCq?Iv~X{ zE`w20!w!Ci@4HFE$5%IENl|!hE0kw=S56x43uk_ao__5a+M~|yof8y41DJ~#JYD@< J);T3K0RT|Hg?j)1 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_002_Dead.png.import b/art/sprites/characters/Character_002_Dead.png.import new file mode 100644 index 0000000..70cfd3c --- /dev/null +++ b/art/sprites/characters/Character_002_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bvyms2xddxkky" +path="res://.godot/imported/Character_002_Dead.png-6e0015f8f83360b89fdd2ea19f2e0a7c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_002_Dead.png" +dest_files=["res://.godot/imported/Character_002_Dead.png-6e0015f8f83360b89fdd2ea19f2e0a7c.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/art/sprites/characters/Character_002_Idle.png b/art/sprites/characters/Character_002_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..c767dc8a501170526c7e0d19fcd54b6c7d381740 GIT binary patch literal 2555 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 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_002_Idle.png.import b/art/sprites/characters/Character_002_Idle.png.import new file mode 100644 index 0000000..92e87f2 --- /dev/null +++ b/art/sprites/characters/Character_002_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://docwang41u716" +path="res://.godot/imported/Character_002_Idle.png-6255c26c24043e08900d4ae605cfeb76.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_002_Idle.png" +dest_files=["res://.godot/imported/Character_002_Idle.png-6255c26c24043e08900d4ae605cfeb76.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/art/sprites/characters/Character_002_Walk.png b/art/sprites/characters/Character_002_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..9d75294acc04fb3d5de517fbcf8f41187f8a8b27 GIT binary patch literal 3020 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 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_003_Dead.png.import b/art/sprites/characters/Character_003_Dead.png.import new file mode 100644 index 0000000..f5537bf --- /dev/null +++ b/art/sprites/characters/Character_003_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dqdudpnru30hk" +path="res://.godot/imported/Character_003_Dead.png-1d1221ee02cee53c8990f3b4ff17debf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_003_Dead.png" +dest_files=["res://.godot/imported/Character_003_Dead.png-1d1221ee02cee53c8990f3b4ff17debf.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/art/sprites/characters/Character_003_Idle.png b/art/sprites/characters/Character_003_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..f4eb96d6b82a3f081769249d6a691a745a01c56e GIT binary patch literal 2649 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 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_003_Idle.png.import b/art/sprites/characters/Character_003_Idle.png.import new file mode 100644 index 0000000..45b1af1 --- /dev/null +++ b/art/sprites/characters/Character_003_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cmirhu1042dd2" +path="res://.godot/imported/Character_003_Idle.png-a766339ea476ee9c81ef530027361cec.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_003_Idle.png" +dest_files=["res://.godot/imported/Character_003_Idle.png-a766339ea476ee9c81ef530027361cec.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/art/sprites/characters/Character_003_Walk.png b/art/sprites/characters/Character_003_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..d09814daefc1fa11e998bfa3a0926b696173d6b6 GIT binary patch literal 3189 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 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_004_Dead.png.import b/art/sprites/characters/Character_004_Dead.png.import new file mode 100644 index 0000000..59c766b --- /dev/null +++ b/art/sprites/characters/Character_004_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cusg1ywjaxe38" +path="res://.godot/imported/Character_004_Dead.png-981400b980c80c3710e64738bd1cf562.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_004_Dead.png" +dest_files=["res://.godot/imported/Character_004_Dead.png-981400b980c80c3710e64738bd1cf562.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/art/sprites/characters/Character_004_Idle.png b/art/sprites/characters/Character_004_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..c591502502ebf596fce556377a9039b7a292ec66 GIT binary patch literal 2651 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 W^LoiwMI&I2WbkzLb6Mw<&;$TSoy6$? literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_006_Dead.png.import b/art/sprites/characters/Character_006_Dead.png.import new file mode 100644 index 0000000..c75c94c --- /dev/null +++ b/art/sprites/characters/Character_006_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4o73ivsxbg75" +path="res://.godot/imported/Character_006_Dead.png-8aed15fa6599b13181065136d33bde6f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_006_Dead.png" +dest_files=["res://.godot/imported/Character_006_Dead.png-8aed15fa6599b13181065136d33bde6f.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/art/sprites/characters/Character_006_Idle.png b/art/sprites/characters/Character_006_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..5cdea3ed26808689b6c8a3b0509b1220e86f2eee GIT binary patch literal 3002 zcmZ{mcT`hb8pUsj7>X1XL^{%*Qk6#yB?y9aBg92-N|i@1fP$C^dOHfEK!bE3g&023}{ zXFg_q6xyoYr%r3DX^EIQB|?MFLGr?4;}Z&8c#fs};Es94C5Uv;uhsNRt_gUw)t%6T zlwB!7>x7TFZz%-^IXhbR7}+}@&-T4{cXRD-B*yB8zUrH z3kdJ04t9S=12{V8nNl~rGJm@bqcSc2L5q08 zMEhR#UeKiL!ZVFH?Flz#DUV^Dw?N#@{XuNpE(~y6PC+7ByNbJco22%EeEYa3BZ*@2 zA=*7uxM7vv>3v%@nmV=^m4WgDue916olNU$=3K-U@gj1}t;#d!n4cOQSSQM=Ypwfz zgOk!~!}TYdPx6L7qp??$HQZ5q8`wpr;zWhXu3IH{3a$P+q)d(+exn46AHwZHFWCU8 z8+jMhc1DB8S;YkyW(hlu%-0v%FYV^t5LNH0dCsD}sBuu^Sk z>xL$zdv=S;Hvs|KldAnipuyAqYhmK?YvFiiC=cLa6Gbi$=PSFH?_^y$=9HKXv$70kXEun2!CmYgSfPQr%T-m43;7zZnb;yTe0q zgfz)k_Q$&8k;TMto_L66k*k}aYW%z(E zE|VGFz;ch9!I_=wO4E=o*kv~wV_T+}&FKq})w05K#Smgi)-XUX?0jjOhHxxBh2$C) zQ!Zx3y=I!lqglQE(Jw4fXEkEFidU$1Sw0UFx68yUcSY%})@}=|UA?8qxImQ=^`IJI zv0VC5HDi`J5_K9nnK*zGLr9h!0CHBJG*DFfMcjL&H1xh(^$drqZeYC zd=)`b?%utbUI5g}!VVgdrW6s{CV*k~f1m=p1DSC$l^WtA0Q^Y#%+{_!*c)MK#!NH- zz&*P%BV^1h{zB@?9XyBIaoIz7B83o}JJ!yuGkwpOHsBe1ix!|L`b1lJNM z9xRI$@7!4TaGm7bhy&c}_Ai!QwlvT^cr)6YG(RHlwauDQ6$jyWp%obZe@XwQ_vd`j z+#?%T+NW)jn&DORKAB;i#gdYE^sK@;qwwL^MTC+jSu7tcOp=U9PJ7;( z_+fQ?6|ttbGcSkLhkdo{9*4>b@oUvM8a0{%FufW$kyMt3O4cFE6(Vs2_4h+)#^Quv9mJ4TA~OpylRfe9;X|~4 zAyJ*DKHk?u)4o4oR@k9^C%uRC14GDTHA}iF_}-=KgD$&}0#Vgp6Y>vTtY}$f34Rq> zuZ;9EH*iLJfon<_oVHaDu=yqLeLQ&|B_H_2vS%@)8G-KXdYmm(OeX;(y|Rqc&~l?K z(t#dJ0|ABq{kQ&2+Mh9Ch-X`NVF)&yDuQx8X-d4DvwDA`)-J!oUEAi=R$T9ok(Fk8 z+v+IML~VEE*q%Tr+M1co1GjB8Kh)U|oxg(gBQeU-3E%p6!uz9Hmi~!_6wx?^dfRk}W8Qa|$Q zpKKozhM?&Fl46r4Wlr9OQay?Xub~OC{wpf^)BW$7i0m~n%kkm;{!YtEfk=twmXzl= zil@eUqwF5gjj?Q|VPk-%u3k5Y)9KK($-AnVarJF4x_vHloO6gU;ZYhu8ac}fH4ZCA z$c$Mj&QO{pJDBz}GvQy|Poyr1eSPN?7U0=$9qimM;-V_8gbL}vi||Rgfoaf#Ql7^Y zsIZIb=GtcVlsXHo)`Xfy{`V=x;;;7>2)v}T0$f9b{7s;kU#N7p#lR*LwA9{*Tp{E%3(*v87_ OfVqjyr8*;z#D4%ofVFS{ literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_006_Idle.png.import b/art/sprites/characters/Character_006_Idle.png.import new file mode 100644 index 0000000..89d1b5a --- /dev/null +++ b/art/sprites/characters/Character_006_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c2vhpfcn6ayla" +path="res://.godot/imported/Character_006_Idle.png-87c1a57347f63318a7da74a3696653fd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_006_Idle.png" +dest_files=["res://.godot/imported/Character_006_Idle.png-87c1a57347f63318a7da74a3696653fd.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/art/sprites/characters/Character_006_Walk.png b/art/sprites/characters/Character_006_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..2a4e18002eaab92279d9e6aae638cb08cc49dd1b GIT binary patch literal 3490 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 Q_FlmJ%z4uq6L;MI02zvnLjV8( literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_006_Walk.png.import b/art/sprites/characters/Character_006_Walk.png.import new file mode 100644 index 0000000..bb5bb39 --- /dev/null +++ b/art/sprites/characters/Character_006_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bq5ba7puv8mx1" +path="res://.godot/imported/Character_006_Walk.png-751a8de1d914918d6e78a313db34ee8e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_006_Walk.png" +dest_files=["res://.godot/imported/Character_006_Walk.png-751a8de1d914918d6e78a313db34ee8e.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/art/sprites/characters/Character_007_Dead.png b/art/sprites/characters/Character_007_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..729e5541cc8cd248c902625df9f28ce404d429c1 GIT binary patch literal 891 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV0Q9!aSW-L^LCDXws0atd%b#} ztdXSNly%!nf|O*fXMAj3B;-4@za8Yetdee@jZi$9!mlr;|ynpX95kD=*-Vw^7BIY zHh)=EvFzT%eJYw^+_iK62Uz$=F8XzF z^Cz+L&GGzRd)Y_%$M;|Lc91E*{HpYM|M8ISYxCGXn!msQBG|$5Ob}2w<5J)nv97C0 zYc0aKd(~u*7Mf2zyk_HnlY{r?)i3sEd}x!Z{#Qv>^!KE?EGc%YYhRm_mlZB}d~a)C zS8cD_rK@Xpx-$Y@VO`5_(97)mIAMDHM&Tdj-}7Y-#9a(u8Z)V+(imvP&i#2NFJF90 zQ}bLkC42pg_a)0e{I~w^me1tOq1m#)=6q>k;X>!DRxh6N_wDT7s`b>$);2Wuy4LBS z)JU^WCLQ~2|A*aYxmK#{xohtKe`-(4_FagZcwhc9+mG4b^Zytcv`kRB8(w-zU+&I_ zEVJ|1WUq_-xcxEzZ>Yha_yV&FGE4^T3}+M>ia8Dt$xM_#aOb>T!_N7&48``p8J^XB zV?6Ws4fBlOH`xuoZ{|;UmySij!<+>e(TzP3Ac-C()fSL54&QVj-Y;-}lj0YcXWKV7 eVx;18`j%TipMCcEj~6ggF?hQAxvX`6ap`haO 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

cMj65Iyr_I0L@%-ZLm7jSSffr;GqCvgzN56a> z3vZXF+`F*nEd7uT!oN6$Smg_pA1mOS<}%GhPIJMdJ_d3Pc;G_%2)zx3lzzrC)<05@ zfa@|~@q|ihtTjzRX_`?!HhLC5dOy#EWZ9-r)O?0NXQnZQLsSqTP(<(?xD4y(qV7a` z*YoY_$c?4>fK?G;q=#PnA*VuE{iV6U$9lnC4d7eCEr_+RPw1%D#;Af!;yi_vvHl>i z#0yTtl<~Xuweo3=tVom_)%GotDRFvDJYruLF3v4}!o1O}?tT84SVaeg(d>BQhZghv zGHSF4(A}FKrf+~%jP0H7_Jc?GkJdRK@m4P$2uW6_8!tqJN}4IhnuC<71fEpSzpYn(MBZc#>Njnp*9!%CV=RsoBtLw6&fU70a4S=V`UOx?fnV z{UC>q#=aE#+Te58a0FTh_-M9j+^2#bP%8M}RYkJFd676?+N-{fetC5er5yBzNbFh* zQ<;W?QsZRK-a;il6PFv-*u)>SXx@3pESaT4Qk`gp2IujZiD*F8p zWU%jRxXpFh7=Qiq83!yLDxy7O)-vS|$s~EYy)fS5Ixi!SB`+#_gojVClCgWE!wx2A z;P{2mBCgvVN$Q)4Fu8e{;2Vk0sK%$aUBz-=1K3Kk8@Yb)4srBpp4-FqjK|$lZc1@y zMP@B@ASJJ^W^%=9Z=d)n)0u@W*S*4!)L8vL+{+enH(s1ex;`laei#4?Q)`oIV~_ZM E00yV)CjbBd literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_007_Walk.png.import b/art/sprites/characters/Character_007_Walk.png.import new file mode 100644 index 0000000..c26bdcc --- /dev/null +++ b/art/sprites/characters/Character_007_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkojdn2l3jl0n" +path="res://.godot/imported/Character_007_Walk.png-34af3247b8cb2b63fd059aac5242294c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_007_Walk.png" +dest_files=["res://.godot/imported/Character_007_Walk.png-34af3247b8cb2b63fd059aac5242294c.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/art/sprites/characters/Character_008_Dead.png b/art/sprites/characters/Character_008_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..72cba1e8b4f9fb4b7ae403d0a9431edc883120c8 GIT binary patch literal 870 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 fsQT}~kOFt~q@RNaniFLoH^A{9BZL$3S<{`L1l literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_009_Idle.png.import b/art/sprites/characters/Character_009_Idle.png.import new file mode 100644 index 0000000..27428e3 --- /dev/null +++ b/art/sprites/characters/Character_009_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dhcaxufn8awxr" +path="res://.godot/imported/Character_009_Idle.png-0ab1c95e4e42dd0240b47ad8441fa4c7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_009_Idle.png" +dest_files=["res://.godot/imported/Character_009_Idle.png-0ab1c95e4e42dd0240b47ad8441fa4c7.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/art/sprites/characters/Character_009_Walk.png b/art/sprites/characters/Character_009_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..2ed34cd88d8bfe5a7e2defb5361180a292dfdac8 GIT binary patch literal 2939 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 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_009_Walk.png.import b/art/sprites/characters/Character_009_Walk.png.import new file mode 100644 index 0000000..ac9d72e --- /dev/null +++ b/art/sprites/characters/Character_009_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c6ytplnnfb3ou" +path="res://.godot/imported/Character_009_Walk.png-6cd650d24ac756f6bfd224f999d84b8f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_009_Walk.png" +dest_files=["res://.godot/imported/Character_009_Walk.png-6cd650d24ac756f6bfd224f999d84b8f.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/art/sprites/characters/Character_010_Dead.png b/art/sprites/characters/Character_010_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..296d4dba20c842b5fe21ff4764a373df707a2243 GIT binary patch literal 859 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 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_010_Idle.png.import b/art/sprites/characters/Character_010_Idle.png.import new file mode 100644 index 0000000..c1fe0e0 --- /dev/null +++ b/art/sprites/characters/Character_010_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cn7ar2h01prir" +path="res://.godot/imported/Character_010_Idle.png-dbb1408fdad0da2fd24a4c1fe75ae620.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_010_Idle.png" +dest_files=["res://.godot/imported/Character_010_Idle.png-dbb1408fdad0da2fd24a4c1fe75ae620.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/art/sprites/characters/Character_010_Walk.png b/art/sprites/characters/Character_010_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..870b58922c84bdc77b23ac1242a76b3f61eb59fc GIT binary patch literal 2992 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 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_010_Walk.png.import b/art/sprites/characters/Character_010_Walk.png.import new file mode 100644 index 0000000..80b445a --- /dev/null +++ b/art/sprites/characters/Character_010_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkfceshiwp5jt" +path="res://.godot/imported/Character_010_Walk.png-420aca922b9f619ed9216c39355318ab.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_010_Walk.png" +dest_files=["res://.godot/imported/Character_010_Walk.png-420aca922b9f619ed9216c39355318ab.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/art/sprites/characters/Character_011_Dead.png b/art/sprites/characters/Character_011_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..857cb8376bd7b6598c8e16e87b43ba740bff377a GIT binary patch literal 799 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV0z~1;uumf=j|LvujoJ#hkP&b zi<#3eh#q0JFsfmf?6}->hVN5*6UW@0OvW7+g5no=Tlz#*7JJFu(A~qAbV)<^PyRjg zKXZx|9{BtCO;E1%uh~91i0k2}%-i`6+qxMK7&2I}F&t-jAi*HR+>p#rz{Ai76@31= zdcK9+w@cgC$15p4dY&Wq_x<&`Iq$vxAO9KbwKadbI=iNCe$cPK*@d(3Ejc~8`OCcP zm-fg0fA)$0pW=)2uh(y7li;sjSzq$!{_)@L#|`DaN&RML{`>RqR;C^GHGRw5cON~P z^Xq%#^!4-Q-aY#FapyVjYfklb^-md29o}oA@h$$o_7MG z>#L9E-}ToQFWfH)bV`bOp*Z%?!5U;R!N)ZWyX;R3J{6nazx=oP^zDCYZ0^3VVc2p1 zCqu#h&x{Z1wl@~c;dl5R9;x~6)6a@saPjBN2S%iYKbfy=T(VzD>uMM<;WK!;`njxg HN@xNA7@uf( literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_011_Dead.png.import b/art/sprites/characters/Character_011_Dead.png.import new file mode 100644 index 0000000..7fd918d --- /dev/null +++ b/art/sprites/characters/Character_011_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://doa0vm7xo5f1b" +path="res://.godot/imported/Character_011_Dead.png-c52196730c14299395595c4ef3547c34.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_011_Dead.png" +dest_files=["res://.godot/imported/Character_011_Dead.png-c52196730c14299395595c4ef3547c34.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/art/sprites/characters/Character_011_Idle.png b/art/sprites/characters/Character_011_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..62eb9412e73645a8477ad0473670fbacff8bee61 GIT binary patch literal 2152 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 gA8+zMl((*}k~8j`d86sE6951J literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_011_Idle.png.import b/art/sprites/characters/Character_011_Idle.png.import new file mode 100644 index 0000000..9756dce --- /dev/null +++ b/art/sprites/characters/Character_011_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dgm07ylwoxuqr" +path="res://.godot/imported/Character_011_Idle.png-079aba020f914b64188e3e11000d6489.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_011_Idle.png" +dest_files=["res://.godot/imported/Character_011_Idle.png-079aba020f914b64188e3e11000d6489.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/art/sprites/characters/Character_011_Walk.png b/art/sprites/characters/Character_011_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..d18a9ea86c182085bd49578445de04fb39d971ec GIT binary patch literal 2639 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 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_011_Walk.png.import b/art/sprites/characters/Character_011_Walk.png.import new file mode 100644 index 0000000..82e49f4 --- /dev/null +++ b/art/sprites/characters/Character_011_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://caxnji7qtoftv" +path="res://.godot/imported/Character_011_Walk.png-e30d6c424901068d6cb1d62875371473.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_011_Walk.png" +dest_files=["res://.godot/imported/Character_011_Walk.png-e30d6c424901068d6cb1d62875371473.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/art/sprites/characters/Character_012_Dead.png b/art/sprites/characters/Character_012_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..aed9b748d44b0e8ce059e80642351a54071f3511 GIT binary patch literal 866 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 Ofy?H$W|gKMzx@pnU?X_| literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_012_Idle.png.import b/art/sprites/characters/Character_012_Idle.png.import new file mode 100644 index 0000000..3c8e414 --- /dev/null +++ b/art/sprites/characters/Character_012_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://y6bkvxi65e7v" +path="res://.godot/imported/Character_012_Idle.png-06d2acb1777ac4e3d84cc727341dcae5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_012_Idle.png" +dest_files=["res://.godot/imported/Character_012_Idle.png-06d2acb1777ac4e3d84cc727341dcae5.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/art/sprites/characters/Character_012_Walk.png b/art/sprites/characters/Character_012_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..da7ea2254c9f309b0bc3bd692b6e1b80590e0d1b GIT binary patch literal 3163 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 X60AMze$E8^X#k8bn_eov=otMU1W)>R literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_012_Walk.png.import b/art/sprites/characters/Character_012_Walk.png.import new file mode 100644 index 0000000..22ed979 --- /dev/null +++ b/art/sprites/characters/Character_012_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bsekmrv5pihek" +path="res://.godot/imported/Character_012_Walk.png-716625a565eca83c3c36b0419ddd28f1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_012_Walk.png" +dest_files=["res://.godot/imported/Character_012_Walk.png-716625a565eca83c3c36b0419ddd28f1.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/art/sprites/characters/Character_013_Dead.png b/art/sprites/characters/Character_013_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..b2f06dc4c7e40101b9bd757e0cc8407b0f5210da GIT binary patch literal 893 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV0Q6zaSW-L^LEZzuMkECH+yAq zLlM#5dJkqc->sPwCq2-rVN^V^w%W03$~HSErwiO2s}BSuW(7*BNGuXfT-0&;w$rfy zp0r1CE$1v1UR6BhDcbY+%+&SEHM%&adyChyKS?rVuwY|2&hS8jL58^@nW2D(p^xzZ zSn&C${WX_8a<@pVxqI@e_Vvjs?C#Y!E&ojFJ$!Xu(EGXnPW`pL8^<&2#cVw(^LO)q zZ#^3?|M}VR(_Zx)zfONG7jf9VL#47N-{}6n{Q1>)9R3H~tBdlP6jZPLCH(pOA3O#@ z(z@^NRZUT;ykJpxf1fh*UuVxr{Ijz?C!M#y?qx6gYW=bMSDg>I+%LIUcju=x(C(>o z*}hJH|Ncef0idI10Ihx18+Pyf+2;>3-p73Wz4PZ4>9syze8rWwUcB`0vP*sGzdoiL z7BO#Enny|I^Qn8LrB@XfY$<=0Tei&H)99Xz-`*ClW;_jb1w>u>*` zNqzBb>z<0OGd?~m$m8doXZ7s$H@|7uw}1NcGG$x&_stu&OuDZ9dg&atuRxE#;@Plw z_xvY+PTktRwSOhk->1K=>!d79ujx#h6LR(6p0hV^pI#MTzxCg`c&=&xx!1ED!W}$Iudb(sQ@hf!fg92 Zz4B46kJ*d8{J?C*;OXk;vd$@?2>=IFmn8rI literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_013_Dead.png.import b/art/sprites/characters/Character_013_Dead.png.import new file mode 100644 index 0000000..63596dc --- /dev/null +++ b/art/sprites/characters/Character_013_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://daukgomwycsgk" +path="res://.godot/imported/Character_013_Dead.png-49b06f46b7fa82025b0265b91dca90b2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_013_Dead.png" +dest_files=["res://.godot/imported/Character_013_Dead.png-49b06f46b7fa82025b0265b91dca90b2.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/art/sprites/characters/Character_013_Idle.png b/art/sprites/characters/Character_013_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..090662821b9f11e90a0878140a514fa64c7f4b9d GIT binary patch literal 2732 zcma);dpy)xAIHzkXj}#pDp@O1k{YHb*>xvObY+DRG2~Vhv#eW&87ieIvSOmfZIg)H z);)tv<1#T3Nm}E63u7b(V?4i}XP^I`KlYE``Ml2Sbvrz|QK8`hmL+M8{mIdCS~NdHFMc^%mqhniaX^H~U%7_M(qyd-lY|JNKq(#KCeB zuaq8;-)BLL52ksey5pwK7KW`hoAkH-d@h*RX<(`~6}{hzloU;#YM83sn%iKs`ehuj zS)Y~cQ&B6}sbn6K3_GUyU+GVAH~q6Rw_VzaB>tr%3kERWQ=ZbTq~~)U_8L(f}j>|e*RrXkk?s>pIJIE z$~&3d49@9WFdr6AAW;gf@VRO)97LEt*~kj@h6Vwm5gmZfYt|M57QUT;LO$Ht_~N{I zfSP}K%aFZGTi0nL=@;WKhG4*eUvIcg(6Ub;?Azf_R=U-&4>b^ZMpz`y-EdR!eQ)&i z3EY97?x1AnD@+VT@O0@m54?Dsw`8x1!iEmBFi*+!&_G`y3wPuXXbpeGpMSJ2No1{R8}mP)0odB;%X;T2Qph2+7>cuJEx!@bF65#3&K!^9 zgX0flUo_T?8^+*~i=p#+1pQ?##%7pG*oSL}E-RlhY2upNj7-)x9P!GMLJ##<_A^Bo z_k&yPCE*;k=~l2-v|R(D?V~7BrU^-zY1*jRHa}&%{uL{RrqCPI371r%tNySO?hOe2 z?h2X0|m!c5;#hg2HH#!79ODakfnI^kg*rzhKOW+w} zI1mdsTNvI3>{h66sRj;phyTw*aVvdkJ#* z;l9fT3`{{HVHfw?S5Tw?-ldB^;^!)_$`O9#roe2HlBUzmJ8{T&7rnthk%{R@O7w`1 z0$6`v7MG>$is%-$D5djpSqiB?9w$YT@MhvXRCxO4%%dPe4ZBE(Z9+3$&H6lMO{#lX z)O&T5I#nODhjJTH`_?&ar|Vq{Nv0w@JX*|y7K+~=Kr3~HQH{?GruUot%N}){q|}@cF|o2XBaF|qyLb}bbk3;#+*we^Ji$5t}O4$ zuGin+QtQHrul0|=7PL(zy8oV$VoRze*(U5&saGyW)1w2T896tVV+EH4nf}|du03y` z$RQjQe^5D`C(nPwH&LR@v`ruT)HomkYVE z>kc-Nw12L6e#yAgZe_%@HHpyxO_*i_DfBa_w|t{0_` z_6g}EK9wlC3p}ne^Xqm@WSa&YxJs zze0LUsqX3ONsfID-*(iBR4d%cgU{9al60;wITv{tJm7dLYK*ccjOt`w(UeyBj?bYy zb241A2-^cW4Awq}cB_k)NDN*z^wzCzyl` z^mqWWCe;gZkv_v82QhQpN?jprpUSIsCo9T3dluEKiIzOZ_$eU7G+YgUY$Ld7}k2uHuYGx)^~^rpq`BcPWK(8-68XpAv4qABxZE7CBp}e3UtmgE$@PQl z{mr?5AhdjOi;u6qVa)F5X|!izgNhd&PkRerl4S#BtU8H~mfcm$Ex*j%$j;!bgXjOs zo));#;Z;t@(zU-`J6%=XTfMW`ckijfHS-wo+EM1Y4l1_}Be9a9WSQp8{GK!QLzyXE z93N~zkX@&EFk&(IX+`!yM9F=%p41#lsBnCrb7s}JRR-YO!~Gmfg!j*by%`> zTUh3&>PLi?`BcA`%(;PXqjLMMWhX1O-YqtS>ta%W&!wv<|` z>_wAWfD0xteuO`vjei8~gI|i&x4n%HQ-s%AAlnDWsQA)T1(OJWnS+O8TJtGU>7J_f zrDckThG}k4>9p5KF`z)HG|qHv12v zs5=<50-r^QE6A+{@b&emf>5P&+pwWNei6}?&&Y9?^Pn%dp zzWk$HctvZ%#o-MEm^L5;e|pg>KR=2P0}>ycT-eveN9~c6;v~*pZ5EH3e-r%*T4PB? z6()e$EKf0}ZT3UyEC*Otf$8^Pr2+p#QAn(WS{5yktnJEFY|USXvM5us_x}@UOME@y W&|24}w2RPvfs?p%Rxd3);{FYCu{GEL literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_013_Idle.png.import b/art/sprites/characters/Character_013_Idle.png.import new file mode 100644 index 0000000..5cbe2bd --- /dev/null +++ b/art/sprites/characters/Character_013_Idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddkfrmcwb11bp" +path="res://.godot/imported/Character_013_Idle.png-7a5d4c80884c699f3b7a04bcfdddcc2b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_013_Idle.png" +dest_files=["res://.godot/imported/Character_013_Idle.png-7a5d4c80884c699f3b7a04bcfdddcc2b.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/art/sprites/characters/Character_013_Walk.png b/art/sprites/characters/Character_013_Walk.png new file mode 100644 index 0000000000000000000000000000000000000000..692f7cd3adc7a5fa00ec83ea2d2a34d7839e860d GIT binary patch literal 3211 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 T&2zv*24HjgycPbGd(1xpgq1VR literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_013_Walk.png.import b/art/sprites/characters/Character_013_Walk.png.import new file mode 100644 index 0000000..58876e9 --- /dev/null +++ b/art/sprites/characters/Character_013_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://be4twnnlpy6m2" +path="res://.godot/imported/Character_013_Walk.png-03bdbadd3a5e3963af2bea56f7d34b60.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_013_Walk.png" +dest_files=["res://.godot/imported/Character_013_Walk.png-03bdbadd3a5e3963af2bea56f7d34b60.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/art/sprites/characters/Character_014_Dead.png b/art/sprites/characters/Character_014_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d8ae2c63db3b098942803471edb1a69c5284b0 GIT binary patch literal 859 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU{>;UaSW-L^LCD5wlE`u+kOxA z`i53#E-}XmmktIk+Tkig26cT+64bOg+?vVJjXWkUMFAa>3s1ziu$7yJc6 z=M?ukdAA<96Zm28_9<%D_buKN=ft-!EOMHl-t~3ASe|4VGFY%N9A|hS!63ujkjzlP z!_dcg04$jPF1{|~LgrNM^1LMxyJo6L^7ttHd9wZU>-T$ZymR`+ z=Vv>guRdOP*7n@Fdyc=F`M3TPf02L8{_=B%qec2hi}I(%8w&qQ|L!k(04P6c$?mPQ z|NVP(^YP!mM@zPsotM4L^6T|?|1Uxpa@SA&EABrjTKlD4{j_txvhwc7T9u#Ku7CLB z(&tZCKlxEmGs!fT?W_6o`R^tlczjsp@hL0uc~;u*zszvE5Wl;Am3gD*xmBz7t>5vZ zLO8$a%k}s3e=Sb8ntl;axdtxUZb~A&MEmG+NJ4WR>$*>9SKjFw-!2y85}Sb4q9e0C04F As{jB1 literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_014_Dead.png.import b/art/sprites/characters/Character_014_Dead.png.import new file mode 100644 index 0000000..1a34f95 --- /dev/null +++ b/art/sprites/characters/Character_014_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cvwe1xf2f73y3" +path="res://.godot/imported/Character_014_Dead.png-23c4d06b6e3863989e695afff750ed5c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_014_Dead.png" +dest_files=["res://.godot/imported/Character_014_Dead.png-23c4d06b6e3863989e695afff750ed5c.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/art/sprites/characters/Character_014_Idle.png b/art/sprites/characters/Character_014_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..89b99a813ff856aa7da865899ab68cd36585d52f GIT binary patch literal 2528 zcmb7`X;>229>x!lVkx4jZDx=)Ic09B<5&n3nF}{FqpY-x+EnUTTJB4LI*oT+QnSz& zLe1UAB{Oq0)YP6vSOZQo&G%d7k_6ewh2=ocF{3dCqyx|98&&p5I(u91xp! zYytovoE(q2DV6^BP*YXT%{Xrk02>RPjvXOKJf35o^xgTVL0iL2!M>t&bNw&5znOeU z+1cuIP379#0w1XK0@Aiy?MSW({uCDQ1ycsC?&pT?0a#J8N{<2H1593 zA@7^sbjoZ?Ns888jE{=;ZcJ_uW6JyMamzh({#TY(mu#sGW@>(GzIgF6FMMsTLu_Ng zdEVIPUP!i~=bsG#ovo@pp8plIWAehjhz{p)*6s8|aA0oi@qPI`+VXO1-pl#zJ+~YUegg6wHM-oV&}DACPy)L5jzap=##p8%eWE z{$-~J4L>1Dg{3?I9?FOY&;_ZJuj~!_&*e+9UOYOuCQISy zIE>kto&;(rTAmoPkgrm>8+uS=P3#nv{@`U&D@ZMARBgZL(flVVl{q!qa3*#2+f062 z*LCNDzYkUkc_rvR!3))Z@0?vS5<``%OlA-Om*&0UqTT-P=SHaqeC1*dkoIIl$ygGR zFh8^-IY950O>F*h5}>p3DLzx>^z)95)tXNFpjMKAp=+mlZ0%;X=^L1Aqm_P;wFLxyO2 zwFm~h0?VbN!1dNj?nK+`fQp)}$FZi1r=BiFV9hDdj7e* zHF>3{&b=<~?DmQ;f43FB;-Pns{^fs;){pm%<%iFmDXpFOKos&EP18ph1R|$A}4%x?6bD z0YRY+6yZv|a#{WO z^y5NpdW!%pb&}BkHhFy#;cvS8-3W#ZN(P* z6^mzj<1B8CB*j zYT9?+1t;~Br*9@I3tjk5EH^}CQ>Bw-mVkN(a?drr7wtRN3r&B-OV;#KwmHwI_b5xT zX0y$pRi;fe~ z5!6Ug=LzC8Ki+iBH8WQE5VODZN<12y2D}T+jZ#fB(f1X@%zkB%#Q(U_^5UEp3@w_N z$Y@781wx$^u?X{^`PkEWgje0-qXb<{gw`tvUJsW&KqpHaR(56A{~;(o2jMa^=5eGb zlK`c$<8f=Y(-PWZ+j`nwzm=J!Zh+)vd$y8|?t-*_9g22o%pBqH%Ts{I|VS%b}hQ*?xI!88kU! zc*pEqC~g?3OG~;O|N6DduDXN2(c{0YGW(0jZ71I$LpFcE(96P>@M}=(ay9PnQ+S1@ zGWB$?QniIoVegp8z+pv&DUG@S1L)DxJFGa=G^V1|=yvQ&OUIfQP@p@2L@hZ0vkr`2hLWEqygbd2iJ#vXu3Jv3u%M7C=x1>Qr3|*uo_XZg= zE*Y;&>2*o2V;E9ygK`_=jJ3`>=Ur!=v(6v;*?aBh`R)Dey*}UH_ZR1YMu`Z@3IYH` zY^*I$^LoNo+x7?V*=XwC27pkNjpebkAx~#qT&xk3vJrX*@?!-DU8_!u>5elUEUtdc zG>I>gi5qb2MjXC{Hjp-Hua=?uUvo~$@Q>)7Ti=sB{fV5hjQ@?F7+4J-X4?>9qQRwY zKVb&7{L)~I0lkcH28m-D#XC#xoE%g+-nhQk@+Jg$9C|q2F5!VEeje*~j>_iX&nH1^ z)d_v@S1pT%mVt|lO)2EHbp~; zJG?1vf0FOylPh?lrlCXwoh}MVwjqZ6r~>osQVZ*(XdhC<4Hm{4xJn}*dR~(R3KdI8 zucE@7#D#O8E#b ziS2#kst?CWmN$MkKGlR)DB{@7!JxE4veB15#FxYlG|PdFq7vVZrezWs%O~uDf()G8 zqE^1?bzB_jlu^#1G%Phho6@9E%&wM^RKUAm-dC@jE}gpQ2my$q;?6|-qv0=WgS7=T zSM_^(4$P66@=NRN`E)no$;&HF1f!_9>&2b=y63f{@@tH?MNr4P&&w>WA2wW;Oo*V~ zvg9uGxDm5erMb~#%r^3ow)6`^Ufbn#DW(?d!1Jf#c0k*jj{H%2rl?4a6M?#Qq$YAq zLRsE~7S|Y_cGdH|pCGG=gzQ*t8l^nJErDf>vo>Xqg%0iIZ!Mx*i<-3QMgkG(hgUnR zqJv#G0t)`?)clmtGotKr)VRKVIUuYrAB2E!vl}62SLC98$l^j-`w+)aK^^(9Zhi$)CH&9!0!{IC z>xMA@dy^j?O{daj4{jn+^Pr4h9B*;O`kK`YDPo6J)GJ2|j=z>5<6Y6H6c-rX5zumT zZhZdfnRbrY+)Y#OKuXMQsLgys%5kNnO82^|nALPFR3z=3SHT-AEKayy5z=MzT!P`Xt;PaPB^cH??V8itlwjMLkzP7(B3u#4-^w@nL~)NGu_kk~VO zS_d?h&x?t%0f;5Sh-9ll6_645x6bYg5&ZoiTB3(Qb3H%p?=Ov?o6-Ev)_DXe7gqOM z&Hlyjsgu8_dr8PInOa&H#X3zGEr`}}l#h8XA=>tp!7jwvx%z%B8rAoA<(mL zlw8n{l$@_V z6sB%mJ|0qNkhyZnNL&do#Axr`Zt$A>?e){?2a#2!>O*FP0K2j)Ee}%6blLhrlF4;p zXrmyd7wqPQ6n!m&p?oFAQZEXt5VGPay75pNF(Z`w&~9gB*Cg*)1S5fknoahdvOzm> zvFSkWgSd!8NXmZcon*S#2>) zZI%K%Zol!68N=4Ocy**E1mTiB2?4LDD*{^!=;Q@AX8S+~Y7n@eO#j5ex}-+~U;TeF z*)wljC7a@caleMwt)sI_Yu7x6l4eY6nLCi9C*v_vkFNp$XL8?W1wGmFAN6+txm}W8 zyW;X=18d)l(qw@GbKu_6$fh7rMZaTWEv&KqFQ)cKGSSLU73&`O8pukXTqG+I+Elpr zgzh)=U5K7&yS;N!Ga+8?lJB=*J{FB?P<>Mx`Wr52GPQJr`GRI5&{~gFa@ePQr>5l`jC7RUF(6F&>Z_)v7izCQ7#tPM z#YplJu7Y7fmR4MKEwfoFAXI5~`jJ3y(LgZ$JoVKBYThDn-9O!Fs882kA-)Pd>Y>N7zU7I2`a9k9c3i}p)In#`- zG`w?h3qU&zObuGmt6<)ft1x7iJ4GCo6}95Q1C8@5=cq29qaWa#Y zDH(*<|0V43)cIU|31&9(30;<`a&3O7Hm%Yzq4;;RqH1nlS!*_9KF16UgehK(z#SDC zUYQw33T^rxiB%}R{YF2+IQO2}+zy(-ie29SfaRYh#NS9NRb1oyer&dVKi7cS>c1A} z!h#yFUaFD(eihO*Oi;ot9GfbYj|kQfAc%4w;~=h53Oko=O6yX4iA7d;;f$4YDEUEa zc;uP&t%6&u~-Hp_l znKEH3WsIg;bh$g7E{;^g)M{HTSfu+JcD9b>f?7>5_2D_Lz6PPEL9VocQ8*%8Sm3JG z(|*afXQ9}#1AVr8;19idn<)e?doicd)q;HokeZQ=_oL#B!?y~N1-R<(;m&FP!gLjs zx%Dk};LMDIaXGAjd9$Q0s8w{hbAs)A=R7z66JLlUA*r7pk0KSgQKdi=eY5ZP3drVY zkmJJT%5+X7$;HP}AP0D{_(Pr@VeXLEYASYSt;3zC0K4%sn&row zz}rE?Uc7s1c_jcg#ihigXx(j|*mkJ${@$Y_50V%VKzrUW2MEX@k-A!}1kZQhH>(S9 z)B78Ff85wnEFfN#oZWeoiyCP3@=1E)TKu|^#)~(4<87-9TKG$d(c(Ng;in2GU+iX& zh~>=nHu)C3HMcl-_f33}7^$uE<3y7w(Ry%|nJFO+qIr&Ag_m}BC%s1Rjai1Y zq`QTb#Wt1dRUc?<+edf+Rie|yj!I}{M>`f|R|DkResq6D+dXQ3W{!r}rLG!}9hzwb zIDg0)G{IPJ2Ft*xxV4Gw>)u{9lw0rJ+4O%hBuAdOTzAIpm|i`vlm|A)(U$MbFW>nG D>qx|; literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_014_Walk.png.import b/art/sprites/characters/Character_014_Walk.png.import new file mode 100644 index 0000000..7121bd1 --- /dev/null +++ b/art/sprites/characters/Character_014_Walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vpdry2yyekvo" +path="res://.godot/imported/Character_014_Walk.png-bab0cfac7c66a94dcc7be5d5366bb948.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_014_Walk.png" +dest_files=["res://.godot/imported/Character_014_Walk.png-bab0cfac7c66a94dcc7be5d5366bb948.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/art/sprites/characters/Character_015_Dead.png b/art/sprites/characters/Character_015_Dead.png new file mode 100644 index 0000000000000000000000000000000000000000..da5abe1dc2f9cbd9a01292debee871d222effea4 GIT binary patch literal 845 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU>5OoaSW-L^LCDXws4_L`~KzU z`0{LX`-A@o$~>kH-|iZk>> z42v|&<$l=kx0t=$>aXGd|LH9MX+c~MKP`A`t?)L9p@4^>kMV#Zg9RJIafSyH3^L3O z$soaVi+}IATs38B^K-$U-LqdGReJO(`^dw{Q?K?E|C;?h{r5Z`Ew#u~_uo|9-rezM z$xZ*Je^q~{GHG;ocbxE7di1-GdA_U2Tzoa+*d-mshmCS?F zhabN^9Tao#oBoCP`St4=&Y6n~3L4JUO}2{8J6gVe`f-!Sf4}}tt>d(L_5IT8>3>hm zx9PjwKR@}x>m!%{JeZzbCo;Wo-_8!8O9NwKkC$xg@jvaWd!OyA`Sbg)Cmwk8%1Y^9 zOk`zrrH}B7_4ng{okV;B6* zP_Q3@OX_c*_rLz$uHm29-*?Oh@Zpa+3b5$IZYpL(-(hd~eHV-u{JvdRx2x_uu&x9`(2@4gn1Q%wvpyQNt8gVesbRZ Xv@C=5@f2lXHev8|^>bP0l+XkKd`XJg literal 0 HcmV?d00001 diff --git a/art/sprites/characters/Character_015_Dead.png.import b/art/sprites/characters/Character_015_Dead.png.import new file mode 100644 index 0000000..42fc6aa --- /dev/null +++ b/art/sprites/characters/Character_015_Dead.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://n71lbmxkvioh" +path="res://.godot/imported/Character_015_Dead.png-b074949bcb971d96a392e2128c495ea5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/sprites/characters/Character_015_Dead.png" +dest_files=["res://.godot/imported/Character_015_Dead.png-b074949bcb971d96a392e2128c495ea5.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/art/sprites/characters/Character_015_Idle.png b/art/sprites/characters/Character_015_Idle.png new file mode 100644 index 0000000000000000000000000000000000000000..f099ff362919278a09c56f6cecd4d3cc8f5d05b9 GIT binary patch literal 2639 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 void: if EventBus.has_signal("corpse_cremated"): EventBus.corpse_cremated.connect(_on_corpse_cremated) + # Sprite mount is deferred to setup() / from_dict() because the atlas pick + # depends on pawn_name (deterministic name-hash), which isn't assigned yet + # when _ready() fires. See _mount_sprite() below. + func setup(p_name: String, start_tile: Vector2i) -> void: pawn_name = p_name @@ -215,9 +232,31 @@ func setup(p_name: String, start_tile: Vector2i) -> void: # Same formula as _draw() body disc: deterministic hue from name hash. var hue := float(pawn_name.hash() % 360) / 360.0 portrait_color = Color.from_hsv(hue, 0.7, 0.85) + # Slice-1 character sprite: depends on pawn_name, so mount here (not _ready). + _mount_sprite() Audit.log("pawn", "%s spawned at %s" % [pawn_name, start_tile]) +## Build the AnimatedSprite2D child from the peasant atlas trio picked +## deterministically from pawn_name. Idempotent — safe to call from setup() +## AND from_dict() (the save-load path also re-enters setup). +func _mount_sprite() -> void: + if _sprite != null: + _sprite.queue_free() + _sprite = null + var atlases := _atlas_for_pawn(self) + var sf: SpriteFrames = _PAWN_SPRITE_FRAMES.build(atlases) + _sprite = AnimatedSprite2D.new() + _sprite.name = "Sprite" + _sprite.sprite_frames = sf + _sprite.centered = true + _sprite.offset = Vector2(0, -8) # bottom-anchor: feet ≈ tile bottom edge + _sprite.z_index = -1 + _sprite.play(&"idle_down") + add_child(_sprite) + Audit.log("pawn_sprite", "%s → atlas idx %d" % [pawn_name, (absi(pawn_name.hash()) % _PEASANT_COUNT) + 1]) + + # ── public API ────────────────────────────────────────────────────────────── func walk_along_path(new_path: Array[Vector2i]) -> void: @@ -919,6 +958,8 @@ func to_dict() -> Dictionary: # Phase 14 — bleed-out timeout counter. Default 0 for pre-Phase-14 saves. "bleed_ticks": _bleed_ticks, "last_damage_source": String(_last_damage_source), + "facing_x": facing.x, + "facing_y": facing.y, # Phase 17 — per-pawn work-priority matrix. Keys stored as plain Strings for # JSON round-trip safety (StringName keys survive the cast back via StringName()). "work_priorities": _serialise_work_priorities(), @@ -928,6 +969,9 @@ func to_dict() -> Dictionary: func from_dict(d: Dictionary) -> void: pawn_name = d.get("name", "") tile = Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))) + facing = Vector2i(int(d.get("facing_x", 0)), int(d.get("facing_y", 1))) + # Re-mount sprite now that pawn_name is set (atlas pick is name-hash driven). + _mount_sprite() _path.clear() for entry in d.get("path", []): @@ -1070,6 +1114,9 @@ func _advance_walk() -> void: job_runner.cancel_job() Audit.log("pawn", "%s walk aborted: %s became impassable" % [pawn_name, next_tile]) return + var delta := next_tile - tile + if delta != Vector2i.ZERO: + facing = _canonical_facing(delta) tile = next_tile _path.remove_at(0) _step_progress = 0.0 @@ -1087,28 +1134,42 @@ func _process(_delta: float) -> void: var next := _path[0] if is_walking() else tile var to_world := _tile_to_world(next) position = from_world.lerp(to_world, _step_progress) + _update_anim() + + +## Pick the right animation each tick based on walk state, facing, and downed. +## Called from _process(). Cheap: only calls _sprite.play() when the target +## animation differs from the current one (AnimatedSprite2D restarts from +## frame 0 on every play() call, so we must guard). +func _update_anim() -> void: + if _sprite == null: + return + if is_downed(): + if _sprite.animation != &"dead": + _sprite.play(&"dead") + return + var prefix: StringName = &"walk" if is_walking() else &"idle" + var dir_suffix: StringName = _facing_suffix() + var target: StringName = StringName("%s_%s" % [prefix, dir_suffix]) + if _sprite.animation != target: + _sprite.play(target) + + +## Map `facing` Vector2i to the animation-name suffix (down/left/right/up). +func _facing_suffix() -> StringName: + if facing == Vector2i(0, -1): + return &"up" + if facing == Vector2i(-1, 0): + return &"left" + if facing == Vector2i(1, 0): + return &"right" + return &"down" func _draw() -> void: - # Phase 14 — use the stored portrait_color (computed once in setup()/from_dict()). - # This is the same formula as the old inline hue derivation; consolidating here - # removes the duplication and ensures the corpse head-dot matches exactly. - var body_colour := portrait_color - - if is_downed(): - # Phase 9 — Downed pawn: rotated 90° (lying on ground) + desaturated. - # draw_set_transform applies to all subsequent draw_* calls in this _draw. - draw_set_transform(Vector2.ZERO, PI / 2.0, Vector2.ONE) - draw_circle(Vector2.ZERO, 6.0, body_colour.lerp(Color(0.5, 0.5, 0.5), 0.6)) - draw_arc(Vector2.ZERO, 7.0, 0.0, TAU, 24, Color(0.0, 0.0, 0.0, 0.4), 1.0) - # Reset transform so selection ring and carry indicator render upright. - draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE) - else: - draw_circle(Vector2.ZERO, 6.0, body_colour) - # Dark outline ring. - draw_arc(Vector2.ZERO, 7.0, 0.0, TAU, 24, Color(0.0, 0.0, 0.0, 0.6), 1.0) - - # Selection ring — drawn after body regardless of downed state. + # Body is the AnimatedSprite2D child (see _ready). _draw() is now overlay-only. + # Selection ring — drawn on top of the sprite (parent _draw runs after the + # child's z_index=-1 sprite draws). if _selected: draw_arc(Vector2.ZERO, 10.0, 0.0, TAU, 32, Color(1.0, 0.9, 0.2, 0.85), 2.0) @@ -1127,3 +1188,30 @@ func _tile_to_world(t: Vector2i) -> Vector2: t.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, t.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0 ) + + +## Maps any tile-delta to a cardinal Vector2i facing direction. Prefers the +## axis with larger absolute magnitude; ties favor horizontal. Returns down +## (0, 1) for zero delta as a safe default. +static func _canonical_facing(delta: Vector2i) -> Vector2i: + if delta == Vector2i.ZERO: + return Vector2i(0, 1) + if abs(delta.x) >= abs(delta.y): + return Vector2i(sign(delta.x), 0) if delta.x != 0 else Vector2i(0, 1) + return Vector2i(0, sign(delta.y)) + + +## Returns the {idle, walk, dead} atlas trio for a pawn. Slice 1: always +## peasant, picked deterministically from name hash (mod 15, +1 for 001-015 +## naming). Slice 2 will branch on equipped armor (helm + cuirass + boots → +## knight atlas, etc.) at this single extension point. +const _PEASANT_COUNT: int = 15 + +static func _atlas_for_pawn(pawn) -> Dictionary: + var idx: int = (absi(pawn.pawn_name.hash()) % _PEASANT_COUNT) + 1 + var n: String = "%03d" % idx + return { + "idle": load("res://art/sprites/characters/Character_%s_Idle.png" % n), + "walk": load("res://art/sprites/characters/Character_%s_Walk.png" % n), + "dead": load("res://art/sprites/characters/Character_%s_Dead.png" % n), + } diff --git a/scenes/pawn/pawn_sprite_frames.gd b/scenes/pawn/pawn_sprite_frames.gd new file mode 100644 index 0000000..ca5593d --- /dev/null +++ b/scenes/pawn/pawn_sprite_frames.gd @@ -0,0 +1,51 @@ +class_name PawnSpriteFrames extends RefCounted +## Builds a SpriteFrames resource from a {idle, walk, dead} atlas trio for a +## peasant character. Atlases are 128×128 with 4 rows (down/left/right/up) +## × 4 frames (32×32 cells). Idle + Walk produce 4 directional animations +## each (loop=true); Dead is a single frame (loop=false) from row 0. +## +## Created via PawnSpriteFrames.build(atlases) from Pawn._ready(). The +## returned SpriteFrames is assigned to an AnimatedSprite2D's sprite_frames. + +const CELL: int = 32 +const DIRS: Array[StringName] = [&"down", &"left", &"right", &"up"] + +## Build a SpriteFrames containing: +## idle_down/left/right/up — 4 frames each, loop, 4 fps +## walk_down/left/right/up — 4 frames each, loop, 8 fps +## dead — 1 frame, no loop (from idle/dead row 0 col 0) +## +## `atlases` is a Dictionary with three Texture2D values keyed by "idle", +## "walk", "dead". Each texture is 128×128. +static func build(atlases: Dictionary) -> SpriteFrames: + var sf := SpriteFrames.new() + # AnimatedSprite2D auto-creates a `default` animation; remove it so the + # scene doesn't render an empty placeholder if a caller mistypes an anim name. + if sf.has_animation(&"default"): + sf.remove_animation(&"default") + + _add_directional(sf, &"idle", atlases["idle"], true, 4.0) + _add_directional(sf, &"walk", atlases["walk"], true, 8.0) + + # Dead — single 32×32 frame from row 0 (down-facing) of the dead atlas. + sf.add_animation(&"dead") + sf.set_animation_loop(&"dead", false) + var dead_at := AtlasTexture.new() + dead_at.atlas = atlases["dead"] + dead_at.region = Rect2(0, 0, CELL, CELL) + sf.add_frame(&"dead", dead_at) + + return sf + + +static func _add_directional(sf: SpriteFrames, prefix: StringName, tex: Texture2D, loop: bool, fps: float) -> void: + for row in 4: + var anim_name := StringName("%s_%s" % [prefix, DIRS[row]]) + sf.add_animation(anim_name) + sf.set_animation_loop(anim_name, loop) + sf.set_animation_speed(anim_name, fps) + for col in 4: + var at := AtlasTexture.new() + at.atlas = tex + at.region = Rect2(col * CELL, row * CELL, CELL, CELL) + sf.add_frame(anim_name, at) diff --git a/scenes/pawn/pawn_sprite_frames.gd.uid b/scenes/pawn/pawn_sprite_frames.gd.uid new file mode 100644 index 0000000..42721ff --- /dev/null +++ b/scenes/pawn/pawn_sprite_frames.gd.uid @@ -0,0 +1 @@ +uid://c4otxk5jg0kxr diff --git a/scenes/ui/workbench_panel.gd.uid b/scenes/ui/workbench_panel.gd.uid new file mode 100644 index 0000000..5c26183 --- /dev/null +++ b/scenes/ui/workbench_panel.gd.uid @@ -0,0 +1 @@ +uid://bsdc4o12x1v41 From 326fd84b90b424cd42fe075483532ee02f3c9065 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 15:23:51 +0100 Subject: [PATCH 10/19] memory.md: pawn reskin Slice 1 session log + 2 patterns --- memory.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/memory.md b/memory.md index 7f7157e..ec0ba97 100644 --- a/memory.md +++ b/memory.md @@ -305,6 +305,11 @@ Same scope as locked in `~/claude/ideas/rimlike/plan.md`. Realistic timeline 3 - **Pattern recorded — "main.gd typed-var pattern requires CanvasLayer.new() + set_script(), not SCRIPT.new()".** First mount attempt used `WORKBENCH_PANEL_SCRIPT.new()` — Godot 4's parser refused with "Cannot infer the type" because `Script.new()` returns generic `Object`. Switched to `var x := CanvasLayer.new(); x.set_script(SCRIPT)` matching the rest of main.gd. Cheap parse error to surface via `--headless --quit`, but worth noting: subagents writing UI mount glue need this idiom explicitly. - **Pattern recorded — "never free a widget from within its own signal callback".** Bill editor crashed when user changed mode FOREVER → UNTIL_N. Root cause: OptionButton.item_selected lambda called `_populate_bills()` directly, which clears + rebuilds all bill rows — freeing the very OptionButton whose signal was still emitting. Same pattern in the Remove button. Fix: `call_deferred("_populate_bills")` so the rebuild runs on the next idle frame after the signal frame completes. Commit `4e09dea`. Applies to any UI where a child Control's signal handler mutates a parent container — always defer rebuilds. +- **Pawn reskin Slice 1 shipped** (commit `b4c9541`). Pawns now render as AnimatedSprite2D children sourced from ElvGames Farming Characters Pack (Pack 1, chars 001-015). 9 anims per pawn: idle×4dirs + walk×4dirs + dead. Atlas pick is `(absi(pawn_name.hash()) % 15) + 1` — Bram=004, Cora=013, Edda=001, all visually distinct. New `_atlas_for_pawn(pawn)` helper is the **single extension point** for the future Slice 2 (armor → atlas swap when equipment+combat phase ships). Slice 1 plan archived to user-memory at `~/.claude/projects/-mnt-d-godot-rimlike/memory/plan_pawn_reskin_slice1.md`. 45 PNGs + .import companions copied to `art/sprites/characters/`. New `scenes/pawn/pawn_sprite_frames.gd` builds the SpriteFrames from a {idle, walk, dead} atlas trio. Code-level verification clean; visual MCP verification still pending (needs editor running). +- **Pattern recorded — "class_name lookups aren't reliable during cold-start parse; use preload() for sibling scripts".** First attempt referenced `PawnSpriteFrames.build()` in pawn.gd via the global class_name registry. Headless parse failed with "Identifier 'PawnSpriteFrames' not declared in the current scope" because pawn.gd was loaded before pawn_sprite_frames.gd registered its class_name globally. Fix: `const _PAWN_SPRITE_FRAMES = preload("res://scenes/pawn/pawn_sprite_frames.gd")` at the top of pawn.gd, then call `_PAWN_SPRITE_FRAMES.build(atlases)`. Class_name is fine for public API documentation; preload is the reliable runtime resolver. +- **Pattern recorded — "deferred-init data must be wired AFTER setup(), not in _ready()".** First sprite mount happened in `_ready()` and read `pawn_name` — but pawn_name is empty at _ready time (assigned later in setup()). All three pawns got atlas idx 1 (hash of empty string). Fix: moved sprite mount to `_mount_sprite()` called from setup() AND from_dict(), both of which assign pawn_name first. Idempotent (frees prior sprite). Same shape will recur for any future render that depends on per-instance saved state — always check whether the data the renderer reads is available at _ready vs after setup/from_dict. +- **Delegation report — pawn reskin Slice 1.** `quick-edit` (Haiku, 1 dispatch) handled the mechanical edits across pawn.gd (facing field, save/load, `_canonical_facing`, `_atlas_for_pawn`). `gdscript-refactor` (Sonnet, 1 dispatch) wrote the new `pawn_sprite_frames.gd` helper (~50 LOC) and wired the AnimatedSprite2D into Pawn._ready (z_index, anim switching, _facing_suffix). Opus handled the asset copy + headless --import, the two parse/runtime fixes (preload + setup-not-ready), the hash-distribution audit, and the commit. The two patterns above were caught on Opus during verification. + ## External references - **Forgejo repo:** https://git.rdx4.com/megaproxy/rimlike (private) From 6b326173287cd5ac8da3a68f99048533d3ca2991 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 15:28:49 +0100 Subject: [PATCH 11/19] Pawn sprite: show_behind_parent fix for invisible bodies z_index=-1 was sinking the sprite below the floor TileMap (z=0), so name labels rendered but bodies didn't. Using show_behind_parent=true keeps the sprite at z=0 (above floor) while still drawing BEFORE the parent's _draw() so selection ring + carry indicator overlay on top. --- scenes/pawn/pawn.gd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scenes/pawn/pawn.gd b/scenes/pawn/pawn.gd index 7010ade..b9f7ba0 100644 --- a/scenes/pawn/pawn.gd +++ b/scenes/pawn/pawn.gd @@ -251,7 +251,11 @@ func _mount_sprite() -> void: _sprite.sprite_frames = sf _sprite.centered = true _sprite.offset = Vector2(0, -8) # bottom-anchor: feet ≈ tile bottom edge - _sprite.z_index = -1 + # show_behind_parent lets the sprite render at the same z_index as the + # parent Pawn (so it stays above the floor TileMap), while ALSO drawing + # beneath the parent's _draw() callback — so selection ring + carry + # indicator overlay on top. z_index=-1 would sink below the floor too. + _sprite.show_behind_parent = true _sprite.play(&"idle_down") add_child(_sprite) Audit.log("pawn_sprite", "%s → atlas idx %d" % [pawn_name, (absi(pawn_name.hash()) % _PEASANT_COUNT) + 1]) From 4bb00a798b5383b6c7bab5ece54950123a61b2b9 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 15:33:18 +0100 Subject: [PATCH 12/19] =?UTF-8?q?Pawn=20labels:=20shift=20NameLabel=20abov?= =?UTF-8?q?e=20the=2032=C3=9732=20sprite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NameLabel was at y=-18 — sat right on top of the sprite head when the body grew from 12px disc to 32×32 character sprite. Bumped to y=-38 so the name floats above the sprite (which spans y=-24..+8). StateLabel at y=+10 unchanged — already just below sprite bottom. Also drop temporary atlas-idx audit log (debug-only, hash distribution confirmed Bram=004 / Cora=013 / Edda=001). --- scenes/pawn/pawn.gd | 1 - scenes/pawn/pawn.tscn | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/scenes/pawn/pawn.gd b/scenes/pawn/pawn.gd index b9f7ba0..2f4565e 100644 --- a/scenes/pawn/pawn.gd +++ b/scenes/pawn/pawn.gd @@ -258,7 +258,6 @@ func _mount_sprite() -> void: _sprite.show_behind_parent = true _sprite.play(&"idle_down") add_child(_sprite) - Audit.log("pawn_sprite", "%s → atlas idx %d" % [pawn_name, (absi(pawn_name.hash()) % _PEASANT_COUNT) + 1]) # ── public API ────────────────────────────────────────────────────────────── diff --git a/scenes/pawn/pawn.tscn b/scenes/pawn/pawn.tscn index 87e312b..1862d14 100644 --- a/scenes/pawn/pawn.tscn +++ b/scenes/pawn/pawn.tscn @@ -6,7 +6,7 @@ script = ExtResource("1_pawn") [node name="NameLabel" type="Label" parent="."] -position = Vector2(-20, -18) +position = Vector2(-20, -38) size = Vector2(40, 12) theme_override_font_sizes/font_size = 8 horizontal_alignment = 1 From ab4d62889bd2ac70606a3e09c29f111be33058a4 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 15:38:45 +0100 Subject: [PATCH 13/19] Procedural shapes for unmapped item types (no more magenta squares) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scenes/entities/item.gd | 189 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 181 insertions(+), 8 deletions(-) diff --git a/scenes/entities/item.gd b/scenes/entities/item.gd index e54cdce..0ae136d 100644 --- a/scenes/entities/item.gd +++ b/scenes/entities/item.gd @@ -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. From c7f97e2c7a7a56559b69b6e56e5f48b9f72eb744 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 15:46:13 +0100 Subject: [PATCH 14/19] Carry indicator: draw the actual item shape, scaled down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scenes/entities/item.gd | 211 ++++++++++++++++++++++------------------ scenes/pawn/pawn.gd | 18 +++- 2 files changed, 127 insertions(+), 102 deletions(-) diff --git a/scenes/entities/item.gd b/scenes/entities/item.gd index 0ae136d..5476e53 100644 --- a/scenes/entities/item.gd +++ b/scenes/entities/item.gd @@ -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) diff --git a/scenes/pawn/pawn.gd b/scenes/pawn/pawn.gd index 2f4565e..5b6acd0 100644 --- a/scenes/pawn/pawn.gd +++ b/scenes/pawn/pawn.gd @@ -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 ───────────────────────────────────────────────────────────────── From 413054157a594004094bc573eee40f0a403f938b Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 15:52:31 +0100 Subject: [PATCH 15/19] Distinct icons for wheat / corn / potato / strawberry harvests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wheat + corn both produce TYPE_GRAIN; potato + strawberry both produce TYPE_VEGETABLE. Until now they rendered identically (yellow stalks for both grains, green-leafed root for both vegetables) since shape was driven by item_type alone. Added an Item.subtype field that carries the origin crop_kind through harvest. draw_item_shape dispatches on subtype FIRST then falls back to item_type — so storage filters (which match on item_type) still treat wheat+corn as one Grain category and potato+strawberry as one Vegetable category, but the visuals are now distinct. New procedural shapes: - wheat: 3 yellow stalks with grain-heads (same as existing grain) - corn: yellow cob with kernel dots wrapped in green husk leaves - potato: 2 brown overlapping lumps with sprout-eye dots - strawberry: red heart-shape body with green calyx + yellow seeds Crop.on_harvest_tick assigns subtype = crop_kind on spawn. SaveSystem._spawn_item now round-trips subtype through saves. Pawn carry indicator + Item._draw both pass subtype to draw_item_shape. --- autoload/save_system.gd | 2 + scenes/entities/crop.gd | 5 +++ scenes/entities/item.gd | 91 ++++++++++++++++++++++++++++++++++++++++- scenes/pawn/pawn.gd | 2 +- 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/autoload/save_system.gd b/autoload/save_system.gd index 115d730..b5dfe76 100644 --- a/autoload/save_system.gd +++ b/autoload/save_system.gd @@ -407,6 +407,8 @@ func _spawn_item(world_scene: Node, d: Dictionary) -> void: Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))) ) ent.quality = int(d.get("quality", 1)) as Item.Quality + ent.subtype = StringName(d.get("subtype", "")) + ent.queue_redraw() func _spawn_wall(world_scene: Node, d: Dictionary) -> void: diff --git a/scenes/entities/crop.gd b/scenes/entities/crop.gd index f47ed74..494d585 100644 --- a/scenes/entities/crop.gd +++ b/scenes/entities/crop.gd @@ -109,6 +109,11 @@ func on_harvest_tick() -> void: var it: Item = ITEM_SCENE.instantiate() get_parent().add_child(it) it.setup(item_type, 1, tile) + # Carry the crop_kind through as a visual subtype so wheat / corn / + # potato / strawberry each render distinctly, while item_type stays + # generic (TYPE_GRAIN / TYPE_VEGETABLE) for stockpile filter purposes. + it.subtype = crop_kind + it.queue_redraw() stage = Stage.TILLED stage_progress = 0 _refresh_sprite_region() diff --git a/scenes/entities/item.gd b/scenes/entities/item.gd index 5476e53..465d151 100644 --- a/scenes/entities/item.gd +++ b/scenes/entities/item.gd @@ -84,6 +84,13 @@ enum Quality { SHODDY, NORMAL, EXCELLENT, MASTERWORK, LEGENDARY } @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 @@ -154,6 +161,7 @@ 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, @@ -167,6 +175,7 @@ func to_dict() -> Dictionary: 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)), @@ -187,8 +196,86 @@ static func from_dict(d: Dictionary) -> Dictionary: ## 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: +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) @@ -375,7 +462,7 @@ func _draw() -> void: var square := Rect2(Vector2(-half, -half), Vector2(half * 2, half * 2)) if not has_sprite: - if not Item.draw_item_shape(self, item_type): + 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) diff --git a/scenes/pawn/pawn.gd b/scenes/pawn/pawn.gd index 5b6acd0..c3a838a 100644 --- a/scenes/pawn/pawn.gd +++ b/scenes/pawn/pawn.gd @@ -1184,7 +1184,7 @@ func _draw() -> void: 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): + if not Item.draw_item_shape(self, carried_item.item_type, carried_item.subtype): 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) From 53cb92041ce790d0908ed41718cf7b6c8b6495df Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 16:09:56 +0100 Subject: [PATCH 16/19] UI pass: medieval-warm Theme + real Build drawer thumbnails Theme (scenes/ui/medieval_theme.gd): - Procedural builder for an app-wide Theme: parchment buttons on dark-wood border, tan panels, ink text, gold focus ring. - Applied on the root Window in Main._ready, with a tree-walk that re-seeds every CanvasLayer's topmost Control (CanvasLayer interrupts the root-Window theme cascade in Godot 4). - Late-mounting popups + modals get themed via a child_entered_tree hook on each CanvasLayer. Build drawer thumbnails (scenes/ui/build_drawer_thumb.gd): - New BuildDrawerThumb Control that dispatches on tool_id and draws a recognisable silhouette of the entity each tool builds. 17 tools covered: chop/mine/dig_grave/no_roof (Designate), stone+wood walls, wood+stone floors, door, crate, bed, torch, 5 workbenches (Carpenter/Smelter/Millstone/Hearth/Cremation Pyre), stockpile, graveyard. - Replaces the flat ColorRect placeholder. _add_tool_btn signature changed from (label, color, callback) to (label, tool_id, callback). --- scenes/main/main.gd | 42 ++++ scenes/ui/build_drawer.gd | 66 ++++--- scenes/ui/build_drawer_thumb.gd | 338 ++++++++++++++++++++++++++++++++ scenes/ui/medieval_theme.gd | 198 +++++++++++++++++++ 4 files changed, 614 insertions(+), 30 deletions(-) create mode 100644 scenes/ui/build_drawer_thumb.gd create mode 100644 scenes/ui/medieval_theme.gd diff --git a/scenes/main/main.gd b/scenes/main/main.gd index c5b559f..b3e6686 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -20,6 +20,11 @@ const RESUME_TOAST_SCRIPT: Script = preload("res://scenes/ui/resume_toa # Phase 17 — PawnDetailPanel (layer 18) and SettingsMenu (layer 26). const PAWN_DETAIL_PANEL_SCRIPT: Script = preload("res://scenes/ui/pawn_detail_panel.gd") const WORKBENCH_PANEL_SCRIPT: Script = preload("res://scenes/ui/workbench_panel.gd") +const MEDIEVAL_THEME_SCRIPT: Script = preload("res://scenes/ui/medieval_theme.gd") + +# Built once in _ready and re-applied to any CanvasLayer-rooted Control because +# CanvasLayer doesn't propagate the root-Window theme cascade. +var _app_theme: Theme = null const SETTINGS_MENU_SCRIPT: Script = preload("res://scenes/ui/settings_menu.gd") # Phase 17 (Agent B) — BuildDrawer bottom-sheet (layer 16). const BUILD_DRAWER_SCRIPT: Script = preload("res://scenes/ui/build_drawer.gd") @@ -40,6 +45,13 @@ func _ready() -> void: assert(SaveSystem != null, "SaveSystem autoload missing") assert(Autosave != null, "Autosave autoload missing") + # Medieval-warm Theme — assigned on the root Window first. Cascade alone + # doesn't reach Controls inside CanvasLayers (CanvasLayer has no theme + # property), so we also walk the tree post-mount and apply to every Control + # encountered. (2026-05-16 polish pass.) + _app_theme = MEDIEVAL_THEME_SCRIPT.build() + get_tree().root.theme = _app_theme + # Phase 15 — Storyteller UI layers. Runtime-instantiated so no .tscn edit is # needed. CanvasLayer ensures correct draw order above World/TopBar regardless # of parent-node position. @@ -156,3 +168,33 @@ func _ready() -> void: top_bar._add_work_log_btns() Audit.log("main", "Phase 17 (Agent C) — WorkPriorityMatrix + AlertsLog mounted.") + + # Apply the medieval theme to every Control under each CanvasLayer. + # CanvasLayers interrupt the root-Window theme cascade so we have to seed + # each one explicitly. Defer one frame so panels that build their UI in + # _ready (PawnDetailPanel, WorkbenchPanel, BuildDrawer) finish first. + call_deferred("_apply_theme_to_canvas_layers") + + +## Walks the scene tree and assigns _app_theme to every Control directly under +## a CanvasLayer (the topmost Control in each layer's branch). From there the +## standard Control-to-Control cascade carries the theme to all descendants. +## Also catches popups and modals that mount later via child_entered_tree. +func _apply_theme_to_canvas_layers() -> void: + for c in get_children(): + if c is CanvasLayer: + _seed_layer_theme(c) + # Watch for late additions (popup menus, modals). + if not c.child_entered_tree.is_connected(_on_layer_child_added): + c.child_entered_tree.connect(_on_layer_child_added) + + +func _seed_layer_theme(layer: CanvasLayer) -> void: + for c in layer.get_children(): + if c is Control and c.theme == null: + c.theme = _app_theme + + +func _on_layer_child_added(node: Node) -> void: + if node is Control and node.theme == null: + node.theme = _app_theme diff --git a/scenes/ui/build_drawer.gd b/scenes/ui/build_drawer.gd index bacfa28..07b9ae0 100644 --- a/scenes/ui/build_drawer.gd +++ b/scenes/ui/build_drawer.gd @@ -180,10 +180,10 @@ func _build_designate_tab() -> Control: var flow := _make_flow_grid() box.add_child(flow) - _add_tool_btn(flow, Strings.t(&"tool.chop"), Color(0.3, 0.7, 0.2), func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) - _add_tool_btn(flow, Strings.t(&"tool.mine"), Color(0.6, 0.6, 0.6), func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) - _add_tool_btn(flow, Strings.t(&"tool.dig_grave"),Color(0.4, 0.3, 0.2), func() -> void: _activate(&"dig_grave",&"", Strings.t(&"tool.dig_grave"))) - _add_tool_btn(flow, Strings.t(&"tool.no_roof"), Color(0.7, 0.7, 0.9), func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) + _add_tool_btn(flow, Strings.t(&"tool.chop"), &"chop", func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) + _add_tool_btn(flow, Strings.t(&"tool.mine"), &"mine", func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) + _add_tool_btn(flow, Strings.t(&"tool.dig_grave"),&"dig_grave", func() -> void: _activate(&"dig_grave", &"", Strings.t(&"tool.dig_grave"))) + _add_tool_btn(flow, Strings.t(&"tool.no_roof"), &"no_roof", func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) return box @@ -197,44 +197,44 @@ func _build_build_tab() -> Control: box.add_child(flow) # Wall — show material chooser on first tap. - _add_tool_btn(flow, Strings.t(&"tool.build_wall_stone"), Color(0.55, 0.55, 0.55), + _add_tool_btn(flow, Strings.t(&"tool.build_wall_stone"), &"build_wall_stone", func() -> void: _activate_wall(&"stone")) - _add_tool_btn(flow, Strings.t(&"tool.build_wall_wood"), Color(0.65, 0.45, 0.25), + _add_tool_btn(flow, Strings.t(&"tool.build_wall_wood"), &"build_wall_wood", func() -> void: _activate_wall(&"wood")) # Floor. - _add_tool_btn(flow, Strings.t(&"tool.build_floor_wood"), Color(0.60, 0.40, 0.20), + _add_tool_btn(flow, Strings.t(&"tool.build_floor_wood"), &"build_floor_wood", func() -> void: _activate_floor(&"wood")) - _add_tool_btn(flow, Strings.t(&"tool.build_floor_stone"), Color(0.60, 0.60, 0.55), + _add_tool_btn(flow, Strings.t(&"tool.build_floor_stone"), &"build_floor_stone", func() -> void: _activate_floor(&"stone")) # Door + Crate. - _add_tool_btn(flow, Strings.t(&"tool.build_door"), Color(0.55, 0.35, 0.15), + _add_tool_btn(flow, Strings.t(&"tool.build_door"), &"build_door", func() -> void: _activate(&"build_door", &"", Strings.t(&"tool.build_door"))) - _add_tool_btn(flow, Strings.t(&"tool.build_crate"), Color(0.65, 0.45, 0.10), + _add_tool_btn(flow, Strings.t(&"tool.build_crate"), &"build_crate", func() -> void: _activate(&"build_crate", &"", Strings.t(&"tool.build_crate"))) # Bed + Torch. - _add_tool_btn(flow, Strings.t(&"tool.build_bed"), Color(0.40, 0.40, 0.80), + _add_tool_btn(flow, Strings.t(&"tool.build_bed"), &"build_bed", func() -> void: _activate(&"build_bed", &"", Strings.t(&"tool.build_bed"))) - _add_tool_btn(flow, Strings.t(&"tool.build_torch"), Color(0.90, 0.70, 0.20), + _add_tool_btn(flow, Strings.t(&"tool.build_torch"), &"build_torch", func() -> void: _activate(&"build_torch", &"", Strings.t(&"tool.build_torch"))) # Workbenches. _add_tool_btn(flow, Strings.t(&"tool.workbench_carpenter"), - Color(0.50, 0.35, 0.15), + &"build_workbench_carpenter", func() -> void: _activate(&"build_workbench_carpenter", &"", Strings.t(&"tool.workbench_carpenter"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_smelter"), - Color(0.60, 0.55, 0.45), + &"build_workbench_smelter", func() -> void: _activate(&"build_workbench_smelter", &"", Strings.t(&"tool.workbench_smelter"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_millstone"), - Color(0.55, 0.55, 0.55), + &"build_workbench_millstone", func() -> void: _activate(&"build_workbench_millstone", &"", Strings.t(&"tool.workbench_millstone"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_hearth"), - Color(0.80, 0.35, 0.15), + &"build_workbench_hearth", func() -> void: _activate(&"build_workbench_hearth", &"", Strings.t(&"tool.workbench_hearth"))) _add_tool_btn(flow, Strings.t(&"tool.workbench_cremation_pyre"), - Color(0.30, 0.25, 0.20), + &"build_workbench_cremation_pyre", func() -> void: _activate(&"build_workbench_cremation_pyre", &"", Strings.t(&"tool.workbench_cremation_pyre"))) return box @@ -248,9 +248,9 @@ func _build_stockpile_tab() -> Control: var flow := _make_flow_grid() box.add_child(flow) - _add_tool_btn(flow, Strings.t(&"tool.stockpile_general"), Color(0.30, 0.60, 0.30), + _add_tool_btn(flow, Strings.t(&"tool.stockpile_general"), &"paint_stockpile", func() -> void: _activate(&"paint_stockpile", &"", Strings.t(&"tool.stockpile_general"))) - _add_tool_btn(flow, Strings.t(&"tool.graveyard"), Color(0.25, 0.20, 0.15), + _add_tool_btn(flow, Strings.t(&"tool.graveyard"), &"graveyard", func() -> void: _activate(&"graveyard", &"", Strings.t(&"tool.graveyard"))) return box @@ -291,10 +291,13 @@ func _make_flow_grid() -> GridContainer: return g -## Add a single tool button to `container`. Each button is a VBoxContainer of -## [ColorRect icon area + Label] wrapped in a Button so the whole cell is one -## touch target. -func _add_tool_btn(container: Control, label_text: String, icon_color: Color, callback: Callable) -> void: +const _THUMB_SCRIPT: Script = preload("res://scenes/ui/build_drawer_thumb.gd") + + +## Add a single tool button to `container`. The button is a VBoxContainer of +## [thumb preview + Label] wrapped in a Button so the whole cell is one touch +## target. `tool_id` drives the procedural preview shape (BuildDrawerThumb). +func _add_tool_btn(container: Control, label_text: String, tool_id: StringName, callback: Callable) -> void: var btn := Button.new() btn.custom_minimum_size = Vector2(BTN_SIZE, BTN_SIZE + LABEL_HEIGHT) btn.focus_mode = Control.FOCUS_NONE @@ -303,13 +306,16 @@ func _add_tool_btn(container: Control, label_text: String, icon_color: Color, ca vb.mouse_filter = Control.MOUSE_FILTER_IGNORE vb.add_theme_constant_override("separation", 2) - # Icon area — procedural colored rect (real sprites land with Phase 17 art pass). - var icon := ColorRect.new() - icon.color = icon_color - icon.custom_minimum_size = Vector2(BTN_SIZE - 8, BTN_SIZE - LABEL_HEIGHT - 8) - icon.size_flags_horizontal = Control.SIZE_SHRINK_CENTER - icon.mouse_filter = Control.MOUSE_FILTER_IGNORE - vb.add_child(icon) + # Procedural preview of the entity this tool builds. + var thumb := Control.new() + thumb.set_script(_THUMB_SCRIPT) + # Use .set() — the static type is Control (set_script doesn't refine it), + # but the runtime instance has the tool_id property from the script. + thumb.set("tool_id", tool_id) + thumb.custom_minimum_size = Vector2(BTN_SIZE - 8, BTN_SIZE - LABEL_HEIGHT - 8) + thumb.size_flags_horizontal = Control.SIZE_SHRINK_CENTER + thumb.mouse_filter = Control.MOUSE_FILTER_IGNORE + vb.add_child(thumb) # Label. var lbl := Label.new() diff --git a/scenes/ui/build_drawer_thumb.gd b/scenes/ui/build_drawer_thumb.gd new file mode 100644 index 0000000..22ec4d5 --- /dev/null +++ b/scenes/ui/build_drawer_thumb.gd @@ -0,0 +1,338 @@ +class_name BuildDrawerThumb extends Control +## Procedural preview thumbnail for one BuildDrawer tool button. +## +## Each tool_id dispatches to a small _draw call that renders a recognisable +## silhouette of the entity that tool builds. All shapes live inside a 40×40 +## box centred in this control's size — the parent button supplies the panel +## frame; this widget only paints the icon. +## +## Cheaper than instantiating live entity scenes into SubViewports; pure draw +## calls, no allocations, recomputes only when the button repaints. + +## Tool identifier — drives the shape dispatch in _draw. +var tool_id: StringName = &"" + + +func _draw() -> void: + var c := size / 2.0 + Vector2(0, -2) # slight upward bias for label clearance + # Soft shadow under every thumb (subtle depth cue against the parchment). + draw_circle(c + Vector2(0, 12), 14.0, Color(0, 0, 0, 0.08)) + var outline := Color(0.10, 0.07, 0.05, 0.85) + match tool_id: + &"build_wall_stone": + # Grey brick wall with staggered courses. + var brick := Color(0.62, 0.60, 0.58) + var brick_hi := Color(0.78, 0.76, 0.72) + var mortar := Color(0.30, 0.28, 0.26) + var r := Rect2(c.x - 16, c.y - 16, 32, 32) + draw_rect(r, brick) + # Three brick courses, staggered seams. + for i in 3: + var y: float = r.position.y + i * 11 + 4 + draw_line(Vector2(r.position.x, y), Vector2(r.end.x, y), mortar, 1.0) + # Vertical seams (staggered per row). + for i in 3: + var y: float = r.position.y + i * 11 + 4 + var stagger: float = 0.0 if i % 2 == 0 else 8.0 + var x_offs: Array[float] = [-8.0, 0.0, 8.0] + for x_off in x_offs: + var x: float = c.x + x_off + stagger + if x >= r.position.x and x <= r.end.x: + draw_line(Vector2(x, y - 11), Vector2(x, y), mortar, 1.0) + # Top edge highlight. + draw_rect(Rect2(r.position.x, r.position.y, r.size.x, 3), brick_hi) + draw_rect(r, outline, false, 1.0) + &"build_wall_wood": + # Brown wood-plank wall, 3 vertical planks. + var wood := Color(0.65, 0.45, 0.25) + var wood_dark := Color(0.42, 0.27, 0.12) + var r := Rect2(c.x - 16, c.y - 16, 32, 32) + draw_rect(r, wood) + draw_line(Vector2(c.x - 5.5, r.position.y), Vector2(c.x - 5.5, r.end.y), wood_dark, 1.0) + draw_line(Vector2(c.x + 5.5, r.position.y), Vector2(c.x + 5.5, r.end.y), wood_dark, 1.0) + # Knot details (small dots). + draw_circle(Vector2(c.x - 11, c.y - 6), 1.2, wood_dark) + draw_circle(Vector2(c.x + 1, c.y + 4), 1.2, wood_dark) + draw_rect(r, outline, false, 1.0) + &"build_floor_wood": + # Tan floorboard plan-view — 4 planks horizontal with grain. + var board := Color(0.78, 0.58, 0.32) + var board_dark := Color(0.50, 0.32, 0.16) + var r := Rect2(c.x - 16, c.y - 12, 32, 24) + draw_rect(r, board) + for i in 1.0: + pass # placeholder loop + draw_line(Vector2(r.position.x, c.y - 6), Vector2(r.end.x, c.y - 6), board_dark, 1.0) + draw_line(Vector2(r.position.x, c.y), Vector2(r.end.x, c.y), board_dark, 1.0) + draw_line(Vector2(r.position.x, c.y + 6), Vector2(r.end.x, c.y + 6), board_dark, 1.0) + draw_rect(r, outline, false, 1.0) + &"build_floor_stone": + # Grey paving stones — 2×2 grid with cross-mortar. + var stone := Color(0.65, 0.63, 0.60) + var mortar := Color(0.35, 0.33, 0.30) + var r := Rect2(c.x - 16, c.y - 12, 32, 24) + draw_rect(r, stone) + draw_line(Vector2(c.x, r.position.y), Vector2(c.x, r.end.y), mortar, 1.0) + draw_line(Vector2(r.position.x, c.y), Vector2(r.end.x, c.y), mortar, 1.0) + draw_rect(r, outline, false, 1.0) + &"build_door": + # Wooden door — rounded-top arch silhouette with a handle dot. + var door := Color(0.55, 0.35, 0.15) + var door_hi := Color(0.75, 0.50, 0.25) + var handle := Color(0.95, 0.80, 0.20) + # Body + var pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 10, c.y + 16), Vector2(c.x - 10, c.y - 6), + Vector2(c.x - 8, c.y - 12), Vector2(c.x, c.y - 16), + Vector2(c.x + 8, c.y - 12), Vector2(c.x + 10, c.y - 6), + Vector2(c.x + 10, c.y + 16), + ]) + draw_colored_polygon(pts, door) + # Inner plank seam. + draw_line(Vector2(c.x, c.y - 14), Vector2(c.x, c.y + 14), door_hi, 1.0) + # Handle. + draw_circle(Vector2(c.x + 5, c.y + 4), 1.5, handle) + draw_polyline(pts + PackedVector2Array([pts[0]]), outline, 1.0) + &"build_crate": + # Wooden crate — brown box with X cross-bracing on the front. + var wood := Color(0.65, 0.42, 0.18) + var wood_dark := Color(0.42, 0.27, 0.10) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, wood) + # Top edge bevel. + draw_rect(Rect2(r.position.x, r.position.y, r.size.x, 3), Color(0.85, 0.62, 0.30)) + # X cross-bracing. + draw_line(r.position, r.end, wood_dark, 2.0) + draw_line(Vector2(r.position.x, r.end.y), Vector2(r.end.x, r.position.y), wood_dark, 2.0) + draw_rect(r, outline, false, 1.0) + &"build_bed": + # Top-down bed view — wood frame, pillow top, blanket body. + var frame := Color(0.55, 0.35, 0.15) + var blanket := Color(0.55, 0.42, 0.78) + var pillow := Color(0.95, 0.93, 0.85) + var r := Rect2(c.x - 12, c.y - 14, 24, 28) + # Frame. + draw_rect(r, frame) + # Blanket inside (slightly inset). + draw_rect(Rect2(r.position.x + 2, r.position.y + 8, r.size.x - 4, r.size.y - 10), blanket) + # Pillow at top. + draw_rect(Rect2(r.position.x + 2, r.position.y + 2, r.size.x - 4, 5), pillow) + # Outline. + draw_rect(r, outline, false, 1.0) + &"build_torch": + # Wall torch — vertical brown shaft with orange flame above. + var shaft := Color(0.45, 0.28, 0.12) + var bracket := Color(0.35, 0.35, 0.38) + var flame_outer := Color(0.95, 0.40, 0.05) + var flame_inner := Color(1.00, 0.85, 0.30) + # Wall bracket + draw_rect(Rect2(c.x - 8, c.y + 6, 16, 4), bracket) + # Torch shaft. + draw_rect(Rect2(c.x - 2, c.y - 6, 4, 14), shaft) + # Flame teardrop. + draw_circle(Vector2(c.x, c.y - 10), 5.0, flame_outer) + draw_circle(Vector2(c.x, c.y - 12), 2.5, flame_inner) + # Smoke wisp. + draw_line(Vector2(c.x, c.y - 16), Vector2(c.x + 2, c.y - 20), Color(0.70, 0.70, 0.70, 0.6), 1.0) + &"build_workbench_carpenter": + # Wood bench with saw on top, two legs visible. + var plank_top := Color(0.78, 0.55, 0.30) + var plank_front := Color(0.55, 0.38, 0.22) + var leg := Color(0.32, 0.22, 0.10) + var saw_blade := Color(0.82, 0.82, 0.85) + var saw_handle := Color(0.55, 0.30, 0.15) + # Legs. + draw_rect(Rect2(c.x - 14, c.y, 3, 16), leg) + draw_rect(Rect2(c.x + 11, c.y, 3, 16), leg) + # Bench body. + draw_rect(Rect2(c.x - 16, c.y - 4, 32, 8), plank_front) + draw_rect(Rect2(c.x - 16, c.y - 8, 32, 4), plank_top) + # Saw blade. + draw_rect(Rect2(c.x - 4, c.y - 12, 12, 2), saw_blade) + draw_rect(Rect2(c.x + 8, c.y - 13, 4, 4), saw_handle) + draw_rect(Rect2(c.x - 16, c.y - 12, 32, 16), outline, false, 1.0) + &"build_workbench_smelter": + # Stone furnace with ember opening + smoke from chimney. + var stone := Color(0.55, 0.55, 0.55) + var stone_front := Color(0.42, 0.42, 0.43) + var ember := Color(0.98, 0.55, 0.10) + var ember_core := Color(1.00, 0.85, 0.30) + var chimney := Color(0.32, 0.30, 0.30) + var smoke := Color(0.75, 0.73, 0.70, 0.7) + # Stone body. + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 22), stone_front) + draw_rect(Rect2(c.x - 14, c.y - 11, 28, 4), stone) + # Ember mouth. + draw_rect(Rect2(c.x - 7, c.y - 2, 14, 8), Color(0.18, 0.10, 0.06)) + draw_rect(Rect2(c.x - 5, c.y, 10, 4), ember) + draw_rect(Rect2(c.x - 3, c.y + 1, 6, 2), ember_core) + # Chimney + smoke. + draw_rect(Rect2(c.x + 4, c.y - 16, 5, 6), chimney) + draw_line(Vector2(c.x + 6, c.y - 18), Vector2(c.x + 5, c.y - 23), smoke, 1.5) + draw_rect(Rect2(c.x - 14, c.y - 11, 28, 25), outline, false, 1.0) + &"build_workbench_millstone": + # Wood frame + round grindstone wheel. + var frame_front := Color(0.42, 0.26, 0.12) + var frame_top := Color(0.55, 0.36, 0.18) + var wheel := Color(0.55, 0.53, 0.50) + var wheel_rim := Color(0.20, 0.18, 0.16) + var wheel_dark := Color(0.34, 0.32, 0.30) + # Frame. + draw_rect(Rect2(c.x - 14, c.y + 2, 28, 12), frame_front) + draw_rect(Rect2(c.x - 14, c.y - 1, 28, 5), frame_top) + # Wheel. + draw_circle(c + Vector2(0, -4), 11.0, wheel_rim) + draw_circle(c + Vector2(0, -4), 9.5, wheel) + # Front-face shadow (lower half). + draw_rect(Rect2(c.x - 9, c.y - 4, 18, 9), wheel_dark) + # Center pin. + draw_circle(c + Vector2(0, -4), 2.0, Color(0.18, 0.16, 0.14)) + draw_rect(Rect2(c.x - 14, c.y - 15, 28, 29), outline, false, 1.0) + &"build_workbench_hearth": + # Tall stone fireplace with wood mantle + flame. + var stone := Color(0.60, 0.58, 0.55) + var stone_dark := Color(0.42, 0.40, 0.38) + var mantle := Color(0.50, 0.34, 0.20) + var opening := Color(0.10, 0.05, 0.02) + var flame_outer := Color(0.95, 0.40, 0.05) + var flame_inner := Color(1.00, 0.85, 0.30) + var log_wood := Color(0.55, 0.32, 0.15) + # Stone surround. + draw_rect(Rect2(c.x - 14, c.y - 16, 28, 32), stone) + draw_line(Vector2(c.x - 14, c.y - 8), Vector2(c.x + 14, c.y - 8), stone_dark, 1.0) + # Mantle band. + draw_rect(Rect2(c.x - 14, c.y - 6, 28, 3), mantle) + # Opening. + draw_rect(Rect2(c.x - 9, c.y - 2, 18, 18), opening) + # Log. + draw_rect(Rect2(c.x - 6, c.y + 10, 12, 3), log_wood) + # Flame. + draw_rect(Rect2(c.x - 4, c.y + 4, 8, 6), flame_outer) + draw_rect(Rect2(c.x - 3, c.y + 1, 6, 4), flame_outer) + draw_rect(Rect2(c.x - 2, c.y + 4, 4, 4), flame_inner) + draw_rect(Rect2(c.x - 14, c.y - 16, 28, 32), outline, false, 1.0) + &"build_workbench_cremation_pyre": + # Charred wood pile with ember glow + ash smoke. + var base_top := Color(0.30, 0.22, 0.12) + var base_front := Color(0.22, 0.15, 0.08) + var ember := Color(0.95, 0.45, 0.10) + var ash_grey := Color(0.70, 0.68, 0.65, 0.7) + draw_rect(Rect2(c.x - 14, c.y - 4, 28, 16), base_front) + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 4), base_top) + draw_rect(Rect2(c.x - 9, c.y + 2, 18, 4), ember) + # Smoke wisps. + draw_rect(Rect2(c.x - 5, c.y - 14, 2, 6), ash_grey) + draw_rect(Rect2(c.x + 1, c.y - 16, 2, 8), ash_grey) + draw_rect(Rect2(c.x + 5, c.y - 12, 2, 4), ash_grey) + draw_rect(Rect2(c.x - 14, c.y - 8, 28, 20), outline, false, 1.0) + &"paint_stockpile": + # Green tile with dashed boundary — a designated stockpile zone. + var fill := Color(0.35, 0.65, 0.30, 0.45) + var border := Color(0.20, 0.45, 0.15) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, fill) + # Dashed border: 4 dashes per side. + for i in 4: + draw_line(Vector2(r.position.x + i * 7, r.position.y), + Vector2(r.position.x + i * 7 + 4, r.position.y), border, 2.0) + draw_line(Vector2(r.position.x + i * 7, r.end.y), + Vector2(r.position.x + i * 7 + 4, r.end.y), border, 2.0) + for i in 3: + draw_line(Vector2(r.position.x, r.position.y + i * 8), + Vector2(r.position.x, r.position.y + i * 8 + 4), border, 2.0) + draw_line(Vector2(r.end.x, r.position.y + i * 8), + Vector2(r.end.x, r.position.y + i * 8 + 4), border, 2.0) + &"graveyard": + # Dark earth tile + grave cross marker. + var earth := Color(0.35, 0.28, 0.20) + var earth_hi := Color(0.50, 0.40, 0.28) + var cross := Color(0.78, 0.78, 0.76) + var r := Rect2(c.x - 14, c.y - 12, 28, 24) + draw_rect(r, earth) + draw_line(Vector2(r.position.x, r.position.y + 3), Vector2(r.end.x, r.position.y + 3), earth_hi, 1.0) + # Cross (gravestone marker). + draw_rect(Rect2(c.x - 1.5, c.y - 8, 3, 18), cross) + draw_rect(Rect2(c.x - 6, c.y - 4, 12, 3), cross) + draw_rect(r, outline, false, 1.0) + &"chop": + # Axe head + handle silhouette over a green target tile. + var grass := Color(0.35, 0.65, 0.30, 0.35) + var handle := Color(0.55, 0.35, 0.15) + var blade := Color(0.78, 0.80, 0.85) + var blade_dark := Color(0.45, 0.48, 0.52) + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), grass) + # Handle (diagonal). + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 6, c.y - 8), handle, 3.0) + # Axe head. + var ax_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x + 2, c.y - 12), Vector2(c.x + 12, c.y - 6), + Vector2(c.x + 8, c.y), Vector2(c.x - 2, c.y - 4), + ]) + draw_colored_polygon(ax_pts, blade) + draw_polyline(ax_pts + PackedVector2Array([ax_pts[0]]), blade_dark, 1.0) + &"mine": + # Pickaxe over a grey stone tile. + var stone := Color(0.62, 0.60, 0.58, 0.4) + var handle := Color(0.55, 0.35, 0.15) + var head := Color(0.48, 0.48, 0.52) + var head_dark := Color(0.28, 0.28, 0.32) + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), stone) + # Handle. + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 6, c.y - 8), handle, 3.0) + # Two-pointed pickaxe head. + var pk_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 6, c.y - 12), Vector2(c.x + 12, c.y - 6), + Vector2(c.x + 4, c.y - 2), Vector2(c.x, c.y - 6), + Vector2(c.x - 4, c.y - 4), + ]) + draw_colored_polygon(pk_pts, head) + draw_polyline(pk_pts + PackedVector2Array([pk_pts[0]]), head_dark, 1.0) + &"dig_grave": + # Shovel + earth mound. + var earth := Color(0.35, 0.25, 0.15) + var earth_hi := Color(0.55, 0.40, 0.25) + var handle := Color(0.55, 0.35, 0.15) + var blade := Color(0.55, 0.55, 0.60) + # Earth mound at bottom. + var mound: PackedVector2Array = PackedVector2Array([ + Vector2(c.x - 14, c.y + 12), Vector2(c.x + 14, c.y + 12), + Vector2(c.x + 8, c.y + 4), Vector2(c.x - 8, c.y + 4), + ]) + draw_colored_polygon(mound, earth) + draw_line(Vector2(c.x - 6, c.y + 6), Vector2(c.x + 6, c.y + 6), earth_hi, 1.0) + # Shovel handle. + draw_line(Vector2(c.x - 8, c.y + 10), Vector2(c.x + 4, c.y - 10), handle, 3.0) + # Shovel blade. + var sh_pts: PackedVector2Array = PackedVector2Array([ + Vector2(c.x + 3, c.y - 12), Vector2(c.x + 10, c.y - 10), + Vector2(c.x + 10, c.y - 4), Vector2(c.x + 4, c.y - 4), + ]) + draw_colored_polygon(sh_pts, blade) + &"no_roof": + # Open square with up-arrow (cancel-roof designation). + var sky := Color(0.55, 0.75, 0.95, 0.4) + var border := Color(0.20, 0.40, 0.65) + var arrow := Color(0.95, 0.95, 0.95) + # Square outline (dashed corners) representing the cell. + draw_rect(Rect2(c.x - 14, c.y - 12, 28, 24), sky) + # Corner brackets. + var corners: Array = [ + [Vector2(c.x - 14, c.y - 12), Vector2(1, 0), Vector2(0, 1)], + [Vector2(c.x + 14, c.y - 12), Vector2(-1, 0), Vector2(0, 1)], + [Vector2(c.x - 14, c.y + 12), Vector2(1, 0), Vector2(0, -1)], + [Vector2(c.x + 14, c.y + 12), Vector2(-1, 0), Vector2(0, -1)], + ] + for corner in corners: + var pos: Vector2 = corner[0] + var dx: Vector2 = corner[1] + var dy: Vector2 = corner[2] + draw_line(pos, pos + dx * 5.0, border, 2.0) + draw_line(pos, pos + dy * 5.0, border, 2.0) + # Up arrow centred. + draw_line(Vector2(c.x, c.y + 6), Vector2(c.x, c.y - 6), arrow, 2.0) + draw_line(Vector2(c.x, c.y - 6), Vector2(c.x - 4, c.y - 2), arrow, 2.0) + draw_line(Vector2(c.x, c.y - 6), Vector2(c.x + 4, c.y - 2), arrow, 2.0) + _: + # Unknown tool — small grey placeholder. + draw_rect(Rect2(c.x - 12, c.y - 12, 24, 24), Color(0.50, 0.50, 0.50)) + draw_rect(Rect2(c.x - 12, c.y - 12, 24, 24), outline, false, 1.0) diff --git a/scenes/ui/medieval_theme.gd b/scenes/ui/medieval_theme.gd new file mode 100644 index 0000000..afecbf7 --- /dev/null +++ b/scenes/ui/medieval_theme.gd @@ -0,0 +1,198 @@ +class_name MedievalTheme extends RefCounted +## Builds a Theme resource implementing the "Medieval warm" palette: +## Panel: tan (198, 168, 128) +## Border: dark wood (90, 55, 30) +## Button: parchment (230, 210, 170), hover lighter, pressed inset shadow +## Text: ink black with off-white on dark panels +## +## Applied on Main scene root so it cascades to every Control descendant. +## Per-panel overrides are still possible — only nodes that don't explicitly +## override a style pick up the global Theme. + +# ── palette ────────────────────────────────────────────────────────────────── +const C_PANEL := Color(0.776, 0.659, 0.502) # tan +const C_PANEL_DARK := Color(0.353, 0.216, 0.118) # dark wood +const C_BUTTON := Color(0.902, 0.824, 0.667) # parchment +const C_BUTTON_HOV := Color(0.961, 0.902, 0.769) # warm parchment +const C_BUTTON_PRESS:= Color(0.776, 0.694, 0.529) # pressed shadow +const C_BUTTON_DIS := Color(0.690, 0.620, 0.510) # disabled +const C_INK := Color(0.106, 0.078, 0.039) +const C_INK_DIM := Color(0.353, 0.275, 0.196) +const C_ACCENT := Color(0.580, 0.180, 0.110) # wax-seal red, for selected tabs +const C_ACCENT_GOLD := Color(0.831, 0.620, 0.149) + + +static func build() -> Theme: + var theme := Theme.new() + # Default font size — small UI, but legible on phone. + theme.default_font_size = 14 + + # ── Button (drives OptionButton + MenuButton too) ───────────────────────── + theme.set_stylebox("normal", "Button", _btn_box(C_BUTTON, false)) + theme.set_stylebox("hover", "Button", _btn_box(C_BUTTON_HOV, false)) + theme.set_stylebox("pressed", "Button", _btn_box(C_BUTTON_PRESS, true)) + theme.set_stylebox("disabled", "Button", _btn_box(C_BUTTON_DIS, false)) + theme.set_stylebox("focus", "Button", _focus_box()) + theme.set_color("font_color", "Button", C_INK) + theme.set_color("font_hover_color", "Button", C_INK) + theme.set_color("font_pressed_color", "Button", C_INK) + theme.set_color("font_disabled_color", "Button", C_INK_DIM) + theme.set_constant("h_separation", "Button", 6) + + # OptionButton inherits Button styling automatically via class fallback in + # Godot, but we set explicit copies in case overrides land. + theme.set_stylebox("normal", "OptionButton", _btn_box(C_BUTTON, false)) + theme.set_stylebox("hover", "OptionButton", _btn_box(C_BUTTON_HOV, false)) + theme.set_stylebox("pressed", "OptionButton", _btn_box(C_BUTTON_PRESS, true)) + theme.set_stylebox("focus", "OptionButton", _focus_box()) + theme.set_color("font_color", "OptionButton", C_INK) + + # ── CheckBox ────────────────────────────────────────────────────────────── + theme.set_color("font_color", "CheckBox", C_INK) + theme.set_color("font_hover_color", "CheckBox", C_INK) + theme.set_color("font_pressed_color", "CheckBox", C_INK) + + # ── PanelContainer ──────────────────────────────────────────────────────── + theme.set_stylebox("panel", "PanelContainer", _panel_box()) + + # ── Panel (raw) — same look as PanelContainer ───────────────────────────── + theme.set_stylebox("panel", "Panel", _panel_box()) + + # ── PopupMenu (recipe picker) ───────────────────────────────────────────── + theme.set_stylebox("panel", "PopupMenu", _panel_box()) + theme.set_stylebox("hover", "PopupMenu", _btn_box(C_BUTTON_HOV, false)) + theme.set_color("font_color", "PopupMenu", C_INK) + theme.set_color("font_hover_color", "PopupMenu", C_INK) + + # ── Label — defaults to ink-on-tan; per-label modulate still works ──────── + theme.set_color("font_color", "Label", C_INK) + + # ── SpinBox / LineEdit ──────────────────────────────────────────────────── + theme.set_stylebox("normal", "LineEdit", _btn_box(C_BUTTON, true)) + theme.set_stylebox("focus", "LineEdit", _focus_box()) + theme.set_color("font_color", "LineEdit", C_INK) + theme.set_color("caret_color", "LineEdit", C_INK) + + # ── Slider (audio sliders in SettingsMenu) ──────────────────────────────── + theme.set_stylebox("slider", "HSlider", _slider_track()) + theme.set_stylebox("grabber_area", "HSlider", _slider_fill()) + theme.set_stylebox("grabber_area_highlight", "HSlider", _slider_fill()) + + # ── ScrollContainer scrollbar ───────────────────────────────────────────── + theme.set_stylebox("scroll", "VScrollBar", _scrollbar_track()) + theme.set_stylebox("grabber", "VScrollBar", _btn_box(C_PANEL_DARK, false)) + theme.set_stylebox("grabber_pressed","VScrollBar", _btn_box(C_PANEL_DARK, true)) + + # ── HSeparator / VSeparator (used between bill rows in WorkbenchPanel) ──── + var sep := StyleBoxFlat.new() + sep.bg_color = C_PANEL_DARK + sep.content_margin_top = 1 + sep.content_margin_bottom = 1 + theme.set_stylebox("separator", "HSeparator", sep) + theme.set_stylebox("separator", "VSeparator", sep) + + return theme + + +# ── helpers ────────────────────────────────────────────────────────────────── + +static func _btn_box(fill: Color, pressed: bool) -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = fill + s.border_color = C_PANEL_DARK + s.border_width_left = 1 + s.border_width_right = 1 + s.border_width_top = 1 + s.border_width_bottom = 1 + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + if pressed: + # Inset shadow on top + left (pressed-in look). + s.shadow_color = Color(0, 0, 0, 0.35) + s.shadow_size = 0 + # Visually offset content slightly down/right when pressed. + s.content_margin_top = 4 + s.content_margin_left = 7 + s.content_margin_right = 5 + s.content_margin_bottom = 2 + else: + # Subtle drop shadow for depth. + s.shadow_color = Color(0, 0, 0, 0.20) + s.shadow_size = 2 + s.shadow_offset = Vector2(0, 1) + s.content_margin_top = 3 + s.content_margin_left = 6 + s.content_margin_right = 6 + s.content_margin_bottom = 3 + return s + + +static func _panel_box() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_PANEL + s.border_color = C_PANEL_DARK + s.border_width_left = 2 + s.border_width_right = 2 + s.border_width_top = 2 + s.border_width_bottom = 2 + s.corner_radius_top_left = 6 + s.corner_radius_top_right = 6 + s.corner_radius_bottom_left = 6 + s.corner_radius_bottom_right = 6 + s.shadow_color = Color(0, 0, 0, 0.35) + s.shadow_size = 4 + s.shadow_offset = Vector2(0, 2) + s.content_margin_top = 8 + s.content_margin_left = 10 + s.content_margin_right = 10 + s.content_margin_bottom = 8 + return s + + +static func _focus_box() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = Color(0, 0, 0, 0) # transparent + s.border_color = C_ACCENT_GOLD + s.border_width_left = 2 + s.border_width_right = 2 + s.border_width_top = 2 + s.border_width_bottom = 2 + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + return s + + +static func _slider_track() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_PANEL_DARK + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + s.content_margin_top = 4 + s.content_margin_bottom = 4 + return s + + +static func _slider_fill() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = C_ACCENT_GOLD + s.corner_radius_top_left = 4 + s.corner_radius_top_right = 4 + s.corner_radius_bottom_left = 4 + s.corner_radius_bottom_right = 4 + return s + + +static func _scrollbar_track() -> StyleBoxFlat: + var s := StyleBoxFlat.new() + s.bg_color = Color(C_PANEL_DARK.r, C_PANEL_DARK.g, C_PANEL_DARK.b, 0.3) + s.corner_radius_top_left = 3 + s.corner_radius_top_right = 3 + s.corner_radius_bottom_left = 3 + s.corner_radius_bottom_right = 3 + return s From 296894ff7ae9ca8d3cb985a31f705c57d246ccbe Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 16:35:59 +0100 Subject: [PATCH 17/19] TopBar: emoji icons + tighter button sizing (less horizontal space) --- scenes/ui/top_bar.gd | 20 ++++++++++++-------- scenes/ui/top_bar.tscn | 10 +++++++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/scenes/ui/top_bar.gd b/scenes/ui/top_bar.gd index c78ee4a..d778e8d 100644 --- a/scenes/ui/top_bar.gd +++ b/scenes/ui/top_bar.gd @@ -44,6 +44,10 @@ var _log_btn: Button = null func _ready() -> void: + var button_row: HBoxContainer = get_node_or_null("Anchor/ButtonRow") + if button_row != null: + button_row.add_theme_constant_override("separation", 4) + pause_btn.text = Strings.t(&"speed.pause") normal_btn.text = Strings.t(&"speed.normal") fast_btn.text = Strings.t(&"speed.fast") @@ -152,8 +156,8 @@ func _add_build_btn() -> void: return var build_btn := Button.new() build_btn.name = "BuildBtn" - build_btn.text = Strings.t(&"ui.build") - build_btn.custom_minimum_size = Vector2(60, 48) + build_btn.text = "🔨" + build_btn.custom_minimum_size = Vector2(40, 40) build_btn.focus_mode = Control.FOCUS_NONE build_btn.pressed.connect(_on_build_pressed) button_row.add_child(build_btn) @@ -176,8 +180,8 @@ func _add_settings_btn() -> void: return var settings_btn := Button.new() settings_btn.name = "SettingsBtn" - settings_btn.text = Strings.t(&"ui.settings.btn") - settings_btn.custom_minimum_size = Vector2(80, 48) + settings_btn.text = "⚙" + settings_btn.custom_minimum_size = Vector2(40, 40) settings_btn.focus_mode = Control.FOCUS_NONE settings_btn.pressed.connect(_on_settings_pressed) button_row.add_child(settings_btn) @@ -201,16 +205,16 @@ func _add_work_log_btns() -> void: var work_btn := Button.new() work_btn.name = "WorkBtn" - work_btn.text = "Work" - work_btn.custom_minimum_size = Vector2(60, 48) + work_btn.text = "👷" + work_btn.custom_minimum_size = Vector2(40, 40) work_btn.focus_mode = Control.FOCUS_NONE work_btn.pressed.connect(_on_work_pressed) button_row.add_child(work_btn) _log_btn = Button.new() _log_btn.name = "LogBtn" - _log_btn.text = "Log" - _log_btn.custom_minimum_size = Vector2(60, 48) + _log_btn.text = "🔔" + _log_btn.custom_minimum_size = Vector2(40, 40) _log_btn.focus_mode = Control.FOCUS_NONE _log_btn.pressed.connect(_on_log_pressed) button_row.add_child(_log_btn) diff --git a/scenes/ui/top_bar.tscn b/scenes/ui/top_bar.tscn index ae00ee1..bb20de2 100644 --- a/scenes/ui/top_bar.tscn +++ b/scenes/ui/top_bar.tscn @@ -20,29 +20,33 @@ offset_bottom = 40.0 [node name="PauseBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 +custom_minimum_size = Vector2(36, 40) text = "‖" [node name="NormalBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 +custom_minimum_size = Vector2(36, 40) text = "1×" [node name="FastBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 +custom_minimum_size = Vector2(36, 40) text = "5×" [node name="UltraBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 +custom_minimum_size = Vector2(36, 40) text = "12×" [node name="SaveBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 -custom_minimum_size = Vector2(48, 48) +custom_minimum_size = Vector2(40, 40) text = "💾" [node name="LoadBtn" type="Button" parent="Anchor/ButtonRow"] focus_mode = 0 -custom_minimum_size = Vector2(48, 48) -text = "Load" +custom_minimum_size = Vector2(40, 40) +text = "📂" [node name="ClockLabel" type="Label" parent="Anchor"] anchor_left = 0.5 From d98d2c24250df0894d09f6a95231ddb5dca2d623 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 16:36:16 +0100 Subject: [PATCH 18/19] Renewable resources: tree growth + WildGrowth + Quarry on BigRockNode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trees: 4 growth stages (Sapling→Young→Growing→Mature), only Mature yields wood. WildGrowth ticker fires every in-game hour; rejection- samples grass tiles and plants a sapling with ~30% probability (capped at MAP_TREE_LIMIT=60). New `paint_plant_tree` designation lets the player manually plant — ghost sapling registered as a build_site that ConstructionProvider fulfils. Stage round-trips through save/load. Initial seed mixes 4 saplings + 6 mature so growth is visible day 1. Quarry: new BigRockNode entity (2×2 permanent stone outcrop, never depletes). 3 nodes seeded far from cabin. New QuarryWorkbench (extends Workbench, auto-FOREVER `quarry_stone` bill, recipe drops 1 stone per 300 work-ticks). New `paint_quarry` designation only accepts BigRockNode tiles. CraftingProvider now supports recipes with `ingredient_count == 0` — skips ingredient-fetch and goes straight to walk+craft toils. Recipe gains `ingredient_count` field (defaults 0). Save/load layering: big_rock_node spawns at priority 0 (same as rock/tree), quarry_workbench at priority 2 (after the node). UI: Plant tree + Build quarry buttons added to Build drawer. build_drawer_thumb gains `plant_tree` (sapling sprout in dirt) and `paint_quarry` (stone block + chisel + cut-stone pile) shapes. inspect_tooltip recognises BigRockNode + shows tree growth stage on hover. Delegation: gdscript-refactor (Sonnet ×2) for trees full impl + quarry skeleton; quick-edit (Haiku) for CraftingProvider no-ingredient plumbing + TopBar polish; integration handled on Opus. --- autoload/save_system.gd | 24 +++- autoload/strings.gd | 7 ++ autoload/world.gd | 19 +++ scenes/ai/crafting_provider.gd | 42 +++++-- scenes/ai/recipe.gd | 5 + scenes/ai/recipe_catalog.gd | 16 +++ scenes/entities/big_rock_node.gd | 140 +++++++++++++++++++++ scenes/entities/big_rock_node.gd.uid | 1 + scenes/entities/big_rock_node.tscn | 6 + scenes/entities/quarry_workbench.gd | 80 ++++++++++++ scenes/entities/quarry_workbench.gd.uid | 1 + scenes/entities/quarry_workbench.tscn | 6 + scenes/entities/tree.gd | 160 ++++++++++++++++++++++-- scenes/ui/build_drawer.gd | 15 ++- scenes/ui/build_drawer_thumb.gd | 40 ++++++ scenes/ui/build_drawer_thumb.gd.uid | 1 + scenes/ui/inspect_tooltip.gd | 28 ++++- scenes/ui/medieval_theme.gd.uid | 1 + scenes/world/designation.gd | 8 ++ scenes/world/world.gd | 154 ++++++++++++++++++++++- 20 files changed, 716 insertions(+), 38 deletions(-) create mode 100644 scenes/entities/big_rock_node.gd create mode 100644 scenes/entities/big_rock_node.gd.uid create mode 100644 scenes/entities/big_rock_node.tscn create mode 100644 scenes/entities/quarry_workbench.gd create mode 100644 scenes/entities/quarry_workbench.gd.uid create mode 100644 scenes/entities/quarry_workbench.tscn create mode 100644 scenes/ui/build_drawer_thumb.gd.uid create mode 100644 scenes/ui/medieval_theme.gd.uid diff --git a/autoload/save_system.gd b/autoload/save_system.gd index b5dfe76..6d134ce 100644 --- a/autoload/save_system.gd +++ b/autoload/save_system.gd @@ -29,6 +29,7 @@ const _PAWN_SCENE: PackedScene = preload("res://scenes/pawn/pawn.tscn") const _TREE_SCENE: PackedScene = preload("res://scenes/entities/tree.tscn") const _ROCK_SCENE: PackedScene = preload("res://scenes/entities/rock.tscn") const _BIG_ROCK_SCENE: PackedScene = preload("res://scenes/entities/big_rock.tscn") +const _BIG_ROCK_NODE_SCENE: PackedScene = preload("res://scenes/entities/big_rock_node.tscn") const _ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") const _WALL_SCENE: PackedScene = preload("res://scenes/entities/wall.tscn") const _FLOOR_SCENE: PackedScene = preload("res://scenes/entities/floor.tscn") @@ -276,6 +277,7 @@ func _collect_entities() -> Array: var registries: Array = [ World.trees, World.rocks, + World.big_rock_nodes, World.items, World.build_queue, # ghost walls / floors / doors / grave_slots World.doors, @@ -316,6 +318,7 @@ const _SPAWN_PRIORITY: Dictionary = { &"tree": 0, &"rock": 0, &"big_rock": 0, + &"big_rock_node": 0, &"wall": 0, &"floor": 0, &"door": 1, @@ -351,6 +354,7 @@ func _register_factories() -> void: _factories[&"tree"] = _spawn_tree _factories[&"rock"] = _spawn_rock _factories[&"big_rock"] = _spawn_big_rock + _factories[&"big_rock_node"] = _spawn_big_rock_node _factories[&"item"] = _spawn_item _factories[&"wall"] = _spawn_wall _factories[&"floor"] = _spawn_floor @@ -377,8 +381,18 @@ func _register_factories() -> void: func _spawn_tree(world_scene: Node, d: Dictionary) -> void: var ent = _TREE_SCENE.instantiate() world_scene.add_child(ent) - ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))) + # Pass growth_stage to setup() so _refresh_sprite() picks the right visual. + # Default 3 (STAGE_MATURE) so pre-growth-system saves load as mature trees. + var gs: int = int(d.get("growth_stage", 3)) + ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0))), gs) ent.chop_progress = int(d.get("chop_progress", 0)) + ent.growth_progress = int(d.get("growth_progress", 0)) + ent.chop_designated = bool(d.get("chop_designated", false)) + ent.pending_plant = bool(d.get("pending_plant", false)) + ent._plant_progress = int(d.get("plant_progress", 0)) + # Re-register as build site if planting is still in progress. + if ent.pending_plant: + World.register_build_site(ent) ent.queue_redraw() @@ -398,6 +412,14 @@ func _spawn_big_rock(world_scene: Node, d: Dictionary) -> void: ent.queue_redraw() +func _spawn_big_rock_node(world_scene: Node, d: Dictionary) -> void: + var ent = _BIG_ROCK_NODE_SCENE.instantiate() + world_scene.add_child(ent) + ent.setup(Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))) + ent.is_quarry_site = bool(d.get("is_quarry_site", false)) + ent.queue_redraw() + + func _spawn_item(world_scene: Node, d: Dictionary) -> void: var ent = _ITEM_SCENE.instantiate() world_scene.add_child(ent) diff --git a/autoload/strings.gd b/autoload/strings.gd index d49660d..a96fe0e 100644 --- a/autoload/strings.gd +++ b/autoload/strings.gd @@ -194,8 +194,15 @@ const TABLE: Dictionary = { &"tool.workbench_millstone": "Millstone", &"tool.workbench_hearth": "Hearth", &"tool.workbench_cremation_pyre": "Cremation Pyre", + &"tool.paint_quarry": "Build quarry", &"tool.stockpile_general": "Stockpile", &"tool.graveyard": "Graveyard", + &"tool.plant_tree": "Plant tree", + # Tree growth stage names (shown in inspect tooltip). + &"tree.stage.sapling": "Sapling", + &"tree.stage.young": "Young tree", + &"tree.stage.growing": "Growing tree", + &"tree.stage.mature": "Mature tree", &"ui.bill.mode_forever": "Forever", &"ui.bill.mode_count": "Do X times", &"ui.bill.mode_until_n": "Do until X", diff --git a/autoload/world.gd b/autoload/world.gd index ffa2b99..c85dc3c 100644 --- a/autoload/world.gd +++ b/autoload/world.gd @@ -19,6 +19,7 @@ var work_providers: Array = [] # from their _ready/_exit_tree. Phase 16 will add stable IDs and persistence wiring. var trees: Array = [] # Array of Tree var rocks: Array = [] # Array of Rock +var big_rock_nodes: Array = [] # Array of BigRockNode (permanent stone outcrops) var items: Array = [] # Array of Item (on-floor stacks) var stockpiles: Array = [] # Array of StorageDestination (StockpileZone for now; containers Phase 5) @@ -207,6 +208,24 @@ func unregister_rock(r) -> void: rocks.erase(r) +func register_big_rock_node(n) -> void: + if not big_rock_nodes.has(n): + big_rock_nodes.append(n) + + +func unregister_big_rock_node(n) -> void: + big_rock_nodes.erase(n) + + +## Returns the BigRockNode whose 2×2 footprint covers `tile`, or null. +## Used by `paint_quarry` designation to validate the build site. +func big_rock_node_at_tile(tile: Vector2i): + for n in big_rock_nodes: + if n.is_at(tile): + return n + return null + + func register_item(it) -> void: if items.has(it): return diff --git a/scenes/ai/crafting_provider.gd b/scenes/ai/crafting_provider.gd index dcab18d..fc59865 100644 --- a/scenes/ai/crafting_provider.gd +++ b/scenes/ai/crafting_provider.gd @@ -69,14 +69,24 @@ func find_best_for(pawn) -> Job: _emit_bill_blocked(b.recipe.label, &"skill_too_low", wb) continue - # Confirm a qualifying ingredient exists on the floor. - var src = _find_ingredient_item(b.recipe.ingredient_type) - if src == null: - _emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb) - continue + # If ingredient_count is 0, no ingredient is required; proceed directly. + # Otherwise, confirm a qualifying ingredient exists on the floor. + var src = null + if b.recipe.ingredient_count > 0: + src = _find_ingredient_item(b.recipe.ingredient_type) + if src == null: + _emit_bill_blocked(b.recipe.label, &"missing_ingredient", wb) + continue + + # Score: total Manhattan travel distance. + # If no ingredient (count==0), distance is just pawn → workbench. + # Otherwise, distance is pawn → ingredient → workbench. + var d: int + if b.recipe.ingredient_count > 0: + d = _manhattan(pawn.tile, src.tile) + _manhattan(src.tile, wb.tile) + else: + d = _manhattan(pawn.tile, wb.tile) - # Score: total Manhattan travel distance pawn → ingredient → workbench. - var d: int = _manhattan(pawn.tile, src.tile) + _manhattan(src.tile, wb.tile) if d < best_dist: best_dist = d best_wb = wb @@ -87,16 +97,22 @@ func find_best_for(pawn) -> Job: if best_wb == null: return null - # Re-resolve the source item in case multiple bills tied on the same item. - var src_item = _find_ingredient_item(best_bill.recipe.ingredient_type) - if src_item == null: - return null + var src_item = null + # If ingredient_count > 0, re-resolve the source item in case multiple bills tied on the same item. + if best_bill.recipe.ingredient_count > 0: + src_item = _find_ingredient_item(best_bill.recipe.ingredient_type) + if src_item == null: + return null var j := Job.new() j.label = "Craft %s at %s" % [best_bill.recipe.label, best_wb.get("label_text") if best_wb.get("label_text") != null else "workbench"] j.target_node = best_wb - j.toils.append(Toil.walk_to(src_item.tile)) - j.toils.append(Toil.pickup()) + + # Only add ingredient-haul toils if ingredient is required. + if best_bill.recipe.ingredient_count > 0: + j.toils.append(Toil.walk_to(src_item.tile)) + j.toils.append(Toil.pickup()) + j.toils.append(Toil.walk_to(best_wb.tile)) j.toils.append(Toil.craft_at(best_wb.get_path(), best_bill_index)) return j diff --git a/scenes/ai/recipe.gd b/scenes/ai/recipe.gd index 768fa6a..5f7ebc8 100644 --- a/scenes/ai/recipe.gd +++ b/scenes/ai/recipe.gd @@ -19,6 +19,9 @@ var id: StringName = &"" ## Item type consumed by this recipe (single-ingredient for Phase 6). var ingredient_type: StringName = &"" +## Count of ingredient_type required by this recipe. 0 = no ingredient (work only). +var ingredient_count: int = 0 + ## Phase 14 — optional secondary ingredient. Empty string = no secondary. ## CraftingProvider Phase 14 follow-up: enforce pickup of ingredient2 before ## assigning a pawn to this bill (currently stub — only ingredient_type enforced). @@ -59,6 +62,7 @@ func to_dict() -> Dictionary: return { "id": String(id), "ingredient_type": String(ingredient_type), + "ingredient_count": ingredient_count, "ingredient2_type": String(ingredient2_type), "ingredient2_count": ingredient2_count, "output_type": String(output_type), @@ -73,6 +77,7 @@ static func from_dict(d: Dictionary) -> Recipe: var r := Recipe.new() r.id = StringName(d.get("id", "")) r.ingredient_type = StringName(d.get("ingredient_type", "")) + r.ingredient_count = int(d.get("ingredient_count", 0)) r.ingredient2_type = StringName(d.get("ingredient2_type", "")) r.ingredient2_count = int(d.get("ingredient2_count", 0)) r.output_type = StringName(d.get("output_type", "")) diff --git a/scenes/ai/recipe_catalog.gd b/scenes/ai/recipe_catalog.gd index c8f9e22..24d84e3 100644 --- a/scenes/ai/recipe_catalog.gd +++ b/scenes/ai/recipe_catalog.gd @@ -111,6 +111,21 @@ static func cremate_corpse() -> Recipe: return r +## Quarry workbench — no input, produces 1 stone per work cycle. Used by the +## QuarryWorkbench placed on a BigRockNode for renewable stone supply. +static func quarry_stone() -> Recipe: + var r := Recipe.new() + r.id = &"quarry_stone" + r.label = "Quarry stone" + r.ingredient_type = &"" # no input + r.ingredient_count = 0 + r.output_type = Item.TYPE_STONE + r.work_ticks = 300 + r.required_skill = &"manual_labor" + r.skill_threshold = 0 + return r + + ## Returns one fresh instance of every recipe in the catalog. Used by UI ## recipe-pickers to enumerate available bills; callers filter by ## `recipe.required_skill` against the workbench's `accepted_skill`. @@ -122,4 +137,5 @@ static func all() -> Array[Recipe]: bread(), meal_from_vegetables(), cremate_corpse(), + quarry_stone(), ] diff --git a/scenes/entities/big_rock_node.gd b/scenes/entities/big_rock_node.gd new file mode 100644 index 0000000..3ff6da3 --- /dev/null +++ b/scenes/entities/big_rock_node.gd @@ -0,0 +1,140 @@ +class_name BigRockNode extends Node2D +## Permanent stone outcrop — a 2×2 immovable boulder that never depletes. +## +## BigRockNode marks its footprint impassable in the pathfinder and registers +## itself with World so the designation system can locate it. A QuarryWorkbench +## can be built on one of its tiles; external code sets is_quarry_site = true +## and quarry_workbench once construction completes. +## +## Draw convention: position is stamped at the TOP-LEFT pixel corner of the +## 2×2 footprint (tile * TILE_SIZE_PX). The visual centre of the 32×32 area +## sits at local (16, 16), so all _draw_* helpers are centred there. +## +## Save/load: to_dict / from_dict round-trip tile + is_quarry_site. +## The quarry_workbench reference is an entity pointer reconstructed by the +## save layer (SaveSystem wires it after both entities are spawned). + +const TILE_SIZE_PX: int = 16 + +## Top-left tile of the 2×2 footprint. +var tile: Vector2i = Vector2i.ZERO + +## Footprint size in tiles (always 2×2; declared as a var so external code can +## read it uniformly without special-casing BigRockNode vs hypothetical future nodes). +var footprint: Vector2i = Vector2i(2, 2) + +## True once a Quarry workbench has been completed on this outcrop. +## Flipped by external code (designation / world.gd) — this file only declares it. +var is_quarry_site: bool = false + +## The QuarryWorkbench that sits on this node after construction. +## null until assigned by the designation completion handler. +var quarry_workbench = null + + +# ── lifecycle ───────────────────────────────────────────────────────────────── + +func _ready() -> void: + # Position at top-left pixel corner so footprint_tiles() aligns with world coords. + position = Vector2(tile.x * TILE_SIZE_PX, tile.y * TILE_SIZE_PX) + # Block pathfinding on every tile in the footprint. + for t in footprint_tiles(): + if World.pathfinder != null: + World.pathfinder.set_cell_walkable(t, false) + World.register_big_rock_node(self) + queue_redraw() + + +func _exit_tree() -> void: + World.unregister_big_rock_node(self) + + +# ── public API ──────────────────────────────────────────────────────────────── + +## One-shot initialiser. Call after add_child() so _ready() has fired. +func setup(p_tile: Vector2i) -> void: + tile = p_tile + position = Vector2(tile.x * TILE_SIZE_PX, tile.y * TILE_SIZE_PX) + queue_redraw() + Audit.log("big_rock_node", "spawned at tile %s" % tile) + + +## Returns the four tiles covered by this node's 2×2 footprint. +## Used by designation, save/load, and inspection code. +func footprint_tiles() -> Array[Vector2i]: + return [ + tile, + tile + Vector2i(1, 0), + tile + Vector2i(0, 1), + tile + Vector2i(1, 1), + ] + + +## True when tile_to_check falls inside the 2×2 footprint. +func is_at(tile_to_check: Vector2i) -> bool: + return ( + tile_to_check.x >= tile.x and tile_to_check.x < tile.x + footprint.x + and tile_to_check.y >= tile.y and tile_to_check.y < tile.y + footprint.y + ) + + +# ── save / load ─────────────────────────────────────────────────────────────── + +func to_dict() -> Dictionary: + return { + "class_id": &"big_rock_node", + "tile_x": tile.x, + "tile_y": tile.y, + "is_quarry_site": is_quarry_site, + } + + +## Restore from a dict produced by to_dict(). quarry_workbench is reconnected +## by the save layer after both entities are spawned. +static func from_dict(d: Dictionary) -> Dictionary: + return { + "tile_x": int(d.get("tile_x", 0)), + "tile_y": int(d.get("tile_y", 0)), + "is_quarry_site": bool(d.get("is_quarry_site", false)), + } + + +# ── render ──────────────────────────────────────────────────────────────────── + +## Draw a procedural pile of grey rocks centred at local (16, 16) — +## the geometric centre of the 32×32 footprint area. +## Three layers create depth: large base blob → medium mid rock → small cap. +func _draw() -> void: + var cx: float = 16.0 + var cy: float = 16.0 + + # Colour palette — warm grey tones suggesting weathered granite. + var base_fill := Color(0.60, 0.58, 0.55) # large base ellipse + var mid_fill := Color(0.45, 0.44, 0.42) # medium middle rock + var top_fill := Color(0.55, 0.53, 0.50) # small perched cap + var outline_col := Color(0.20, 0.18, 0.16, 0.70) # subtle dark rim + + # Bottom: large flattened blob — widest at the base to read as a ground-hugging mass. + var base_w: float = 24.0 + var base_h: float = 16.0 + var base_rect := Rect2(Vector2(cx - base_w / 2.0, cy - base_h / 2.0 + 2.0), Vector2(base_w, base_h)) + draw_rect(base_rect, base_fill) + + # Dark outline arc around the base blob. + draw_rect(base_rect, outline_col, false, 1.0) + + # Middle: slightly elevated, narrower rock sitting on top of the base. + var mid_w: float = 16.0 + var mid_h: float = 12.0 + var mid_oy: float = -4.0 # shift up from centre + var mid_rect := Rect2(Vector2(cx - mid_w / 2.0, cy - mid_h / 2.0 + mid_oy), Vector2(mid_w, mid_h)) + draw_rect(mid_rect, mid_fill) + draw_rect(mid_rect, outline_col, false, 1.0) + + # Top: small cap perched on the very top. + var top_w: float = 8.0 + var top_h: float = 6.0 + var top_oy: float = -10.0 # above the mid rock + var top_rect := Rect2(Vector2(cx - top_w / 2.0, cy - top_h / 2.0 + top_oy), Vector2(top_w, top_h)) + draw_rect(top_rect, top_fill) + draw_rect(top_rect, outline_col, false, 1.0) diff --git a/scenes/entities/big_rock_node.gd.uid b/scenes/entities/big_rock_node.gd.uid new file mode 100644 index 0000000..af363c5 --- /dev/null +++ b/scenes/entities/big_rock_node.gd.uid @@ -0,0 +1 @@ +uid://bhn0lknhgn1od diff --git a/scenes/entities/big_rock_node.tscn b/scenes/entities/big_rock_node.tscn new file mode 100644 index 0000000..f93f19f --- /dev/null +++ b/scenes/entities/big_rock_node.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://big_rock_node_entity"] + +[ext_resource type="Script" path="res://scenes/entities/big_rock_node.gd" id="1_big_rock_node"] + +[node name="BigRockNode" type="Node2D"] +script = ExtResource("1_big_rock_node") diff --git a/scenes/entities/quarry_workbench.gd b/scenes/entities/quarry_workbench.gd new file mode 100644 index 0000000..91ef0f7 --- /dev/null +++ b/scenes/entities/quarry_workbench.gd @@ -0,0 +1,80 @@ +class_name QuarryWorkbench extends Workbench +## Quarry workbench — built on a BigRockNode tile, produces stone indefinitely. +## +## Subclasses Workbench to reuse the build + bill machinery. A default +## FOREVER bill with the quarry_stone recipe is auto-added in _ready() so the +## workbench begins dripping stone as soon as construction completes. +## +## No-ingredient recipe: quarry_stone uses ingredient_count = 0. CraftingProvider +## must handle the zero-ingredient path before this workbench produces anything +## (see plan_quarry_bigrock.md Step 1 — handled separately). +## +## Variant appearance: overrides _draw() unconditionally so that the quarry +## silhouette is always shown regardless of label_text (mirrors CremationPyre). +## accepted_skill = manual_labor — any labourer can work the quarry. +## +## World registration: inherited from Workbench._ready / _exit_tree. + + +func _init() -> void: + label_text = "Quarry" + accepted_skill = &"manual_labor" + + +func _ready() -> void: + label_text = "Quarry" + accepted_skill = &"manual_labor" + super._ready() + # Auto-populate a default FOREVER bill so the bench is immediately usable + # once construction completes. Mirrors CremationPyre's bill wiring. + var b := Bill.new() + b.recipe = RecipeCatalog.quarry_stone() + b.mode = Bill.Mode.FOREVER + bills.append(b) + Audit.log("quarry", "QuarryWorkbench ready at %s — bill added" % tile) + + +# ── render ──────────────────────────────────────────────────────────────────── + +## Override Workbench._draw() to always dispatch to _draw_quarry(). +## Alpha is 0.4 while under construction, 1.0 once complete (same as all benches). +func _draw() -> void: + var alpha: float = 1.0 if is_completed() else 0.4 + _draw_quarry(alpha) + + +## Procedural quarry appearance: a wooden frame base with a large stone block +## on top, chisel-mark details, and a small pile of freshly cut stones to the +## right side. Local coords follow Workbench convention — (0, 0) is the +## bottom-right of the tile; the bench draws UP into negative-y space. +func _draw_quarry(alpha: float) -> void: + var frame_top := Color(0.55, 0.36, 0.18, alpha) # light wood top face + var frame_front := Color(0.42, 0.26, 0.12, alpha) # darker wood front + var frame_edge := Color(0.25, 0.14, 0.06, alpha) # wood grain seam + var stone_top := Color(0.60, 0.58, 0.55, alpha) # stone block top face + var stone_front := Color(0.44, 0.43, 0.41, alpha) # stone block front + var chisel_mark := Color(0.28, 0.27, 0.25, alpha) # cut line on stone + var pile_light := Color(0.62, 0.61, 0.59, alpha) # loose stone pile + var pile_dark := Color(0.38, 0.37, 0.35, alpha) # pile shadow + var outline := Color(0.20, 0.18, 0.16, 0.70 * alpha) + + # Wooden frame base — front face + top lip. + draw_rect(Rect2(Vector2(-8.0, -7.0), Vector2(16.0, 7.0)), frame_front) + draw_rect(Rect2(Vector2(-8.0, -10.0), Vector2(16.0, 3.0)), frame_top) + draw_line(Vector2(-8.0, -7.0), Vector2(8.0, -7.0), frame_edge, 1.0) + + # Stone block sitting on the frame — occupies most of the upper half. + draw_rect(Rect2(Vector2(-7.0, -15.0), Vector2(11.0, 5.0)), stone_top) + draw_rect(Rect2(Vector2(-7.0, -10.5), Vector2(11.0, 0.5)), stone_front) # thin visible front edge + + # Chisel marks on the stone top — short diagonal cuts suggesting work in progress. + draw_line(Vector2(-4.0, -13.0), Vector2(-2.0, -11.5), chisel_mark, 1.0) + draw_line(Vector2(-1.0, -14.0), Vector2( 1.0, -12.5), chisel_mark, 1.0) + draw_line(Vector2( 2.0, -13.0), Vector2( 4.0, -11.5), chisel_mark, 1.0) + + # Small pile of cut stones on the right — two rounded rects stacked. + draw_rect(Rect2(Vector2(5.0, -9.0), Vector2(4.0, 3.0)), pile_dark) + draw_rect(Rect2(Vector2(5.0, -11.0), Vector2(3.0, 2.0)), pile_light) + + # Tile outline. + draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) diff --git a/scenes/entities/quarry_workbench.gd.uid b/scenes/entities/quarry_workbench.gd.uid new file mode 100644 index 0000000..a3ef967 --- /dev/null +++ b/scenes/entities/quarry_workbench.gd.uid @@ -0,0 +1 @@ +uid://bpbcwr4dh1736 diff --git a/scenes/entities/quarry_workbench.tscn b/scenes/entities/quarry_workbench.tscn new file mode 100644 index 0000000..ec28b7a --- /dev/null +++ b/scenes/entities/quarry_workbench.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://quarry_workbench_entity"] + +[ext_resource type="Script" path="res://scenes/entities/quarry_workbench.gd" id="1_quarry"] + +[node name="QuarryWorkbench" type="Node2D"] +script = ExtResource("1_quarry") diff --git a/scenes/entities/tree.gd b/scenes/entities/tree.gd index 50b2a28..a77d505 100644 --- a/scenes/entities/tree.gd +++ b/scenes/entities/tree.gd @@ -1,10 +1,15 @@ ## Tree entity — choppable by a pawn with a Chop job. Drops wood Item nodes -## when felled. +## when felled. Trees also grow through four stages (Sapling → Young → Growing +## → Mature); only Mature trees can be chopped. ## ## Chopping model (docs/implementation.md Phase 4): ## A ChopProvider creates a Job whose INTERACT toil calls on_chop_tick() once ## per sim tick via JobRunner. After CHOP_TICKS ticks the tree is felled. ## +## Growth model: on_sim_tick() is called once per sim tick by world.gd's sweep. +## After STAGE_TICKS ticks at each sub-mature stage the tree advances one stage +## and _refresh_sprite() updates the visual. +## ## World registration (World.register_tree / World.unregister_tree) is called ## here but the methods land in World during Opus integration. @@ -22,6 +27,19 @@ const WOOD_DROPS_ON_FELL: int = 3 ## Stack size per dropped Item (Phase 4 simplicity: 3 items of stack 1 each). const STACK_SIZE_PER_DROP: int = 1 +# ── growth stage constants ───────────────────────────────────────────────────── + +## Growth stage indices. +const STAGE_SAPLING: int = 0 +const STAGE_YOUNG: int = 1 +const STAGE_GROWING: int = 2 +const STAGE_MATURE: int = 3 + +## Sim ticks spent in each sub-mature stage before advancing. +## 5 in-game hours per stage at 20 Hz = 1200 ticks/hour × 5 = 6000 ticks. +## At default 5× speed that is ~5 min real time per stage, ~15 min seed → mature. +const STAGE_TICKS: int = 6000 + # ── state ───────────────────────────────────────────────────────────────────── var tile: Vector2i = Vector2i.ZERO @@ -31,6 +49,17 @@ var chop_progress: int = 0 ## ignores undesignated trees (Rimworld parity — pawns don't auto-chop). var chop_designated: bool = false +## Current growth stage (STAGE_SAPLING..STAGE_MATURE). Default MATURE so +## existing seed trees load at full size with no visual regression. +var growth_stage: int = STAGE_MATURE +## Ticks elapsed within the current sub-mature stage. +var growth_progress: int = 0 + +## Set to true on a ghost tree spawned by the plant_tree designation. The +## ConstructionProvider will issue a build job; on completion this flag clears +## and the tree grows normally. +var pending_plant: bool = false + # Preloaded scene for spawned wood items. const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") @@ -57,18 +86,27 @@ const _TREE_SILHOUETTES: int = 4 # silhouettes per atlas (columns) func _ready() -> void: position = _tile_to_world(tile) - _build_sprite() + _refresh_sprite() # Y-sort so the canopy draws behind walls/pawns that are visually south of # the trunk base. Position.y is the trunk-base row. y_sort_enabled = true World.register_tree(self) -## Adds a Sprite2D child painted with one of the 12 ElvGames tree variants -## (4 silhouettes × 3 season palettes). Variant chosen deterministically -## from the tile coord so the same tile always gets the same tree silhouette -## across boots and load/save. -func _build_sprite() -> void: +## Rebuild the Sprite2D child to match the current growth_stage. +## Sapling (stage 0): no Sprite2D — rendered procedurally in _draw(). +## Young (1) → scale 0.35, Growing (2) → 0.65, Mature (3) → 1.0. +## Any existing "Sprite" child is removed first so re-calls don't stack. +func _refresh_sprite() -> void: + var old := get_node_or_null("Sprite") + if old != null: + old.queue_free() + if growth_stage == STAGE_SAPLING: + # No Sprite2D for saplings — all rendering done in _draw(). + queue_redraw() + return + var scale_map: Array[float] = [1.0, 0.35, 0.65, 1.0] # indexed by stage + var sprite_scale: float = scale_map[growth_stage] var sprite := Sprite2D.new() sprite.name = "Sprite" var hash_seed: int = tile.x * 31 + tile.y * 17 @@ -83,9 +121,18 @@ func _build_sprite() -> void: # Sprite center is at offset.y; sprite half-height is _TREE_VARIANT_H/2 = 40. # We want bottom edge at +8 (tile bottom) → center at 8 - 40 = -32. sprite.offset = Vector2(0, -32) + sprite.scale = Vector2(sprite_scale, sprite_scale) # Render behind pawns/items that are at higher z_index; trees live at z=0. sprite.z_index = 0 add_child(sprite) + queue_redraw() + + +## Adds a Sprite2D child painted with one of the 12 ElvGames tree variants +## (4 silhouettes × 3 season palettes). Kept for call-site compatibility but +## now delegates to _refresh_sprite(). New code should call _refresh_sprite(). +func _build_sprite() -> void: + _refresh_sprite() func _exit_tree() -> void: @@ -95,17 +142,81 @@ func _exit_tree() -> void: # ── public API ──────────────────────────────────────────────────────────────── ## One-shot initialiser. Call after add_child() so _ready() already fired. -func setup(start_tile: Vector2i) -> void: +## start_stage defaults to STAGE_MATURE for backward compatibility. +func setup(start_tile: Vector2i, start_stage: int = STAGE_MATURE) -> void: tile = start_tile chop_progress = 0 + growth_stage = start_stage + growth_progress = 0 position = _tile_to_world(tile) + _refresh_sprite() queue_redraw() - Audit.log("tree", "spawned at %s" % tile) + Audit.log("tree", "spawned at %s (stage=%d)" % [tile, growth_stage]) -## True when the tree hasn't been fully chopped yet. +## True when the tree is mature, unChopped, and not a pending-plant ghost. +## Only mature trees yield wood — saplings/young/growing cannot be felled. func is_choppable() -> bool: - return chop_progress < CHOP_TICKS + return chop_progress < CHOP_TICKS and growth_stage == STAGE_MATURE and not pending_plant + + +## Called by world.gd's sim-tick sweep once per sim tick. +## Advances growth_progress and promotes the stage on threshold. No-op for +## mature trees and for pending-plant ghosts (ghost must be built first). +func on_sim_tick() -> void: + if growth_stage >= STAGE_MATURE or pending_plant: + return + growth_progress += 1 + if growth_progress >= STAGE_TICKS: + growth_stage += 1 + growth_progress = 0 + _refresh_sprite() + Audit.log("tree", "grew to stage %d at %s" % [growth_stage, tile]) + + +# ── pending-plant / build-site duck-type API ────────────────────────────────── +# ConstructionProvider requires is_buildable() / on_build_tick() / label() +# on every entity in World.build_queue. A pending_plant tree satisfies this +# interface so the provider can assign a pawn to "build" (plant) it. + +## Ticks of pawn work needed to complete a manual planting job. +const PLANT_TICKS: int = 30 + +## Progress counter within the planting job (0..PLANT_TICKS). +var _plant_progress: int = 0 + + +## True while the tree is a pending-plant ghost awaiting pawn work. +func is_buildable() -> bool: + return pending_plant and _plant_progress < PLANT_TICKS + + +## Human-readable label for the ConstructionProvider job entry. +func label() -> String: + return "Plant tree" + + +## Called by JobRunner's BUILD toil once per sim tick while a pawn works this +## site. After PLANT_TICKS the ghost becomes a real sapling. +func on_build_tick() -> void: + if not is_buildable(): + return + _plant_progress += 1 + queue_redraw() + if _plant_progress >= PLANT_TICKS: + _complete_plant() + + +## Finish the planting job: clear the pending flag, register as a real sapling, +## remove from World.build_queue, and clear the designation highlight. +func _complete_plant() -> void: + pending_plant = false + _plant_progress = 0 + World.unregister_build_site(self) + World.clear_designation_at(tile) + _refresh_sprite() + queue_redraw() + Audit.log("tree", "planted at %s — sapling begins growing" % tile) ## Called by the INTERACT toil in JobRunner once per sim tick while the pawn @@ -145,6 +256,10 @@ func to_dict() -> Dictionary: "tile_y": tile.y, "chop_progress": chop_progress, "chop_designated": chop_designated, + "growth_stage": growth_stage, + "growth_progress": growth_progress, + "pending_plant": pending_plant, + "plant_progress": _plant_progress, } @@ -154,15 +269,34 @@ static func from_dict(d: Dictionary) -> Dictionary: "tile_y": int(d.get("tile_y", 0)), "chop_progress": int(d.get("chop_progress", 0)), "chop_designated": bool(d.get("chop_designated", false)), + # Default to STAGE_MATURE (3) so pre-growth-system saves load as mature trees. + "growth_stage": int(d.get("growth_stage", 3)), + "growth_progress": int(d.get("growth_progress", 0)), + "pending_plant": bool(d.get("pending_plant", false)), + "plant_progress": int(d.get("plant_progress", 0)), } # ── render ──────────────────────────────────────────────────────────────────── func _draw() -> void: - # Canopy + trunk now come from the Sprite2D child (see _build_sprite). + # Sapling stage: draw a procedural sprout — no atlas sprite available. + # Three small green leaf-dots clustered above a thin brown stem. + if growth_stage == STAGE_SAPLING: + # Ghost tint for pending-plant saplings so the player can tell it's + # waiting for a pawn to build it. + var alpha := 0.55 if pending_plant else 1.0 + # Stem + draw_line(Vector2(0.0, 6.0), Vector2(0.0, 0.0), Color(0.35, 0.22, 0.10, alpha), 1.5) + # Three leaf dots + draw_circle(Vector2(0.0, -2.0), 2.5, Color(0.30, 0.65, 0.20, alpha)) + draw_circle(Vector2(-3.0, 1.0), 1.8, Color(0.25, 0.58, 0.18, alpha)) + draw_circle(Vector2(3.0, 0.5), 1.8, Color(0.28, 0.62, 0.19, alpha)) + return + + # Mature / growing stages: canopy + trunk come from the Sprite2D child. # This _draw renders only the chop-progress notch overlaid on the trunk. - if chop_progress > 0: + if chop_progress > 0 and growth_stage == STAGE_MATURE: var ratio := float(chop_progress) / float(CHOP_TICKS) var notch_depth := ratio * 3.0 draw_line( diff --git a/scenes/ui/build_drawer.gd b/scenes/ui/build_drawer.gd index 07b9ae0..ea85b1b 100644 --- a/scenes/ui/build_drawer.gd +++ b/scenes/ui/build_drawer.gd @@ -180,10 +180,11 @@ func _build_designate_tab() -> Control: var flow := _make_flow_grid() box.add_child(flow) - _add_tool_btn(flow, Strings.t(&"tool.chop"), &"chop", func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) - _add_tool_btn(flow, Strings.t(&"tool.mine"), &"mine", func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) - _add_tool_btn(flow, Strings.t(&"tool.dig_grave"),&"dig_grave", func() -> void: _activate(&"dig_grave", &"", Strings.t(&"tool.dig_grave"))) - _add_tool_btn(flow, Strings.t(&"tool.no_roof"), &"no_roof", func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) + _add_tool_btn(flow, Strings.t(&"tool.chop"), &"chop", func() -> void: _activate(&"chop", &"", Strings.t(&"tool.chop"))) + _add_tool_btn(flow, Strings.t(&"tool.mine"), &"mine", func() -> void: _activate(&"mine", &"", Strings.t(&"tool.mine"))) + _add_tool_btn(flow, Strings.t(&"tool.dig_grave"), &"dig_grave", func() -> void: _activate(&"dig_grave", &"", Strings.t(&"tool.dig_grave"))) + _add_tool_btn(flow, Strings.t(&"tool.no_roof"), &"no_roof", func() -> void: _activate(&"no_roof", &"", Strings.t(&"tool.no_roof"))) + _add_tool_btn(flow, Strings.t(&"tool.plant_tree"), &"plant_tree", func() -> void: _activate(&"plant_tree", &"", Strings.t(&"tool.plant_tree"))) return box @@ -237,6 +238,12 @@ func _build_build_tab() -> Control: &"build_workbench_cremation_pyre", func() -> void: _activate(&"build_workbench_cremation_pyre", &"", Strings.t(&"tool.workbench_cremation_pyre"))) + # Quarry — must be painted on a stone outcrop (BigRockNode); world.gd + # rejects placements on plain ground. + _add_tool_btn(flow, Strings.t(&"tool.paint_quarry"), + &"paint_quarry", + func() -> void: _activate(&"paint_quarry", &"", Strings.t(&"tool.paint_quarry"))) + return box diff --git a/scenes/ui/build_drawer_thumb.gd b/scenes/ui/build_drawer_thumb.gd index 22ec4d5..d6c07d9 100644 --- a/scenes/ui/build_drawer_thumb.gd +++ b/scenes/ui/build_drawer_thumb.gd @@ -332,6 +332,46 @@ func _draw() -> void: draw_line(Vector2(c.x, c.y + 6), Vector2(c.x, c.y - 6), arrow, 2.0) draw_line(Vector2(c.x, c.y - 6), Vector2(c.x - 4, c.y - 2), arrow, 2.0) draw_line(Vector2(c.x, c.y - 6), Vector2(c.x + 4, c.y - 2), arrow, 2.0) + &"paint_quarry": + # Stone block on wood frame + small pile of cut stones beside. + var frame := Color(0.42, 0.26, 0.12) + var frame_top := Color(0.55, 0.36, 0.18) + var stone := Color(0.65, 0.63, 0.60) + var stone_hi := Color(0.82, 0.80, 0.76) + var stone_dark := Color(0.32, 0.30, 0.28) + var chisel_steel := Color(0.78, 0.80, 0.85) + # Wood frame base. + draw_rect(Rect2(c.x - 14, c.y + 6, 28, 8), frame) + draw_rect(Rect2(c.x - 14, c.y + 3, 28, 3), frame_top) + # Large stone block on top. + draw_rect(Rect2(c.x - 10, c.y - 8, 14, 11), stone) + draw_rect(Rect2(c.x - 10, c.y - 8, 14, 3), stone_hi) + # Chisel marks (3 short dark lines on the stone face). + draw_line(Vector2(c.x - 6, c.y - 3), Vector2(c.x - 4, c.y - 3), stone_dark, 1.0) + draw_line(Vector2(c.x - 2, c.y - 3), Vector2(c.x, c.y - 3), stone_dark, 1.0) + draw_line(Vector2(c.x + 2, c.y - 3), Vector2(c.x - 0, c.y - 3), stone_dark, 1.0) + # Pile of cut stones beside the block. + draw_rect(Rect2(c.x + 6, c.y, 4, 3), stone) + draw_rect(Rect2(c.x + 8, c.y - 3, 3, 3), stone_hi) + # Chisel tool on top of block. + draw_rect(Rect2(c.x - 4, c.y - 12, 2, 4), chisel_steel) + draw_rect(Rect2(c.x - 5, c.y - 13, 4, 2), frame_top) + &"plant_tree": + # Green sapling sprout rising from dirt — signals manual tree planting. + var dirt := Color(0.45, 0.32, 0.18) + var dirt_hi := Color(0.62, 0.46, 0.28) + var stem := Color(0.35, 0.22, 0.10) + var leaf_a := Color(0.30, 0.65, 0.20) + var leaf_b := Color(0.25, 0.55, 0.18) + # Dirt base rectangle. + draw_rect(Rect2(c.x - 14, c.y + 2, 28, 14), dirt) + draw_line(Vector2(c.x - 14, c.y + 5), Vector2(c.x + 14, c.y + 5), dirt_hi, 1.0) + # Stem. + draw_line(Vector2(c.x, c.y + 2), Vector2(c.x, c.y - 8), stem, 2.0) + # Three leaf dots at top of stem. + draw_circle(Vector2(c.x, c.y - 10), 4.0, leaf_a) + draw_circle(Vector2(c.x - 5, c.y - 6), 3.0, leaf_b) + draw_circle(Vector2(c.x + 5, c.y - 5), 3.0, leaf_b) _: # Unknown tool — small grey placeholder. draw_rect(Rect2(c.x - 12, c.y - 12, 24, 24), Color(0.50, 0.50, 0.50)) diff --git a/scenes/ui/build_drawer_thumb.gd.uid b/scenes/ui/build_drawer_thumb.gd.uid new file mode 100644 index 0000000..6d94197 --- /dev/null +++ b/scenes/ui/build_drawer_thumb.gd.uid @@ -0,0 +1 @@ +uid://w802akkpbc6l diff --git a/scenes/ui/inspect_tooltip.gd b/scenes/ui/inspect_tooltip.gd index c3a716d..da793cd 100644 --- a/scenes/ui/inspect_tooltip.gd +++ b/scenes/ui/inspect_tooltip.gd @@ -264,12 +264,36 @@ func _describe_wolf(w) -> String: func _describe_tree(t) -> String: + # Growth stage label. + var stage: int = int(t.get("growth_stage")) if "growth_stage" in t else 3 + var stage_key_map: Array[StringName] = [ + &"tree.stage.sapling", + &"tree.stage.young", + &"tree.stage.growing", + &"tree.stage.mature", + ] + var stage_label: String = Strings.t(stage_key_map[clamp(stage, 0, 3)]) + + # Pending-plant ghost indicator. + var pending: bool = bool(t.get("pending_plant")) if "pending_plant" in t else false + if pending: + return "[b]%s[/b]\n[color=#aaa]awaiting pawn[/color]" % stage_label + + # Growth progress for sub-mature trees. + var is_mature: bool = (stage >= 3) + if not is_mature: + var progress: int = int(t.get("growth_progress")) if "growth_progress" in t else 0 + var stage_ticks: int = int(t.get("STAGE_TICKS")) if "STAGE_TICKS" in t else 1 + var pct_grow: int = int(100.0 * float(progress) / float(max(stage_ticks, 1))) + return "[b]%s[/b]\nGrowing %d%%" % [stage_label, pct_grow] + + # Mature tree — show chop progress if any. var pct: int = int(100.0 * float(t.chop_progress) / float(t.CHOP_TICKS)) var designated: bool = bool(t.get("chop_designated")) var tag := " · [color=#fc6]marked[/color]" if designated else "" if pct > 0: - return "[b]Tree[/b]\nChop %d%%%s" % [pct, tag] - return "[b]Tree[/b]%s" % tag + return "[b]%s[/b]\nChop %d%%%s" % [stage_label, pct, tag] + return "[b]%s[/b]%s" % [stage_label, tag] func _describe_rock(r) -> String: diff --git a/scenes/ui/medieval_theme.gd.uid b/scenes/ui/medieval_theme.gd.uid new file mode 100644 index 0000000..cb1f529 --- /dev/null +++ b/scenes/ui/medieval_theme.gd.uid @@ -0,0 +1 @@ +uid://c26o807ldrrrx diff --git a/scenes/world/designation.gd b/scenes/world/designation.gd index 5958268..b22ba5d 100644 --- a/scenes/world/designation.gd +++ b/scenes/world/designation.gd @@ -40,6 +40,10 @@ const TOOL_BUILD_WORKBENCH_HEARTH: StringName = &"build_workbench_hearth" const TOOL_BUILD_WORKBENCH_CREMATION_PYRE: StringName = &"build_workbench_cremation_pyre" # Phase 17 — Stockpile tab. const TOOL_PAINT_STOCKPILE: StringName = &"paint_stockpile" +# Tree planting — ghost sapling that ConstructionProvider will fulfil. +const TOOL_PLANT_TREE: StringName = &"plant_tree" +# Quarry — must paint on a BigRockNode tile; spawns a QuarryWorkbench ghost. +const TOOL_PAINT_QUARRY: StringName = &"paint_quarry" # ── tool → material override ───────────────────────────────────────────────── # For build_wall and build_floor the tool is shared but the material differs. @@ -73,6 +77,8 @@ const _ATLAS_BY_TOOL: Dictionary = { &"build_workbench_hearth": Vector2i(1, 0), &"build_workbench_cremation_pyre":Vector2i(3, 0), &"paint_stockpile": Vector2i(0, 0), + &"plant_tree": Vector2i(0, 0), # grass ghost — tinted green + &"paint_quarry": Vector2i(2, 0), # stone-grey ghost } # Placeholder source ID — mirrors World.PLACEHOLDER_SOURCE_ID. @@ -120,6 +126,8 @@ func set_active_tool(tool: StringName) -> void: TOOL_BUILD_WORKBENCH_MILLSTONE, TOOL_BUILD_WORKBENCH_HEARTH, TOOL_BUILD_WORKBENCH_CREMATION_PYRE, TOOL_PAINT_STOCKPILE, + TOOL_PLANT_TREE, + TOOL_PAINT_QUARRY, ], "Designation.set_active_tool: unknown tool '%s'" % tool ) diff --git a/scenes/world/world.gd b/scenes/world/world.gd index cb40443..d861ba9 100644 --- a/scenes/world/world.gd +++ b/scenes/world/world.gd @@ -32,6 +32,8 @@ const PAWN_SCENE: PackedScene = preload("res://scenes/pawn/pawn.tscn") const TREE_SCENE: PackedScene = preload("res://scenes/entities/tree.tscn") const ROCK_SCENE: PackedScene = preload("res://scenes/entities/rock.tscn") const BIG_ROCK_SCENE: PackedScene = preload("res://scenes/entities/big_rock.tscn") +const BIG_ROCK_NODE_SCENE: PackedScene = preload("res://scenes/entities/big_rock_node.tscn") +const QUARRY_WORKBENCH_SCENE: PackedScene = preload("res://scenes/entities/quarry_workbench.tscn") const STOCKPILE_SCENE: PackedScene = preload("res://scenes/world/stockpile_zone.tscn") const WALL_SCENE: PackedScene = preload("res://scenes/entities/wall.tscn") const FLOOR_SCENE: PackedScene = preload("res://scenes/entities/floor.tscn") @@ -60,10 +62,15 @@ const SAMPLE_PAWNS: Array[Dictionary] = [ ] # Phase 4 — sample harvestables. Trees clustered east, rocks south-east. +# Mix of 8 mature + 4 saplings so players see growth in action from day 1. const SAMPLE_TREES: Array[Vector2i] = [ Vector2i(58, 30), Vector2i(60, 31), Vector2i(62, 30), Vector2i(61, 33), Vector2i(63, 34), Vector2i(59, 35), + Vector2i(57, 28), Vector2i(64, 32), # 2 more mature + Vector2i(56, 36), Vector2i(65, 29), # 2 more mature ] +# The first 4 in SAMPLE_TREES_SAPLING are planted as saplings (stage 0). +const SAMPLE_TREES_SAPLING_COUNT: int = 4 const SAMPLE_ROCKS: Array[Vector2i] = [ Vector2i(60, 60), Vector2i(62, 60), Vector2i(63, 62), Vector2i(58, 62), ] @@ -75,9 +82,28 @@ const SAMPLE_BIG_ROCKS: Array[Vector2i] = [ Vector2i(56, 64), ] +# Permanent stone outcrops (BigRockNode). Scattered far from the cabin at +# (44, 22)..(51, 28) so the player has to scout / plan transport routes. +# Each is a 2×2 footprint that never depletes; player paints `paint_quarry` +# to build a QuarryWorkbench on it. +const SAMPLE_BIG_ROCK_NODES: Array[Vector2i] = [ + Vector2i(12, 30), # west, near map edge + Vector2i(68, 12), # north-east corner area + Vector2i(70, 60), # south-east corner +] + # HaulingProvider re-flow cadence — every 5 sim seconds at 1× (100 ticks). const HAUL_SWEEP_INTERVAL_TICKS: int = 100 +# WildGrowth — spontaneous sapling spawning on eligible grass tiles. +# 1200 ticks = 1 in-game hour at 20 Hz (20 ticks/s × 60 s/min = 1200 ticks/min, +# but 1 in-game minute = 20 ticks at 1× so 1 hour = 1200 ticks at 1×). +const WILD_GROWTH_INTERVAL: int = 1200 +const WILD_GROWTH_SPAWN_PROBABILITY: float = 0.30 +const MAP_TREE_LIMIT: int = 60 +# Rejection-sample attempts before giving up for this tick. +const WILD_GROWTH_MAX_ATTEMPTS: int = 10 + # Phase 11 — global darkness tint. Day = white, night = deep cool blue. # Driven by Clock.darkness_factor() (0..1) each sim tick. const NIGHT_TINT: Color = Color(0.20, 0.22, 0.40, 1.0) @@ -419,11 +445,15 @@ func _spawn_sample_harvestables() -> void: # Boot seed auto-designates so the production-chain demo runs end-to-end # without requiring a player to paint chop/mine first. Real player-painted # trees / rocks still gate on chop_designated / mine_designated (Rimworld parity). - for t_tile in SAMPLE_TREES: + for i in SAMPLE_TREES.size(): var tree = TREE_SCENE.instantiate() add_child(tree) - tree.setup(t_tile) - tree.chop_designated = true + # First SAMPLE_TREES_SAPLING_COUNT trees spawn as saplings (stage 0) + # so the player can observe growth from day 1. The rest are mature. + var stage: int = HarvestableTree.STAGE_SAPLING if i < SAMPLE_TREES_SAPLING_COUNT else HarvestableTree.STAGE_MATURE + tree.setup(SAMPLE_TREES[i], stage) + if stage == HarvestableTree.STAGE_MATURE: + tree.chop_designated = true for r_tile in SAMPLE_ROCKS: var rock = ROCK_SCENE.instantiate() add_child(rock) @@ -434,8 +464,13 @@ func _spawn_sample_harvestables() -> void: add_child(big) big.setup(br_origin) big.mine_designated = true - Audit.log("world", "spawned %d trees + %d rocks + %d big rocks" % [ - SAMPLE_TREES.size(), SAMPLE_ROCKS.size(), SAMPLE_BIG_ROCKS.size() + # Permanent stone outcrops (never deplete; Quarry workbench built on them). + for node_origin in SAMPLE_BIG_ROCK_NODES: + var node = BIG_ROCK_NODE_SCENE.instantiate() + add_child(node) + node.setup(node_origin) + Audit.log("world", "spawned %d trees + %d rocks + %d big rocks + %d stone outcrops" % [ + SAMPLE_TREES.size(), SAMPLE_ROCKS.size(), SAMPLE_BIG_ROCKS.size(), SAMPLE_BIG_ROCK_NODES.size() ]) @@ -747,6 +782,38 @@ func _on_designation_added(cell: Vector2i, tool: StringName) -> void: sz.accepted_types = [] as Array[StringName] # wildcard sz.queue_redraw() entity = sz + # Quarry — must be placed on a BigRockNode tile. Spawns a + # QuarryWorkbench ghost (auto-FOREVER bill on completion). + &"paint_quarry": + var node = World.big_rock_node_at_tile(cell) + if node == null: + Audit.log("world", "paint_quarry: %s not on a stone outcrop — rejected" % cell) + return + # Refuse if this node already has a quarry built/queued. + for ws in World.workbenches: + if "label_text" in ws and ws.label_text == "Quarry" and node.is_at(ws.tile): + Audit.log("world", "paint_quarry: outcrop at %s already has a quarry" % cell) + return + var quarry = QUARRY_WORKBENCH_SCENE.instantiate() + add_child(quarry) + quarry.setup(cell) + entity = quarry + # Tree planting — spawn a ghost sapling with pending_plant=true so + # ConstructionProvider can queue a build job (1 wood, 30 ticks of work). + # The ghost renders as a translucent sprout until the pawn completes it. + &"plant_tree": + # Reject if tile is already occupied by a tree. + for existing_t in World.trees: + if is_instance_valid(existing_t) and existing_t.tile == cell: + Audit.log("world", "plant_tree: tile %s already has a tree — skipped" % cell) + return + var pt = TREE_SCENE.instantiate() + add_child(pt) + pt.setup(cell, HarvestableTree.STAGE_SAPLING) + pt.pending_plant = true + # Register as a build site so ConstructionProvider can assign a pawn. + World.register_build_site(pt) + entity = pt _: Audit.log("world", "unknown designation tool: %s" % tool) return @@ -839,11 +906,88 @@ func _on_designation_cleared(cell: Vector2i) -> void: func _on_sim_tick_world_sweep(tick_n: int) -> void: _update_dark_overlay() + + # Tree growth — tick every registered tree; mature + pending-plant trees are + # no-ops inside on_sim_tick(), so iterating all is safe. + for tree in World.trees: + if is_instance_valid(tree): + tree.on_sim_tick() + + # WildGrowth — attempt to plant a new sapling once per WILD_GROWTH_INTERVAL. + if tick_n % WILD_GROWTH_INTERVAL == 0: + _try_wild_growth() + if tick_n % HAUL_SWEEP_INTERVAL_TICKS != 0: return hauling_provider.sweep_for_better_destinations() +## Attempt to spawn one wild sapling on a random eligible grass tile. +## Eligibility: walkable + grass terrain + no entity overlap + < 2 tree neighbours. +## Gives up after WILD_GROWTH_MAX_ATTEMPTS rejected tries to avoid lag spikes. +func _try_wild_growth() -> void: + if World.trees.size() >= MAP_TREE_LIMIT: + return + if randf() >= WILD_GROWTH_SPAWN_PROBABILITY: + return + var rng := RandomNumberGenerator.new() + rng.seed = Sim.tick + 9973 # stable within a tick, different each call cycle + for _attempt in WILD_GROWTH_MAX_ATTEMPTS: + var candidate := Vector2i( + rng.randi_range(0, MAP_SIZE_TILES.x - 1), + rng.randi_range(0, MAP_SIZE_TILES.y - 1) + ) + if not _wild_growth_tile_eligible(candidate): + continue + var tree = TREE_SCENE.instantiate() + add_child(tree) + tree.setup(candidate, HarvestableTree.STAGE_SAPLING) + Audit.log("world", "wild growth: sapling at %s (total %d)" % [candidate, World.trees.size()]) + return + + +## Returns true if `tile` is a valid WildGrowth spawn location. +## Checks: walkable, grass terrain (source 0, atlas (0,0)), no entity at tile, +## and fewer than 2 trees among the 4 cardinal neighbours. +func _wild_growth_tile_eligible(tile: Vector2i) -> bool: + # Bounds check (pathfinder bounds == map bounds). + if pathfinder == null: + return false + if not pathfinder.is_walkable(tile): + return false + # Grass terrain only — atlas (0,0) on source 0 = TILE_GRASS. + if terrain_layer == null: + return false + var src_id: int = terrain_layer.get_cell_source_id(tile) + var atlas: Vector2i = terrain_layer.get_cell_atlas_coords(tile) + if src_id != PLACEHOLDER_SOURCE_ID or atlas != TILE_GRASS: + return false + # No existing tree, rock, or item at this tile. + for t in World.trees: + if is_instance_valid(t) and t.tile == tile: + return false + for r in World.rocks: + if is_instance_valid(r): + if r.has_method("footprint_tiles"): + if tile in r.footprint_tiles(): + return false + elif r.tile == tile: + return false + for it in World.items: + if is_instance_valid(it) and it.tile == tile: + return false + # No clumping: reject if 2+ cardinal neighbours already have a tree. + var neighbour_trees: int = 0 + var offsets: Array[Vector2i] = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)] + for offset in offsets: + var nb: Vector2i = tile + offset + for t in World.trees: + if is_instance_valid(t) and t.tile == nb: + neighbour_trees += 1 + break + return neighbour_trees < 2 + + # Phase 11 — interpolate CanvasModulate between DAY_TINT and NIGHT_TINT based # on Clock.darkness_factor() (0 = full day, 1 = full night). # Called every sim tick; Color.lerp is a handful of float ops — negligible cost. From 5a6ec53b1263f7ac6322cea8700db5c5fdf11d08 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 16 May 2026 16:42:38 +0100 Subject: [PATCH 19/19] Tree growth: dedicated stage atlas + tuned WildGrowth rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sub-mature stages (0/1/2) now use FG_Tree_Stages.png (a 128×32 atlas with 8 progressively-larger tree cells from the bundle's "Crops with Stages 03" pack). Stage 0 = tiny sprout (col 0), Stage 1 = small leaf (col 1), Stage 2 = small tree (col 3). Stage 3 (Mature) keeps the existing 64×80 seasonal canopy atlases. Visually distinct progression replaces the previous scale-down-the- mature-texture placeholder + procedural sapling dots. WildGrowth pacing tuned: INTERVAL 1200 → 3000, PROBABILITY 0.30 → 0.12, LIMIT 60 → 80. Previous values flooded the map with saplings within ~30 seconds of 12× play. New rate gives a slow but visible regrowth over a season at default speed. _draw simplified: removed procedural sapling fallback (atlas handles all stages now). Pending-plant ghosts get the alpha tint via sprite.modulate. --- art/sprites/FG_Tree_Stages.png | Bin 0 -> 636 bytes art/sprites/FG_Tree_Stages.png.import | 40 +++++++++++++++ scenes/entities/tree.gd | 69 +++++++++++++------------- scenes/world/world.gd | 6 +-- 4 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 art/sprites/FG_Tree_Stages.png create mode 100644 art/sprites/FG_Tree_Stages.png.import diff --git a/art/sprites/FG_Tree_Stages.png b/art/sprites/FG_Tree_Stages.png new file mode 100644 index 0000000000000000000000000000000000000000..9b4881d872cd2bf19f1354b9ab9a740782aa0f7d GIT binary patch literal 636 zcmeAS@N?(olHy`uVBq!ia0vp^4M42G!3-qd{%H^gQjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`%>sNvT-l-n(x2v;Y)WNxv6kpeW6)JkV^II(#o)sraE5`Qhe51}LDff2 zE=@yYQ)J|ss;Zf`wm_}6-C?>wN~k2rFZe$?V0g5DzYtKCv%n*=n1O-sFbFdq&tH)O zbkIgm7srqY_orcRCp8)H==fSzugJLnKQ&)CDN|eF{`bSFQ*kSOs|=DLq(9&gKSNV=ZCuKNOKR&cgx)BYdv`LC8`+JwD6 z?E6E&iY@XI7hA-wOJA$*o^0T$wJBke01Hg~e&PP%1`fX#zku6yER2^Xt~cGFP|{Sw z(sSZ*S8792&imsR471D+g-gy5+_mQE0S$Rm2Enx~FYToye>Gh&bldr6$K9oU4N?!+ zo}9MkX|a>JRk7=|#^~QCWM^0Rl2Nf8alFZQQsl&oIJX41yJA$Hf{xc8C0`Q`$L void: ## Rebuild the Sprite2D child to match the current growth_stage. -## Sapling (stage 0): no Sprite2D — rendered procedurally in _draw(). -## Young (1) → scale 0.35, Growing (2) → 0.65, Mature (3) → 1.0. -## Any existing "Sprite" child is removed first so re-calls don't stack. +## Stages 0-2 use _STAGE_TEX (a dedicated growth-stage atlas with progressively +## larger trees per cell). Stage 3 (Mature) uses the seasonal full-canopy +## atlases. Any existing "Sprite" child is removed first so re-calls don't stack. func _refresh_sprite() -> void: var old := get_node_or_null("Sprite") if old != null: old.queue_free() - if growth_stage == STAGE_SAPLING: - # No Sprite2D for saplings — all rendering done in _draw(). - queue_redraw() - return - var scale_map: Array[float] = [1.0, 0.35, 0.65, 1.0] # indexed by stage - var sprite_scale: float = scale_map[growth_stage] var sprite := Sprite2D.new() sprite.name = "Sprite" - var hash_seed: int = tile.x * 31 + tile.y * 17 - var silhouette: int = hash_seed % _TREE_SILHOUETTES - # Independent hash mix for season so neighbouring tiles don't all match. - var season: int = ((hash_seed / _TREE_SILHOUETTES) + tile.x * 7 + tile.y * 11) % _TREE_TEXES.size() - sprite.texture = _TREE_TEXES[season] sprite.region_enabled = true - sprite.region_rect = Rect2(silhouette * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H) sprite.centered = true - # Lift the sprite up so its bottom edge sits at the tile's bottom row. - # Sprite center is at offset.y; sprite half-height is _TREE_VARIANT_H/2 = 40. - # We want bottom edge at +8 (tile bottom) → center at 8 - 40 = -32. - sprite.offset = Vector2(0, -32) - sprite.scale = Vector2(sprite_scale, sprite_scale) - # Render behind pawns/items that are at higher z_index; trees live at z=0. sprite.z_index = 0 + if growth_stage < STAGE_MATURE: + # Sub-mature: 16×32 cell from _STAGE_TEX. Bottom edge at tile bottom + # (+8); sprite half-height 16 → centre offset y = 8 - 16 = -8. + var col: int = _STAGE_COLS[growth_stage] + sprite.texture = _STAGE_TEX + sprite.region_rect = Rect2(col * _STAGE_CELL_W, 0, _STAGE_CELL_W, _STAGE_CELL_H) + sprite.offset = Vector2(0, -8) + else: + # Mature: full 64×80 seasonal canopy. Bottom at +8 → centre at -32. + var hash_seed: int = tile.x * 31 + tile.y * 17 + var silhouette: int = hash_seed % _TREE_SILHOUETTES + var season: int = ((hash_seed / _TREE_SILHOUETTES) + tile.x * 7 + tile.y * 11) % _TREE_TEXES.size() + sprite.texture = _TREE_TEXES[season] + sprite.region_rect = Rect2(silhouette * _TREE_VARIANT_W, 0, _TREE_VARIANT_W, _TREE_VARIANT_H) + sprite.offset = Vector2(0, -32) add_child(sprite) queue_redraw() @@ -280,19 +288,12 @@ static func from_dict(d: Dictionary) -> Dictionary: # ── render ──────────────────────────────────────────────────────────────────── func _draw() -> void: - # Sapling stage: draw a procedural sprout — no atlas sprite available. - # Three small green leaf-dots clustered above a thin brown stem. - if growth_stage == STAGE_SAPLING: - # Ghost tint for pending-plant saplings so the player can tell it's - # waiting for a pawn to build it. - var alpha := 0.55 if pending_plant else 1.0 - # Stem - draw_line(Vector2(0.0, 6.0), Vector2(0.0, 0.0), Color(0.35, 0.22, 0.10, alpha), 1.5) - # Three leaf dots - draw_circle(Vector2(0.0, -2.0), 2.5, Color(0.30, 0.65, 0.20, alpha)) - draw_circle(Vector2(-3.0, 1.0), 1.8, Color(0.25, 0.58, 0.18, alpha)) - draw_circle(Vector2(3.0, 0.5), 1.8, Color(0.28, 0.62, 0.19, alpha)) - return + # Ghost tint for pending-plant saplings — apply via modulate on the sprite + # child instead of drawing extra overlay shapes here. + if pending_plant and growth_stage == STAGE_SAPLING: + var s := get_node_or_null("Sprite") + if s != null: + s.modulate = Color(1, 1, 1, 0.55) # Mature / growing stages: canopy + trunk come from the Sprite2D child. # This _draw renders only the chop-progress notch overlaid on the trunk. diff --git a/scenes/world/world.gd b/scenes/world/world.gd index d861ba9..d36e2b0 100644 --- a/scenes/world/world.gd +++ b/scenes/world/world.gd @@ -98,9 +98,9 @@ const HAUL_SWEEP_INTERVAL_TICKS: int = 100 # WildGrowth — spontaneous sapling spawning on eligible grass tiles. # 1200 ticks = 1 in-game hour at 20 Hz (20 ticks/s × 60 s/min = 1200 ticks/min, # but 1 in-game minute = 20 ticks at 1× so 1 hour = 1200 ticks at 1×). -const WILD_GROWTH_INTERVAL: int = 1200 -const WILD_GROWTH_SPAWN_PROBABILITY: float = 0.30 -const MAP_TREE_LIMIT: int = 60 +const WILD_GROWTH_INTERVAL: int = 3000 # ~2.5 in-game hours between attempts +const WILD_GROWTH_SPAWN_PROBABILITY: float = 0.12 # 12% chance per attempt +const MAP_TREE_LIMIT: int = 80 # Rejection-sample attempts before giving up for this tick. const WILD_GROWTH_MAX_ATTEMPTS: int = 10