Visual pass 2: tree + rock + stone wall sprite swaps
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) <noreply@anthropic.com>
This commit is contained in:
parent
314c7a70b4
commit
2de5130ae0
5 changed files with 145 additions and 51 deletions
BIN
art/sprites/FG_Tree_Spring.png
Normal file
BIN
art/sprites/FG_Tree_Spring.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
40
art/sprites/FG_Tree_Spring.png.import
Normal file
40
art/sprites/FG_Tree_Spring.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bk8qj28u6iafb"
|
||||
path="res://.godot/imported/FG_Tree_Spring.png-0348f219e087c5b2725bff866605af1b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://art/sprites/FG_Tree_Spring.png"
|
||||
dest_files=["res://.godot/imported/FG_Tree_Spring.png-0348f219e087c5b2725bff866605af1b.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
|
||||
|
|
@ -27,14 +27,41 @@ var mine_progress: int = 0
|
|||
# Preloaded scene for spawned stone items.
|
||||
const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn")
|
||||
|
||||
## Rock sprite atlas — re-uses the Grasslands tileset (already imported for the
|
||||
## decoration overlay). The three coords below were eyeballed from the
|
||||
## /tmp/rocks_review.png probe in the 2026-05-12 visual pass: two gray boulders
|
||||
## and one brown rock for variety.
|
||||
const _ROCK_TEX: Texture2D = preload("res://art/tiles/FG_Grasslands_Spring.png")
|
||||
const _ROCK_VARIANT_COORDS: Array[Vector2i] = [
|
||||
Vector2i(24, 7), # gray boulder A
|
||||
Vector2i(28, 7), # gray boulder B
|
||||
Vector2i(12, 13), # brown rock pile
|
||||
]
|
||||
|
||||
|
||||
# ── lifecycle ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func _ready() -> 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue