From 2de5130ae0dba949181dfa1fb79991f8ec702d14 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Tue, 12 May 2026 14:19:06 +0100 Subject: [PATCH] Visual pass 2: tree + rock + stone wall sprite swaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces three procedural _draw() entities with bundle sprites: - Tree: was draw_rect trunk + draw_circle canopy. Now Sprite2D using FG_Tree_Spring.png (64x80, 4 variants picked deterministically from tile coord). Bottom-anchored so trunk base sits at tile bottom, canopy rises into the cell above; y_sort_enabled so canopies tuck behind pawns south of the trunk. Chop-progress notch overlay retained. - Rock: was draw_colored_polygon hex. Now Sprite2D reading from the existing FG_Grasslands_Spring.png decoration atlas at three eyeballed coords (2 gray boulders, 1 brown rock pile). Variant deterministic per tile. Mine-progress crack overlay retained. - Stone wall: was procedural top-band + front-band + mortar lines. Now Sprite2D from FG_Fortress.png at (1,1) — clean tan-stone brick fill. Bottom-anchored (offset.y=-8) so the 16x16 sprite spans y=-16..0, matching the procedural draw box exactly. Ghost state via modulate.a. Wood walls still use procedural _draw_wood_wall — no clean 16x16 wood tile found in the bundle yet (Pixel Crawler Walls.png is 32x32, would need crop+rescale). Asset additions: - art/sprites/FG_Tree_Spring.png (Tier 1, Grasslands pack) - FG_Fortress.png and FG_Grasslands_Spring.png were already in art/tiles from earlier passes; this commit just consumes them from new sites. Headless boots clean, runtime verified: trees look like chunky pixel-art trees with root flare, rocks read as real boulders, cabin walls show proper brick texture. License: all ElvGames Humble bundle — commercial OK with credit. Credit-string compilation still open. Co-Authored-By: Claude Opus 4.7 (1M context) --- art/sprites/FG_Tree_Spring.png | Bin 0 -> 3229 bytes art/sprites/FG_Tree_Spring.png.import | 40 ++++++++++++++++++ scenes/entities/rock.gd | 58 +++++++++++++------------- scenes/entities/tree.gd | 47 +++++++++++++++------ scenes/entities/wall.gd | 51 +++++++++++++++++----- 5 files changed, 145 insertions(+), 51 deletions(-) create mode 100644 art/sprites/FG_Tree_Spring.png create mode 100644 art/sprites/FG_Tree_Spring.png.import diff --git a/art/sprites/FG_Tree_Spring.png b/art/sprites/FG_Tree_Spring.png new file mode 100644 index 0000000000000000000000000000000000000000..cb396971c379c97ef9e8feedf6fc34540293c0a4 GIT binary patch literal 3229 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 void: position = _tile_to_world(tile) + _build_sprite() World.register_rock(self) +## Adds a Sprite2D child with one of the rock variants. Variant chosen +## deterministically from the tile coord so the same tile renders the same +## rock across boots and load/save. +func _build_sprite() -> void: + var sprite := Sprite2D.new() + sprite.name = "Sprite" + sprite.texture = _ROCK_TEX + sprite.region_enabled = true + var coord: Vector2i = _ROCK_VARIANT_COORDS[(tile.x * 31 + tile.y * 17) % _ROCK_VARIANT_COORDS.size()] + sprite.region_rect = Rect2(coord.x * TILE_SIZE_PX, coord.y * TILE_SIZE_PX, TILE_SIZE_PX, TILE_SIZE_PX) + sprite.centered = true + sprite.offset = Vector2.ZERO # 16×16 tile, sits centered on the tile + add_child(sprite) + + func _exit_tree() -> void: World.unregister_rock(self) @@ -99,35 +126,8 @@ static func from_dict(d: Dictionary) -> Dictionary: # ── render ──────────────────────────────────────────────────────────────────── func _draw() -> void: - # Angular cluster of 3–4 triangles in a dark-grey / light-grey palette. - var c1 := Color(0.55, 0.55, 0.50) # light face - var c2 := Color(0.38, 0.38, 0.36) # shadow face - - # Main body polygon (roughly an irregular hex). - var body := PackedVector2Array([ - Vector2(-5.0, 3.0), - Vector2(-6.0, -1.0), - Vector2(-2.0, -6.0), - Vector2(3.0, -5.0), - Vector2(6.0, 0.0), - Vector2(4.0, 4.0), - ]) - draw_colored_polygon(body, c1) - - # Shadow face on the bottom-right triangle to give depth. - var shadow := PackedVector2Array([ - Vector2(3.0, -5.0), - Vector2(6.0, 0.0), - Vector2(4.0, 4.0), - Vector2(-5.0, 3.0), - ]) - draw_colored_polygon(shadow, c2) - - # Outline. - draw_polyline(body, Color(0.0, 0.0, 0.0, 0.5), 1.0) - draw_line(body[5], body[0], Color(0.0, 0.0, 0.0, 0.5), 1.0) - - # Mine-progress crack: a dark jagged line on the face when partially mined. + # Rock body comes from the Sprite2D child (see _build_sprite). + # This _draw renders only the mine-progress crack overlaid on the sprite. if mine_progress > 0: var ratio := float(mine_progress) / float(MINE_TICKS) var crack_len := ratio * 5.0 diff --git a/scenes/entities/tree.gd b/scenes/entities/tree.gd index d7e7208..e9259d8 100644 --- a/scenes/entities/tree.gd +++ b/scenes/entities/tree.gd @@ -31,14 +31,47 @@ var chop_progress: int = 0 # 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") +const _TREE_VARIANT_W: int = 64 +const _TREE_VARIANT_H: int = 80 +const _TREE_VARIANT_COUNT: int = 4 + # ── lifecycle ───────────────────────────────────────────────────────────────── func _ready() -> void: position = _tile_to_world(tile) + _build_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 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. +func _build_sprite() -> void: + var sprite := Sprite2D.new() + sprite.name = "Sprite" + sprite.texture = _TREE_TEX + 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.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) + # Render behind pawns/items that are at higher z_index; trees live at z=0. + sprite.z_index = 0 + add_child(sprite) + + func _exit_tree() -> void: World.unregister_tree(self) @@ -106,18 +139,8 @@ static func from_dict(d: Dictionary) -> Dictionary: # ── render ──────────────────────────────────────────────────────────────────── func _draw() -> void: - # Brown trunk: small filled rect at centre-bottom (~4 wide × 6 tall). - var trunk_color := Color(0.45, 0.28, 0.12) - draw_rect(Rect2(Vector2(-2.0, 1.0), Vector2(4.0, 6.0)), trunk_color) - - # Green canopy: large filled circle centered near the top. - var canopy_color := Color(0.22, 0.60, 0.18) - draw_circle(Vector2(0.0, -3.0), 7.0, canopy_color) - - # Canopy outline. - draw_arc(Vector2(0.0, -3.0), 7.0, 0.0, TAU, 24, Color(0.0, 0.0, 0.0, 0.4), 1.0) - - # Chop-progress wedge: a dark angled line on the trunk when partially chopped. + # Canopy + trunk now come from the Sprite2D child (see _build_sprite). + # This _draw renders only the chop-progress notch overlaid on the trunk. if chop_progress > 0: var ratio := float(chop_progress) / float(CHOP_TICKS) var notch_depth := ratio * 3.0 diff --git a/scenes/entities/wall.gd b/scenes/entities/wall.gd index 4a7b9bf..14fd709 100644 --- a/scenes/entities/wall.gd +++ b/scenes/entities/wall.gd @@ -23,6 +23,13 @@ const TILE_SIZE_PX: int = 16 ## Sim ticks to complete construction at 1× speed (100 ticks = 5 sim seconds). const BUILD_TICKS: int = 100 +## ElvGames Fortress tileset — coord (1, 1) is a plain tan-stone fill tile. +## Eyeballed from /tmp/walls/probe.png in the 2026-05-12 visual pass. +## We use a single sprite per material (Phase 5 lock: no autotile yet). +const _STONE_TEX: Texture2D = preload("res://art/tiles/FG_Fortress.png") +const _STONE_FILL_COORD: Vector2i = Vector2i(1, 1) + + ## Supported materials. Phase 5 uses MATERIAL_STONE; MATERIAL_WOOD is reserved ## for the Phase 6+ art-authoring pass. const MATERIAL_STONE: StringName = &"stone" @@ -61,10 +68,35 @@ func setup(p_tile: Vector2i, p_material: StringName) -> void: tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, tile.y * TILE_SIZE_PX + TILE_SIZE_PX ) + # Stone uses a sprite from FG_Fortress; wood still draws procedurally below + # until we find a 16×16 wood-wall tile that fits the perspective. + if wall_material == MATERIAL_STONE: + _build_stone_sprite() queue_redraw() Audit.log("wall", "%s wall ghost placed at %s" % [wall_material, tile]) +## Builds the stone-fill Sprite2D child. Bottom-anchored so it sits flush with +## the tile's bottom edge (matching the procedural draw box y=-16..0). +func _build_stone_sprite() -> void: + var sprite := Sprite2D.new() + sprite.name = "Sprite" + sprite.texture = _STONE_TEX + sprite.region_enabled = true + sprite.region_rect = Rect2( + _STONE_FILL_COORD.x * TILE_SIZE_PX, + _STONE_FILL_COORD.y * TILE_SIZE_PX, + TILE_SIZE_PX, + TILE_SIZE_PX, + ) + sprite.centered = true + # Sprite center at y=-8 so 16×16 sprite spans y=-16..0 (matches procedural). + sprite.offset = Vector2(0, -8) + # Ghost state — translucent until built. + sprite.modulate.a = 1.0 if _completed else 0.4 + add_child(sprite) + + ## True while the wall still needs construction work. ## JobRunner's _tick_build checks this to decide when the toil is done. func is_buildable() -> bool: @@ -126,18 +158,11 @@ static func from_dict(d: Dictionary) -> Dictionary: # ── render ───────────────────────────────────────────────────────────────────── func _draw() -> void: - # 3/4-perspective wall rendering — fits WITHIN the wall's own tile so it - # never encroaches on adjacent floor/interior tiles. Two-band look: - # Top band (lit) = the wall's "top surface" (looking down at it) - # Bottom band (dark) = the wall's "front face" (looking at the side) - # - # Origin (0, 0) is at the tile's bottom-centre. Tile spans local Y: -16 to 0. - # We draw entirely within that 16×16 box. + # Stone walls render via the Sprite2D child (see _build_stone_sprite). + # Wood walls still draw procedurally until a wood-wall sprite lands. var alpha: float = 1.0 if _completed else 0.4 - if wall_material == MATERIAL_STONE: - _draw_stone_wall(alpha) - else: + if wall_material == MATERIAL_WOOD: _draw_wood_wall(alpha) @@ -187,5 +212,11 @@ func _complete() -> void: # Stamp the data-layer TileMap so room / roof / save logic sees the wall. World.mark_wall_tile(tile, wall_material) + # Solidify the ghost: sprite (if any) → full opacity; wood _draw rereads alpha. + var sprite: Sprite2D = get_node_or_null("Sprite") + if sprite != null: + sprite.modulate.a = 1.0 + queue_redraw() + queue_redraw() Audit.log("wall", "%s wall completed at %s" % [wall_material, tile])