4a41ccb600
ci / verify (push) Successful in 47s
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.
92 lines
3.5 KiB
GDScript
92 lines
3.5 KiB
GDScript
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")
|