ci / verify (push) Successful in 54s
Bullets were drawn everywhere on screen, including out in the black past the edge of sight: shots that could not reach you, streaking across the dark. They now fade out beyond sight, under one condition that bounds the whole change: no enemy bullet may hit you without having been on screen, at full strength, for at least WARNING_TIME (0.75s) first. So each enemy bullet is drawn out to the edge of sight or to however far it could close in that window with you running straight at it -- its own speed, plus acceleration over the window, plus yours -- whichever is further, and fades over 80 units beyond. With today's content the edge of sight always wins: the fastest enemy bullet is 300 u/s, so head-on it is on screen 0.83s before it lands. The speed term is what keeps that true when something faster is added. Player shots fade at the edge of sight however fast they are, since they cannot hurt you; a Sniper's 1500 u/s shot was the commonest thing crossing the dark. Shadow does not hide a bullet. One behind a wall within sight range is still drawn over the darkness, because you can step round the corner into it, and hiding it would be exactly the invisible hit the condition rules out. The guarantee is tested the hard way: every emitter in the content, and bullets far faster than any of them, simulated head-on against a player running at them, measuring how long each was fully drawn before it landed. The rule's weakenings -- no speed term, forgetting the player's own approach, ignoring acceleration -- each fail it. The connect screen's backdrop passes no eye and draws everything, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.5 KiB
GDScript
113 lines
4.5 KiB
GDScript
extends GutTest
|
|
## Which bullets are drawn, and the promise that bounds it: no enemy bullet can
|
|
## hit you without having been on screen, at full strength, for at least
|
|
## BulletRenderer.WARNING_TIME first. Fading distant bullets is only worth doing
|
|
## if that holds for every bullet the game can fire.
|
|
|
|
const RENDERER := preload("res://src/view/bullet_renderer.gd")
|
|
const SIGHT := SimConfig.FOG_VIEW_RADIUS
|
|
|
|
|
|
## The worst case for warning: the bullet coming straight at you and you running
|
|
## straight at it. Stepped the way BulletPool integrates, from far enough out
|
|
## that it starts undrawn. Returns seconds it was fully drawn before it hit.
|
|
func _warning(speed: float, accel: float) -> float:
|
|
var dt := SimConfig.TICK_DELTA
|
|
var gap := 6000.0
|
|
var v := speed
|
|
var t := 0
|
|
var first_full := -1
|
|
var reach := SimConfig.PLAYER_RADIUS + 7.0
|
|
while gap > reach and t < 60 * 120:
|
|
if first_full < 0 and RENDERER.visibility(gap, v, accel,
|
|
SimConfig.TEAM_ENEMY, SIGHT) >= 1.0:
|
|
first_full = t
|
|
v = maxf(v + accel * dt, 0.0)
|
|
gap -= (v + SimConfig.PLAYER_SPEED) * dt
|
|
t += 1
|
|
assert_gte(first_full, 0, "the bullet was drawn at all before it arrived")
|
|
return float(t - first_full) * dt
|
|
|
|
|
|
func test_every_enemy_bullet_in_the_game_is_seen_in_time() -> void:
|
|
var emitters: Array[BulletEmitter] = []
|
|
for id in Content.ALL_ENEMIES:
|
|
emitters.append_array(Content.enemy(id).emitters)
|
|
for id in Content.ALL_BOSSES:
|
|
for phase in Content.boss(id).phases:
|
|
emitters.append_array(phase.emitters)
|
|
assert_gt(emitters.size(), 0)
|
|
for e in emitters:
|
|
var warned := _warning(e.speed, e.accel)
|
|
assert_gte(warned, RENDERER.WARNING_TIME - SimConfig.TICK_DELTA,
|
|
"a %.0f u/s bullet was on screen %.2fs before it could land" % [e.speed, warned])
|
|
|
|
|
|
func test_a_bullet_far_faster_than_anything_today_is_still_seen_in_time() -> void:
|
|
# The sight radius covers today's content on its own. This is the case the
|
|
# speed term exists for: content added later, faster than the radius allows.
|
|
for case in [[1200.0, 0.0], [600.0, 900.0], [2000.0, 0.0]]:
|
|
var warned := _warning(case[0], case[1])
|
|
assert_gte(warned, RENDERER.WARNING_TIME - SimConfig.TICK_DELTA,
|
|
"%.0f u/s, accelerating %.0f: on screen %.2fs" % [case[0], case[1], warned])
|
|
|
|
|
|
func test_inside_sight_everything_is_drawn_fully() -> void:
|
|
for team in [SimConfig.TEAM_ENEMY, SimConfig.TEAM_PLAYER]:
|
|
assert_eq(RENDERER.visibility(SIGHT - 1.0, 130.0, 0.0, team, SIGHT), 1.0)
|
|
|
|
|
|
func test_a_slow_shot_far_beyond_sight_is_not_drawn() -> void:
|
|
assert_eq(RENDERER.visibility(SIGHT + 400.0, 130.0, 0.0, SimConfig.TEAM_ENEMY, SIGHT),
|
|
0.0, "out in the black and nowhere near you")
|
|
|
|
|
|
func test_the_edge_fades_rather_than_cuts() -> void:
|
|
var mid := RENDERER.visibility(SIGHT + RENDERER.FADE_OUT * 0.5, 130.0, 0.0,
|
|
SimConfig.TEAM_ENEMY, SIGHT)
|
|
assert_between(mid, 0.3, 0.7)
|
|
|
|
|
|
func test_player_shots_fade_at_the_edge_of_sight_however_fast() -> void:
|
|
# They cannot hurt you, so there is nothing to warn about -- and a Sniper's
|
|
# shot streaking across the dark was the commonest case of all.
|
|
assert_eq(RENDERER.visibility(SIGHT + 200.0, SimConfig.MAX_BULLET_SPEED, 0.0,
|
|
SimConfig.TEAM_PLAYER, SIGHT), 0.0)
|
|
assert_gt(RENDERER.visibility(SIGHT + 200.0, SimConfig.MAX_BULLET_SPEED, 0.0,
|
|
SimConfig.TEAM_ENEMY, SIGHT), 0.0,
|
|
"an enemy bullet that fast is drawn further out, because it could reach you")
|
|
|
|
|
|
# --- The renderer actually applies it ---------------------------------------
|
|
|
|
func _drawn(r: Node2D) -> int:
|
|
var n := 0
|
|
for layer in r._layers:
|
|
n += (layer as MultiMeshInstance2D).multimesh.visible_instance_count
|
|
return n
|
|
|
|
|
|
func test_the_renderer_leaves_out_what_is_too_far_to_matter() -> void:
|
|
var r: Node2D = RENDERER.new()
|
|
add_child_autofree(r)
|
|
var pool := BulletPool.new()
|
|
var eye := Vector2(100.0, 100.0)
|
|
pool.spawn(eye + Vector2(200.0, 0.0), Vector2(-130.0, 0.0), 7.0, 300, 10,
|
|
SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB)
|
|
pool.spawn(eye + Vector2(SIGHT + 500.0, 0.0), Vector2(-130.0, 0.0), 7.0, 300, 10,
|
|
SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB)
|
|
r.render_pool(pool, eye, SIGHT)
|
|
assert_eq(_drawn(r), 1, "the near bullet only")
|
|
|
|
|
|
func test_with_no_eye_everything_is_drawn() -> void:
|
|
# The connect screen's backdrop has no player and wants the whole pattern.
|
|
var r: Node2D = RENDERER.new()
|
|
add_child_autofree(r)
|
|
var pool := BulletPool.new()
|
|
pool.spawn(Vector2.ZERO, Vector2.RIGHT, 7.0, 300, 10, SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB)
|
|
pool.spawn(Vector2(5000.0, 0.0), Vector2.RIGHT, 7.0, 300, 10,
|
|
SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB)
|
|
r.render_pool(pool)
|
|
assert_eq(_drawn(r), 2)
|