Fix bullet atlas, animation speed, sprite flip, fire rate, silent shots
ci / verify (push) Successful in 46s
ci / verify (push) Successful in 46s
Bullet sprites were wrong because I read the pack's layout backwards. Each of
its 8 PNGs is one ANIMATION FRAME, and a column within a file is a COLOUR
variant -- so walking columns cycled the palette while the shape sat still, and
the rows I picked were in a region of larger multi-cell sprites, which is the
scattered debris that showed on screen. tools/build_local_assets.py now composes
a proper atlas: 8 frames across, one row per SimConfig.KIND_*.
Animations ran at whatever the machine's framerate was, because they counted
_process calls. At 240fps that is four times too fast on its own, before the
requested halving. Both actor and bullet animation are now driven by elapsed
seconds, so they look the same on any machine.
The knight shifted sideways instead of mirroring: a Rect2 with negative width
does not flip, the draw call normalises it, so the sprite kept its orientation
and jumped right by its own width. Mirrored through the canvas transform about
the sprite's centre instead.
Shots were silent when a bullet spawned inside an enemy. The sound rode on
BULLET_SPAWN, which is deliberately suppressed for a bullet resolved and removed
within the same tick -- so no event, no sound, and this got more likely the
closer you stood. Firing is now its own event (PLAYER_FIRED): the shot happened
whether or not a bullet survived to be replicated. Protocol 3 -> 4, because
inserting mid-enum shifts the wire value of every event after it.
Fire cooldown 7 -> 14 ticks (4.3 shots/sec).
Licence question 2, confirmed verbatim: the paid tier DOES grant commercial use
("You may use these assets in personal, commercial or non-commercial
projects"), but its next sentence still forbids redistribution "no matter how
much you modify it". Paying moves these from local-only-and-non-commercial to
local-only-and-commercial; it never makes them committable. Only replacing them
with permissively licensed art does that.
Question 3: CREDITS.md now credits every pack, including the two whose licences
do not require it and the unused ones in considering_dont_use_yet/.
158 tests. check.sh, test.sh and smoke.sh pass.
This commit is contained in:
+13
-8
@@ -52,19 +52,24 @@ func test_every_bullet_frame_is_inside_the_sheet() -> void:
|
||||
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 kind in SimConfig.KIND_HEAVY + 1:
|
||||
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))
|
||||
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])
|
||||
|
||||
|
||||
## 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)
|
||||
## 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
|
||||
|
||||
@@ -69,6 +69,33 @@ func test_input_flood_cannot_grow_the_queue_without_bound() -> void:
|
||||
"a flood of inputs must not become unbounded server memory")
|
||||
|
||||
|
||||
## The shot sound used to ride on BULLET_SPAWN, and a bullet born inside an
|
||||
## enemy is resolved and removed within the same tick -- so no spawn event was
|
||||
## ever emitted and the shot was silent. Firing is now its own event.
|
||||
func test_firing_is_reported_even_when_the_bullet_dies_instantly() -> void:
|
||||
var p: SimPlayer = world.players[PEER]
|
||||
p.pos = Vector2.ZERO
|
||||
p.aim = 0.0
|
||||
# The inert practice dummy, sitting exactly where the muzzle is, so the
|
||||
# bullet is consumed on the tick it is created. Deliberately not a turret:
|
||||
# a turret's own ring would fill the event list with its spawns and the
|
||||
# setup check below would pass for the wrong reason.
|
||||
world.spawn_enemy(Content.dummy(), Vector2(SimConfig.PLAYER_MUZZLE_OFFSET, 0.0))
|
||||
_drive(1, Vector2.ZERO, InputFrame.BTN_FIRE)
|
||||
assert_eq(_events_of(SimEvent.Type.PLAYER_FIRED).size(), 1,
|
||||
"the shot happened and has to be reported")
|
||||
assert_eq(_events_of(SimEvent.Type.BULLET_SPAWN).size(), 0,
|
||||
"setup check: the bullet really did die on the tick it spawned")
|
||||
|
||||
|
||||
func test_firing_is_reported_once_per_shot() -> void:
|
||||
_drive(60, Vector2.ZERO, InputFrame.BTN_FIRE)
|
||||
var fired := _events_of(SimEvent.Type.PLAYER_FIRED).size()
|
||||
var expected := 60 / SimConfig.PLAYER_FIRE_COOLDOWN
|
||||
assert_almost_eq(float(fired), float(expected), 1.0,
|
||||
"one report per shot the cooldown actually allowed")
|
||||
|
||||
|
||||
func test_fire_rate_is_enforced_by_the_server() -> void:
|
||||
# Hold fire every single tick; the server still applies its own cooldown.
|
||||
_drive(60, Vector2.ZERO, InputFrame.BTN_FIRE)
|
||||
|
||||
Reference in New Issue
Block a user