33845a8a42
ci / verify (push) Successful in 46s
Researched the licence at each of the four itch.io sources. Two of the four
forbid redistribution, and publishing a repository containing them is exactly
that, so they are out of the tree.
0x72 DungeonTileset II CC0 1.0 commercial ok, no credit required
Helton Yan Pixel Combat CC BY 4.0 commercial ok, CREDIT REQUIRED
Fire Pixel Bullet 16x16 custom non-commercial, NO REDISTRIBUTION
750+ Effect and FX Pixel custom non-commercial, NO REDISTRIBUTION
Both bdragon1727 packs state verbatim: "You cannot do: Resell / redistribute
this asset." The FX pack's paid tier is no better for this purpose -- "You can
NOT re-distribute the file, no matter how much you modify it" -- so paying does
not unlock it and neither does editing the art.
bullets.png and impact.png therefore move to assets/local/, gitignored.
Art.bullets_texture() / impact_texture() load them at runtime and return null
when absent; the bullet renderer falls back to the generated dot it used before,
tinted per kind so bullet types stay distinguishable. Nothing there may be
preload()ed -- preload resolves at compile time and would fail the build on
every machine without the files, which is every clone.
Verified both ways rather than assumed: with the files present the game uses
them and no fallback triggers; with them moved aside, 156/156 tests pass and the
game runs clean on the generated dots.
Added CREDITS.md, because Helton Yan's CC BY 4.0 makes attribution a licence
obligation rather than a courtesy. A file in the source tree does not discharge
it once the game ships -- an in-game credits screen is now a tracked todo, and
flagged as a licence requirement rather than polish.
NOT addressed here, and it needs a decision: these files remain in git history
at 4a41ccb. Removing them from the tip does not remove them from a clone. If
this repo is ever made public, history has to be rewritten (git filter-repo)
and force-pushed. Also, both bdragon packs are non-commercial in their free
tier, which conflicts with the stated Steam goal -- either contribute for the
commercial tier (still no redistribution) or replace with CC0 art, which would
solve both problems and let the assets back into the repo.
110 lines
4.1 KiB
GDScript
110 lines
4.1 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_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_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:
|
|
var sheet := Art.bullets_texture()
|
|
if sheet == null:
|
|
pass_test("bullet sheet not present locally; nothing to validate")
|
|
return
|
|
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(sheet, 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 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")
|