The Cantor gets its own portal; bullets stop strobing
ci / verify (push) Successful in 48s

A dungeon now names the boss arena it ends in, and the arena decides the boss.
Three entrances in the hub: Warden's Descent, The Choir Vault, Proving Grounds.

That replaces the seed coin-flip from the last commit, which was the wrong
call. Which boss you are about to fight is the single thing a player decides
before walking into a dungeon, and rolling it for them makes that decision
unavailable. Only the Proving Grounds still leaves its arena open, because a
harness you re-enter every couple of minutes wants whichever fight comes up
first -- and it is the one place where "either will do" is true.

test_every_boss_has_a_dungeon_that_reaches_it is the check that matters here:
adding a boss and forgetting to give it a way in is now a failing test rather
than a boss nobody meets. The smoke test asserts all three portals open over a
real socket -- bots pick theirs by account id, so a run exercises each.

Bullets no longer animate. The sheet's eight frames are a COLOUR cycle rather
than a shape change, so running it had every bullet on screen strobing through
a palette in unison, which with a few hundred in the air is exactly as hard to
look at as it sounds. Each bullet holds one frame and turns slowly instead,
offset by its own id so a ring of twenty does not rotate as one rigid wheel.
The rate is slow enough that nothing completes a full turn inside its own
lifetime, so it reads as drift rather than spin, and a test pins that against
the longest-lived bullet in the game. The renderer slices four textures at
startup now instead of thirty-two.

check.sh clean, 414 tests, SMOKE PASS (22 assertions), all four diagnostics
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 10:36:06 +02:00
parent 42568a40ad
commit 4a98cf4b0e
15 changed files with 251 additions and 100 deletions
+29
View File
@@ -47,6 +47,9 @@ func test_every_animation_frame_is_inside_the_tileset() -> void:
"frame %d of strip %s runs off the atlas at %s" % [n, strip, f])
## Only BULLET_STILL_FRAME is ever drawn, but the whole sheet still has to be
## the shape the atlas builder produced -- a short one would mean the still
## frame of a later kind is cut from the wrong row.
func test_every_bullet_frame_is_inside_the_sheet() -> void:
var sheet := Art.bullets_texture()
if sheet == null:
@@ -59,6 +62,32 @@ func test_every_bullet_frame_is_inside_the_sheet() -> void:
"bullet kind %d frame %d is outside the sheet at %s" % [kind, n, r])
func test_the_still_frame_exists_for_every_kind() -> void:
var sheet := Art.bullets_texture()
if sheet == null:
pass_test("bullet sheet not present locally")
return
assert_lt(Art.BULLET_STILL_FRAME, Art.BULLET_FRAMES)
for kind in SimConfig.KIND_HEAVY + 1:
var r := Art.bullet_region(kind, Art.BULLET_STILL_FRAME)
assert_true(_fits(sheet, r), "kind %d has no still frame" % kind)
## Slow enough that nothing completes a turn inside its own lifetime. A bullet
## that visibly spins is the flicker this replaced, wearing a different hat.
func test_bullets_turn_slowly_enough_to_read_as_drift() -> void:
var longest := float(SimConfig.PLAYER_BULLET_LIFETIME)
for id in Content.ALL_ENEMIES:
for e in Content.enemy(id).emitters:
longest = maxf(longest, float(e.lifetime))
for id in Content.ALL_BOSSES:
for phase in Content.boss(id).phases:
for e in phase.emitters:
longest = maxf(longest, float(e.lifetime))
var turn := Art.BULLET_SPIN_RATE * longest * SimConfig.TICK_DELTA
assert_lt(turn, TAU, "the longest-lived bullet turns %.2f rad in its life" % turn)
## 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: