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_the_committed_texture_loads() -> void: assert_not_null(Art.TILESET) assert_gt(Art.TILESET.get_width(), 0) ## The licence-restricted sheets are absent from a clean clone by design, so ## their absence must be a supported state rather than a failure. When they are ## present locally, the rect tests below still validate them. func test_the_game_runs_without_the_restricted_art() -> void: var bullets := Art.bullets_texture() if bullets == null: assert_true(true, "no bullet sheet: renderer falls back to generated dots") else: assert_gt(bullets.get_width(), 0) func test_terrain_rects_are_inside_the_tileset() -> void: var rects: Array[Rect2] = [Art.WALL, Art.WALL_CAP, Art.PIT, Art.BARRICADE, Art.PILLAR] 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: var sheet := Art.bullets_texture() if sheet == null: pass_test("bullet sheet not present locally; nothing to validate") return for kind in SimConfig.KIND_HEAVY + 1: for n in Art.BULLET_FRAMES: var r := Art.bullet_region(kind, n) assert_true(_fits(sheet, r), "bullet kind %d frame %d is outside the sheet at %s" % [kind, n, r]) ## The atlas has one row per kind. A short sheet would silently draw the wrong ## bullet rather than error. func test_the_bullet_atlas_has_a_row_for_every_kind() -> void: var sheet := Art.bullets_texture() if sheet == null: pass_test("bullet sheet not present locally") return var needed := float(SimConfig.KIND_HEAVY + 1) * Art.BULLET_CELL assert_gte(float(sheet.get_height()), needed, "atlas is too short for %d bullet kinds" % (SimConfig.KIND_HEAVY + 1)) assert_gte(float(sheet.get_width()), Art.BULLET_FRAMES * Art.BULLET_CELL) ## 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.ALL_ENEMIES: 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 impact := Art.impact_texture() if impact == null: pass_test("impact sheet not present locally; nothing to validate") return 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(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") ## The wall cap is mostly transparent by design -- it is an overlay for the top ## edge of a wall, not a tile. Drawing it on its own is what produced a black ## void along every wall, so this records why it must never be used alone. func test_the_wall_cap_is_mostly_transparent() -> void: var img := Art.TILESET.get_image() var clear := 0 for y in int(Art.WALL_CAP.size.y): for x in int(Art.WALL_CAP.size.x): var a := img.get_pixel(int(Art.WALL_CAP.position.x) + x, int(Art.WALL_CAP.position.y) + y).a if a < 0.15: clear += 1 assert_gt(clear, 100, "if this ever becomes opaque the cap could stand alone, but while it " + "is mostly empty it must always be drawn over floor and wall") ## The wall face, by contrast, has to be solid: it is the one tile drawn with ## nothing but floor beneath it. func test_the_wall_face_is_opaque() -> void: var img := Art.TILESET.get_image() var clear := 0 for y in int(Art.WALL.size.y): for x in int(Art.WALL.size.x): if img.get_pixel(int(Art.WALL.position.x) + x, int(Art.WALL.position.y) + y).a < 0.15: clear += 1 assert_eq(clear, 0, "the wall face must not have holes in it") ## The player anchor exists because the knight is bottom-heavy in its cell. ## If the art is ever re-cut, this catches the anchor silently becoming wrong. func test_the_player_anchor_matches_where_the_knight_actually_is() -> void: var img := Art.TILESET.get_image() var top := -1 var bottom := -1 for y in int(Art.PLAYER_IDLE.size.y): for x in int(Art.PLAYER_IDLE.size.x): if img.get_pixel(int(Art.PLAYER_IDLE.position.x) + x, int(Art.PLAYER_IDLE.position.y) + y).a > 0.15: if top < 0: top = y bottom = y break assert_gte(top, 0, "the idle frame should not be empty") var body_centre := (float(top) + float(bottom)) * 0.5 var cell_centre := Art.PLAYER_IDLE.size.y * 0.5 assert_almost_eq(Art.PLAYER_ANCHOR.y, cell_centre - body_centre, 1.0, "anchor should cancel the offset between the cell's centre and the " + "character's, or the hitbox sits away from the body")