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:
@@ -0,0 +1,91 @@
|
||||
extends GutTest
|
||||
## Every sprite rect must land inside its texture.
|
||||
##
|
||||
## A wrong atlas coordinate does not error — it silently draws whatever happens
|
||||
## to be at those pixels, or nothing. Without a display to check against, this
|
||||
## is what catches a mistyped rect.
|
||||
|
||||
|
||||
func _fits(tex: Texture2D, r: Rect2) -> bool:
|
||||
var size := Vector2(tex.get_size())
|
||||
return r.position.x >= 0.0 and r.position.y >= 0.0 \
|
||||
and r.end.x <= size.x and r.end.y <= size.y
|
||||
|
||||
|
||||
func test_every_texture_loaded() -> void:
|
||||
for tex in [Art.TILESET, Art.BULLETS, Art.IMPACT]:
|
||||
assert_not_null(tex)
|
||||
assert_gt(tex.get_width(), 0)
|
||||
|
||||
|
||||
func test_terrain_rects_are_inside_the_tileset() -> void:
|
||||
var rects: Array[Rect2] = [Art.WALL, Art.WALL_TOP, Art.PIT, Art.BARRICADE]
|
||||
rects.append_array(Art.FLOOR_VARIANTS)
|
||||
for r in rects:
|
||||
assert_true(_fits(Art.TILESET, r), "terrain rect %s is outside the atlas" % r)
|
||||
|
||||
|
||||
## Actor strips are read frame by frame along the sheet, so the LAST frame is
|
||||
## what has to fit, not the first.
|
||||
func test_every_animation_frame_is_inside_the_tileset() -> void:
|
||||
var strips: Array[Rect2] = [Art.PLAYER_IDLE, Art.PLAYER_RUN, Art.BOSS_IDLE]
|
||||
strips.append_array(Art.ENEMY_IDLE)
|
||||
for strip in strips:
|
||||
for n in Art.ACTOR_FRAMES:
|
||||
var f := Art.frame(strip, n)
|
||||
assert_true(_fits(Art.TILESET, f),
|
||||
"frame %d of strip %s runs off the atlas at %s" % [n, strip, f])
|
||||
|
||||
|
||||
func test_every_bullet_frame_is_inside_the_sheet() -> void:
|
||||
for kind in Art.BULLET_CELLS.size():
|
||||
var cell: Vector2i = Art.BULLET_CELLS[kind]
|
||||
for n in Art.BULLET_FRAMES:
|
||||
var r := Rect2(
|
||||
Vector2(float(cell.x + n), float(cell.y)) * Art.BULLET_CELL,
|
||||
Vector2(Art.BULLET_CELL, Art.BULLET_CELL))
|
||||
assert_true(_fits(Art.BULLETS, r),
|
||||
"bullet kind %d frame %d is outside the sheet at %s" % [kind, n, r])
|
||||
|
||||
|
||||
## One entry per SimConfig.KIND_*, or a bullet kind silently renders as another.
|
||||
func test_there_is_a_sprite_for_every_bullet_kind() -> void:
|
||||
assert_eq(Art.BULLET_CELLS.size(), SimConfig.KIND_HEAVY + 1)
|
||||
|
||||
|
||||
## One entry per EnemyDef.visual actually used by content, or an enemy draws as
|
||||
## the wrong creature.
|
||||
func test_there_is_a_sprite_for_every_enemy_visual() -> void:
|
||||
for id in [Content.ENEMY_DRIFTER, Content.ENEMY_TURRET,
|
||||
Content.ENEMY_STALKER, Content.ENEMY_DUMMY]:
|
||||
var v := Content.enemy(id).visual
|
||||
assert_lt(v, Art.ENEMY_IDLE.size(), "%s has visual %d with no sprite" % [id, v])
|
||||
|
||||
|
||||
func test_impact_sheet_covers_its_declared_frames() -> void:
|
||||
var last := Rect2(
|
||||
Vector2(float((Art.IMPACT_FRAMES - 1) % Art.IMPACT_COLUMNS), 0.0) * Art.IMPACT_CELL,
|
||||
Vector2(Art.IMPACT_CELL, Art.IMPACT_CELL))
|
||||
assert_true(_fits(Art.IMPACT, last))
|
||||
|
||||
|
||||
func test_every_sound_loaded_and_is_short_enough_to_be_a_sound_effect() -> void:
|
||||
var sounds := {
|
||||
"shoot": Art.SFX_SHOOT,
|
||||
"player_hit": Art.SFX_PLAYER_HIT,
|
||||
"enemy_death": Art.SFX_ENEMY_DEATH,
|
||||
"boss_death": Art.SFX_BOSS_DEATH,
|
||||
}
|
||||
for name in sounds:
|
||||
var s: AudioStream = sounds[name]
|
||||
assert_not_null(s, "%s failed to load" % name)
|
||||
assert_gt(s.get_length(), 0.0, "%s is empty" % name)
|
||||
# The source pack ships 2.5s+ masters. Anything that long here means the
|
||||
# conversion step was skipped and shots will smear into each other.
|
||||
assert_lt(s.get_length(), 1.5, "%s is too long for a game SFX" % name)
|
||||
|
||||
|
||||
func test_the_shoot_sound_is_shorter_than_the_fire_cooldown_allows_to_overlap() -> void:
|
||||
var cooldown := float(SimConfig.PLAYER_FIRE_COOLDOWN) * SimConfig.TICK_DELTA
|
||||
assert_lt(Art.SFX_SHOOT.get_length(), cooldown * 4.0,
|
||||
"held fire would stack more than a few voices of this sound at once")
|
||||
Reference in New Issue
Block a user