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>
143 lines
4.8 KiB
GDScript
143 lines
4.8 KiB
GDScript
class_name Rooms
|
|
extends RefCounted
|
|
## Hand-authored room stamps, as text.
|
|
##
|
|
## Boss arenas are authored rather than generated: a boss fight is a designed
|
|
## space -- sightlines, cover, room to dodge a wall pattern -- and a generator
|
|
## that produces "a room" produces a bad one about as often as a good one. The
|
|
## corridors and filler rooms around them are generated; the fights are not.
|
|
##
|
|
## Legend:
|
|
## # wall o pillar ~ pit = barricade
|
|
## . floor B boss spawn D doorway (floor, corridors connect here)
|
|
|
|
const LEGEND := {
|
|
"#": MapGrid.Kind.WALL,
|
|
"o": MapGrid.Kind.PILLAR,
|
|
"~": MapGrid.Kind.PIT,
|
|
"=": MapGrid.Kind.BARRICADE,
|
|
".": MapGrid.Kind.FLOOR,
|
|
"B": MapGrid.Kind.FLOOR,
|
|
"D": MapGrid.Kind.FLOOR,
|
|
"P": MapGrid.Kind.FLOOR,
|
|
"S": MapGrid.Kind.FLOOR,
|
|
"T": MapGrid.Kind.FLOOR,
|
|
"U": MapGrid.Kind.FLOOR,
|
|
}
|
|
|
|
## Legend characters that record a position rather than only painting a tile.
|
|
const MARKERS := ["B", "D", "P", "S", "T", "U"]
|
|
|
|
|
|
## Wide, with pillars to break the Warden's rings and pits that shape where you
|
|
## can retreat to. The door is deliberately on one wall only: the boss cannot
|
|
## follow you out, so the exit is a real decision rather than an accident.
|
|
static func warden_hall() -> PackedStringArray:
|
|
return PackedStringArray([
|
|
"#########################",
|
|
"#.......................#",
|
|
"#..~~~...........~~~....#",
|
|
"#..~~~....o.o....~~~....#",
|
|
"#.........................",
|
|
"#....o.................o#",
|
|
"#.............B.........#",
|
|
"#....o.................o#",
|
|
"D.........................",
|
|
"#..~~~....o.o....~~~....#",
|
|
"#..~~~..........~~~.....#",
|
|
"#.......................#",
|
|
"#########################",
|
|
])
|
|
|
|
|
|
## Tighter and more claustrophobic, with barricades you can see over but not
|
|
## shoot through -- a room that rewards knowing where the boss is without
|
|
## giving you a firing lane to it.
|
|
static func choir_vault() -> PackedStringArray:
|
|
return PackedStringArray([
|
|
"#####################",
|
|
"#...................#",
|
|
"#..===.......===....#",
|
|
"#...................#",
|
|
"#....o.......o......#",
|
|
"D.........B.........#",
|
|
"#....o.......o......#",
|
|
"#...................#",
|
|
"#..===.......===....#",
|
|
"#...................#",
|
|
"#####################",
|
|
])
|
|
|
|
|
|
## Which boss each arena belongs to. Kept beside the stamps because the pairing
|
|
## is a content decision, not a generation one: the Cantor's patterns assume
|
|
## the vault's barricades the way the Warden's assume the hall's pits.
|
|
static func boss_for_arena(arena: StringName) -> StringName:
|
|
match arena:
|
|
&"warden_hall": return Content.BOSS_WARDEN
|
|
&"choir_vault": return Content.BOSS_CANTOR
|
|
return Content.BOSS_WARDEN
|
|
|
|
|
|
static func size_of(stamp: PackedStringArray) -> Vector2i:
|
|
if stamp.is_empty():
|
|
return Vector2i.ZERO
|
|
var w := 0
|
|
for row in stamp:
|
|
w = maxi(w, row.length())
|
|
return Vector2i(w, stamp.size())
|
|
|
|
|
|
## Paint a stamp into [param grid] with its top-left at [param origin].
|
|
## Returns the marker positions found, keyed by their legend character.
|
|
static func stamp(grid: MapGrid, s: PackedStringArray, origin: Vector2i) -> Dictionary:
|
|
var markers := {}
|
|
for m in MARKERS:
|
|
markers[m] = []
|
|
for y in s.size():
|
|
var row := s[y]
|
|
for x in row.length():
|
|
var ch := row[x]
|
|
if not LEGEND.has(ch):
|
|
continue
|
|
var tx := origin.x + x
|
|
var ty := origin.y + y
|
|
grid.set_tile(tx, ty, LEGEND[ch])
|
|
if markers.has(ch):
|
|
markers[ch].append(Vector2i(tx, ty))
|
|
return markers
|
|
|
|
## The hub. Hand-authored like the boss arenas.
|
|
##
|
|
## P a dungeon portal S player spawn T practice target
|
|
## U the upgrade NPC
|
|
##
|
|
## Each `P`, in reading order, opens the matching entry in Dungeons.ORDER: the
|
|
## Warden's Descent, the Choir Vault, and the Proving Grounds. Adding a dungeon
|
|
## means adding an entry there and a marker here, and they have to stay in the
|
|
## same order.
|
|
static func lobby() -> PackedStringArray:
|
|
return PackedStringArray([
|
|
"#########################################",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#.........P.........P.........P.........#",
|
|
"#.......................................#",
|
|
"#.....o...........................o.....#",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#.........T...................U.........#",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#.....o...........................o.....#",
|
|
"#...................S...................#",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#.......................................#",
|
|
"#########################################",
|
|
])
|