Replace placeholder rendering with real sprites and audio

Terrain, actors, boss and bullets are sprites now, and four sounds play off
server events. 548KB total across seven files, out of 3.9GB of source packs.

- src/view/art.gd holds every atlas rect and sound path in one table, copied
  from the 0x72 pack's own tile_list. Scattering coordinates through draw calls
  would make re-cutting the atlas a hunt, and a wrong rect invisible.
- Bullets get one MultiMesh per kind rather than one overall: a
  MultiMeshInstance2D carries a single texture and each kind needs a different
  region of the sheet. Four draw calls, no custom shader passing UVs through
  per-instance data.
- src/view/sfx.gd is a 16-voice round-robin pool. Bullet-hell fire rates mean
  sounds overlap constantly, and identical sounds landing within two frames are
  collapsed so a ring hitting eight bullets is a bang rather than clipping.
  Every sound is triggered by a server event, never a local guess, so what you
  hear matches what happened.

Audio was converted, not copied: the pack ships 24-bit/96kHz masters averaging
2MB. Downsampled to 16-bit/44.1kHz mono, silence-trimmed, and the shoot sound
hard-capped to 0.30s -- it came out at 2.03s, against a fire cooldown of 0.12s,
which would have smeared held fire into noise.

tests/unit/test_art.gd checks what cannot be eyeballed here: every atlas rect
lands inside its texture, every animation frame of a strip fits (the last frame
is what runs off the sheet, not the first), there is a sprite per bullet kind
and per enemy visual, and no sound is long enough to stack badly. A wrong atlas
coordinate does not error -- it silently draws the wrong pixels.

Licence sources recorded in docs/ASSETS.md now that they are known. All four
are free versions and four rows still say "confirm at source": free itch packs
vary on credit and commercial use, and there is no credits screen yet.

155 tests (was 146). check.sh, test.sh and smoke.sh pass.
This commit is contained in:
2026-09-03 23:43:13 +02:00
parent 7ee6c8e761
commit 9fa2e260f2
23 changed files with 563 additions and 113 deletions
+47
View File
@@ -0,0 +1,47 @@
extends Node
## Sound effects, played from the events the client already receives.
##
## Strictly a view concern: sound is triggered by server events arriving, never
## by anything the client decides for itself, so what you hear matches what
## actually happened. Nothing under src/sim/ knows this exists.
## Voices in the pool. Bullet-hell fire rates mean sounds overlap constantly;
## one player would cut its own tail off on every shot.
const VOICES := 16
## Identical sounds landing within this many frames are collapsed into one, so
## a boss ring hitting eight bullets at once is a bang rather than a wall of
## clipping.
const DEDUPE_FRAMES := 2
var _voices: Array[AudioStreamPlayer] = []
var _next: int = 0
var _last_played: Dictionary = {}
var _frame: int = 0
func _ready() -> void:
for i in VOICES:
var p := AudioStreamPlayer.new()
p.bus = "Master"
add_child(p)
_voices.append(p)
func _process(_delta: float) -> void:
_frame += 1
func play(stream: AudioStream, volume_db: float = 0.0) -> void:
if stream == null:
return
var last: int = _last_played.get(stream, -999)
if _frame - last < DEDUPE_FRAMES:
return
_last_played[stream] = _frame
# Round-robin rather than "find a free voice": with this many voices the
# oldest is always the best one to steal, and it needs no search.
var v := _voices[_next]
_next = (_next + 1) % _voices.size()
v.stream = stream
v.volume_db = volume_db
v.play()