From 5ba185f7752c64b19a13d031cd8429c93168662f Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Fri, 14 Aug 2026 22:39:03 -0700 Subject: [PATCH] feat: wire _ph placeholder sprites into actors (mech, grunt, burster, slug, scrap) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator asked for placeholder sprites for playtest feel. Actors now render the hand-authored pixel maps (tools/aseprite/maps -> assets/ sprites_ph, committed) via code-created Sprite2D — no scene texture UIDs to hand-author. Dynamic feedback preserved: grunt/burster heat tint + hit flash drive sprite.modulate, heat-aura ring stays a _draw overlay; salvage fade drives modulate.a. Arena floor/walls, HUD, and FX stay primitives. CLAUDE.md greybox-art note updated (was 'no sprites'); real art (watercolor scenery + pixel sprites) still later. Smoke gate 3/3 green on nh3-dev headless 4.7.1. --- .gitignore | 3 -- CLAUDE.md | 12 +++++-- assets/sprites_ph/burster_ph.png | Bin 0 -> 271 bytes assets/sprites_ph/burster_ph.png.import | 40 ++++++++++++++++++++++++ assets/sprites_ph/grunt_ph.png | Bin 0 -> 194 bytes assets/sprites_ph/grunt_ph.png.import | 40 ++++++++++++++++++++++++ assets/sprites_ph/m03_ph.png | Bin 0 -> 385 bytes assets/sprites_ph/m03_ph.png.import | 40 ++++++++++++++++++++++++ assets/sprites_ph/scrap_ph.png | Bin 0 -> 152 bytes assets/sprites_ph/scrap_ph.png.import | 40 ++++++++++++++++++++++++ assets/sprites_ph/slug_ph.png | Bin 0 -> 107 bytes assets/sprites_ph/slug_ph.png.import | 40 ++++++++++++++++++++++++ scenes/burster.tscn | 1 + scripts/grunt.gd | 33 ++++++++++++++----- scripts/kinetic_projectile.gd | 10 +++--- scripts/mech.gd | 11 ++++--- scripts/salvage.gd | 17 +++++----- 17 files changed, 256 insertions(+), 31 deletions(-) create mode 100644 assets/sprites_ph/burster_ph.png create mode 100644 assets/sprites_ph/burster_ph.png.import create mode 100644 assets/sprites_ph/grunt_ph.png create mode 100644 assets/sprites_ph/grunt_ph.png.import create mode 100644 assets/sprites_ph/m03_ph.png create mode 100644 assets/sprites_ph/m03_ph.png.import create mode 100644 assets/sprites_ph/scrap_ph.png create mode 100644 assets/sprites_ph/scrap_ph.png.import create mode 100644 assets/sprites_ph/slug_ph.png create mode 100644 assets/sprites_ph/slug_ph.png.import diff --git a/.gitignore b/.gitignore index 37d81fd..7135c97 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,3 @@ # OS noise .DS_Store - -# Generated placeholder sprites (regenerate via tools/aseprite/) -assets/sprites_ph/ diff --git a/CLAUDE.md b/CLAUDE.md index 0da257a..b251b83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,7 +116,12 @@ Non-negotiable canon highlights (full rationale in CONCEPT.md): yet). - 2D, pixel-first: integer scaling, nearest-neighbor filtering (set in project.godot). Hi-bit mixed-res effects layer comes - later — greybox is deliberately art-free. + later. Actors now use `_ph` placeholder sprites (hand-authored + pixel maps in `tools/aseprite/maps/`, rendered to + `assets/sprites_ph/`); arena floor/walls, HUD, and FX stay + primitives. Real art (watercolor scenery + pixel sprites, CONCEPT.md + § Tech & art direction) comes at the art pass — placeholders only, + no polish, until then. ## Current phase: GREYBOX (v1 per ROADMAP.md) @@ -126,5 +131,6 @@ KINETIC) · the KINETIC+HEAT chemistry row (knock a heated enemy into others → heat spreads on impact) · two enemy types (untagged grunt swarm + self-heating burster). It exists to falsify three bets: positioning ↔ build interaction, card-freeze rhythm, -chemistry delight. Greybox art: primitives and placeholder shapes -only — no sprites, no polish. +chemistry delight. Greybox art: `_ph` placeholder pixel sprites for +actors, primitives for arena/HUD/FX — no polish; the real art pass +comes later. diff --git a/assets/sprites_ph/burster_ph.png b/assets/sprites_ph/burster_ph.png new file mode 100644 index 0000000000000000000000000000000000000000..225e38cd28ae9f1742efa4e6283f7bf2e13103ac GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE{`Dd&9P?>$R+zj*bi;HbZFj%#A%D9 z+pap4I2SVJv^_KtNYk)m-Y9wOK!ftl0}N-T0-c-OBI$7AX$9|_eNJ<3Twr40Qw_ z0|P@$d>Sdri2^LdD)ex+BBIIxl z_gwHjbq5NcE+K&IV}?}*q&Z;6-eW{1KwNqm81UB%gS#Ml_UIntgzg%=g%2*UWA8DB z3EedWDXYM-Zcqlx7)0g}5w5 zw}e=A=zyq#2tDXwX@XpJ$Y5}n1M~nyMZtkxGl?>cSaryNV55{6Ibzje14ML96{@+|&-NOR}J})MLDD!JGjSu)FHF+9k>${jquImt-%_)%W vXLMjq!|C~#bg~)n+4km_TdCiqC?)jYpTE*b$>gTe~DWM4fp8Phd literal 0 HcmV?d00001 diff --git a/assets/sprites_ph/scrap_ph.png.import b/assets/sprites_ph/scrap_ph.png.import new file mode 100644 index 0000000..b8ad36f --- /dev/null +++ b/assets/sprites_ph/scrap_ph.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://76bnxqxl7uxt" +path="res://.godot/imported/scrap_ph.png-780db8d2b3ed50cbb4f3dc204a549011.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/sprites_ph/scrap_ph.png" +dest_files=["res://.godot/imported/scrap_ph.png-780db8d2b3ed50cbb4f3dc204a549011.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/assets/sprites_ph/slug_ph.png b/assets/sprites_ph/slug_ph.png new file mode 100644 index 0000000000000000000000000000000000000000..eb0c11e0f50de95e31c246e9c585189b6977aa69 GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^96-#>!3HGPo$&JjQbwLGjv*Cul2Z~A5++Ov{&l{A z)uy_ne`27Q-^>4RXPXD8_}TwDcmN0lo@O05;BkV5;Z7xg?2>2(1)y#QPgg&ebxsLQ E08DrzaR2}S literal 0 HcmV?d00001 diff --git a/assets/sprites_ph/slug_ph.png.import b/assets/sprites_ph/slug_ph.png.import new file mode 100644 index 0000000..16a39c5 --- /dev/null +++ b/assets/sprites_ph/slug_ph.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bnbsde6j7gwsi" +path="res://.godot/imported/slug_ph.png-c68013faba14e2a7a9a40558a57d8215.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/sprites_ph/slug_ph.png" +dest_files=["res://.godot/imported/slug_ph.png-c68013faba14e2a7a9a40558a57d8215.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/burster.tscn b/scenes/burster.tscn index 5edfdd0..5d75658 100644 --- a/scenes/burster.tscn +++ b/scenes/burster.tscn @@ -15,6 +15,7 @@ contact_damage = 3.0 body_radius = 11.0 base_color = Color(0.6, 0.4, 0.25, 1) salvage_value = 3 +sprite_path = "res://assets/sprites_ph/burster_ph.png" self_heat_rate = 0.5 detonation_damage = 2.5 detonation_mech_damage = 15.0 diff --git a/scripts/grunt.gd b/scripts/grunt.gd index 37f481a..81435b1 100644 --- a/scripts/grunt.gd +++ b/scripts/grunt.gd @@ -28,6 +28,8 @@ signal died @export var body_radius := 8.0 @export var base_color := Color(0.45, 0.47, 0.4) @export var salvage_value := 1 +## Placeholder sprite; overridden per scene (grunt vs burster). +@export var sprite_path := "res://assets/sprites_ph/grunt_ph.png" ## > 0 makes this a burster: heat/second it applies to itself. @export var self_heat_rate := 0.0 @@ -54,6 +56,7 @@ var _chem_cd := 0.0 var _flash := 0.0 var _dead := false var _mech: Node2D +var _sprite: Sprite2D func _ready() -> void: @@ -61,6 +64,10 @@ func _ready() -> void: if self_heat_rate > 0.0: add_to_group("bursters") _mech = get_tree().get_first_node_in_group("mech") + _sprite = Sprite2D.new() + _sprite.texture = load(sprite_path) + add_child(_sprite) + _update_tint() func _physics_process(delta: float) -> void: @@ -100,6 +107,20 @@ func _physics_process(delta: float) -> void: if _flash > 0.0: _flash -= delta queue_redraw() + _update_tint() + + +## The sprite carries the dynamic feedback the primitive used to: heat +## tints the body toward orange (HEAT's read), a hit flashes it bright. +## The heat-aura ring stays a _draw overlay (below the sprite). +func _update_tint() -> void: + if _sprite == null: + return + if _flash > 0.0: + _sprite.modulate = Color(2.5, 2.5, 2.5) + return + var heat_k := clampf(heat / heat_threshold, 0.0, 1.0) + _sprite.modulate = Color(1, 1, 1).lerp(Color(1.5, 0.75, 0.4), heat_k) func hit(damage: float, kb: Vector2) -> void: @@ -108,6 +129,7 @@ func hit(damage: float, kb: Vector2) -> void: hp -= damage _knockback += kb _flash = 0.12 + _update_tint() queue_redraw() if hp <= 0.0: _die() @@ -117,6 +139,7 @@ func add_heat(amount: float) -> void: if _dead: return heat += amount + _update_tint() queue_redraw() @@ -146,12 +169,8 @@ func _die() -> void: func _draw() -> void: - var body := base_color + # Body is now the sprite; _draw carries only the heat-aura ring + # (renders under the sprite, extends past it — a visible glow). var heat_k := clampf(heat / heat_threshold, 0.0, 1.0) if heat_k > 0.0: - body = body.lerp(Color(0.95, 0.5, 0.15), heat_k * 0.8) - if _flash > 0.0: - body = Color(0.95, 0.95, 0.95) - draw_circle(Vector2.ZERO, body_radius, body) - if heat_k > 0.0: - draw_arc(Vector2.ZERO, body_radius + 2.0, 0.0, TAU, 24, Color(0.95, 0.55, 0.2, 0.4 + heat_k * 0.5), 1.5) + draw_arc(Vector2.ZERO, body_radius + 3.0, 0.0, TAU, 24, Color(0.95, 0.55, 0.2, 0.4 + heat_k * 0.5), 1.5) diff --git a/scripts/kinetic_projectile.gd b/scripts/kinetic_projectile.gd index c39b0c9..e425f2e 100644 --- a/scripts/kinetic_projectile.gd +++ b/scripts/kinetic_projectile.gd @@ -9,12 +9,17 @@ extends Area2D @export var knockback := 220.0 @export var lifetime := 0.8 +const SPRITE := preload("res://assets/sprites_ph/slug_ph.png") + var direction := Vector2.RIGHT func _ready() -> void: add_to_group("projectiles") rotation = direction.angle() + var spr := Sprite2D.new() + spr.texture = SPRITE + add_child(spr) body_entered.connect(_on_body_entered) @@ -29,8 +34,3 @@ func _on_body_entered(body: Node) -> void: if body.is_in_group("enemies"): body.hit(damage, direction * knockback) queue_free() - - -func _draw() -> void: - # KINETIC hue: pale steel-blue (placeholder — tag colors unassigned in canon). - draw_rect(Rect2(-5, -1.5, 10, 3), Color(0.75, 0.85, 0.95)) diff --git a/scripts/mech.gd b/scripts/mech.gd index fcf6d07..be1bb09 100644 --- a/scripts/mech.gd +++ b/scripts/mech.gd @@ -16,9 +16,15 @@ var hull: float var _dash := Vector2.ZERO +const SPRITE := preload("res://assets/sprites_ph/m03_ph.png") + + func _ready() -> void: add_to_group("mech") hull = hull_max + var spr := Sprite2D.new() + spr.texture = SPRITE + add_child(spr) func _physics_process(delta: float) -> void: @@ -42,8 +48,3 @@ func take_damage(amount: float) -> void: if hull <= 0.0: # The run manager decides what death means (tally + relaunch). destroyed.emit() - - -func _draw() -> void: - draw_rect(Rect2(-10, -12, 20, 24), Color(0.82, 0.84, 0.88)) - draw_rect(Rect2(-6, -6, 12, 12), Color(0.55, 0.6, 0.68)) diff --git a/scripts/salvage.gd b/scripts/salvage.gd index 9c93401..2f28a76 100644 --- a/scripts/salvage.gd +++ b/scripts/salvage.gd @@ -4,12 +4,15 @@ extends Area2D ## levels). Drifts to the mech inside the magnet radius, collected on ## contact, fades out if ignored. Constructed in code, no scene. +const SPRITE := preload("res://assets/sprites_ph/scrap_ph.png") + var value := 1 var magnet_radius := 70.0 var drift_speed := 150.0 var lifetime := 20.0 var _t := 0.0 +var _sprite: Sprite2D func _ready() -> void: @@ -22,6 +25,9 @@ func _ready() -> void: sh.radius = 6.0 cs.shape = sh add_child(cs) + _sprite = Sprite2D.new() + _sprite.texture = SPRITE + add_child(_sprite) body_entered.connect(_on_body_entered) @@ -33,8 +39,9 @@ func _physics_process(delta: float) -> void: var mech := get_tree().get_first_node_in_group("mech") as Node2D if mech != null and global_position.distance_to(mech.global_position) <= magnet_radius: global_position = global_position.move_toward(mech.global_position, drift_speed * delta) - if _t >= lifetime - 3.0: - queue_redraw() + # Fade the sprite out over the last 3s of life. + if _t >= lifetime - 3.0 and _sprite != null: + _sprite.modulate.a = clampf((lifetime - _t) / 3.0, 0.0, 1.0) func _on_body_entered(body: Node) -> void: @@ -43,9 +50,3 @@ func _on_body_entered(body: Node) -> void: if hand != null: hand.add_salvage(value) queue_free() - - -func _draw() -> void: - var a := 1.0 if _t < lifetime - 3.0 else clampf((lifetime - _t) / 3.0, 0.0, 1.0) - var pts := PackedVector2Array([Vector2(0, -5), Vector2(4, 0), Vector2(0, 5), Vector2(-4, 0)]) - draw_colored_polygon(pts, Color(0.95, 0.8, 0.35, a))