Files
transcience/tests/unit/test_bullet_pool.gd
T
Adyrem 651c4ad94a
ci / verify (push) Successful in 1m57s
Initial commit: Transcience MVP
Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated
server with client-side prediction. Clients send input only; the server
resolves every hit for both players and enemies (no PvP).

- SimWorld: whole simulation as plain RefCounted objects (no nodes, no
  physics server), ~0.24ms/tick at peak load -- runs headless for free and
  drives 78 tests in under a second
- BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn
  events rather than per-tick state
- Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash
  enemies and bosses -- a new boss is data in src/content/content.gd, no
  simulation changes
- The Warden of the Fold: stationary 4-phase boss built entirely on that
  format
- Lobby hub with a portal into on-demand dungeon instances; one process
  hosts the hub plus every concurrent dungeon
- Emergency escape: 3s server-owned channel, cancelled by damage
- tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over
  ENet), bench.gd; git hooks wired to the same scripts
- docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
2026-09-03 16:03:57 +02:00

95 lines
2.9 KiB
GDScript

extends GutTest
## The pool is the hottest data structure in the game and the one place a leak
## would end the run, so its slot bookkeeping is tested directly.
var pool: BulletPool
func before_each() -> void:
pool = BulletPool.new()
func _spawn(p := Vector2.ZERO, v := Vector2(100, 0), life := 60) -> int:
return pool.spawn(p, v, 5.0, life, 10, SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB)
func test_spawn_assigns_unique_ids() -> void:
var a := _spawn()
var b := _spawn()
assert_ne(pool.uid[a], pool.uid[b])
assert_eq(pool.live_count, 2)
func test_despawned_slots_are_reused() -> void:
var a := _spawn()
pool.despawn(a)
assert_eq(pool.live_count, 0)
var b := _spawn()
assert_eq(b, a, "the free list should hand back the slot just released")
assert_eq(pool.high_water, 1, "reuse must not grow the scanned range")
func test_double_despawn_does_not_corrupt_the_count() -> void:
var a := _spawn()
pool.despawn(a)
pool.despawn(a)
assert_eq(pool.live_count, 0)
func test_step_integrates_velocity() -> void:
var a := _spawn(Vector2.ZERO, Vector2(600, 0))
pool.step()
assert_almost_eq(pool.pos[a].x, 600.0 * SimConfig.TICK_DELTA, 0.001)
func test_turn_curves_the_bullet() -> void:
var a := pool.spawn(Vector2.ZERO, Vector2(100, 0), 5.0, 60, 10,
SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB, 0.0, deg_to_rad(90.0))
pool.step()
assert_almost_eq(pool.vel[a].angle(), deg_to_rad(90.0), 0.001)
func test_bullets_expire_at_end_of_life() -> void:
var a := _spawn(Vector2.ZERO, Vector2.ZERO, 3)
for _i in 3:
pool.step()
assert_eq(pool.alive[a], 0)
assert_eq(pool.live_count, 0)
func test_bullets_leaving_the_arena_are_culled() -> void:
var start := Vector2(SimConfig.ARENA_HALF.x, 0.0)
var a := _spawn(start, Vector2(100000, 0), 600)
pool.step()
assert_eq(pool.alive[a], 0, "a bullet past the cull margin must not linger")
func test_pool_saturation_returns_minus_one_rather_than_growing() -> void:
for _i in SimConfig.MAX_BULLETS:
_spawn(Vector2.ZERO, Vector2.ZERO, 10000)
assert_eq(pool.live_count, SimConfig.MAX_BULLETS)
assert_eq(_spawn(), -1)
func test_advance_slot_matches_repeated_steps() -> void:
var a := pool.spawn(Vector2.ZERO, Vector2(150, 40), 5.0, 300, 10,
SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB, 30.0, 0.02)
var other := BulletPool.new()
var b := other.spawn(Vector2.ZERO, Vector2(150, 40), 5.0, 300, 10,
SimConfig.TEAM_ENEMY, SimConfig.KIND_ORB, 30.0, 0.02)
pool.advance_slot(a, 12)
for _i in 12:
other.step()
# Client catch-up must land on exactly the server's integration, or bullets
# would drift a little further out of place with every packet.
assert_almost_eq(pool.pos[a].x, other.pos[b].x, 0.0001)
assert_almost_eq(pool.pos[a].y, other.pos[b].y, 0.0001)
func test_find_by_uid_only_matches_live_bullets() -> void:
var a := _spawn()
var id: int = pool.uid[a]
assert_eq(pool.find_by_uid(id), a)
pool.despawn(a)
assert_eq(pool.find_by_uid(id), -1)