Replaces the fixed centred arena with per-world tile geometry, which is the foundation the remaining features sit on. - MapGrid: tile grid with three independent flags -- blocks movement, bullets, sight -- so a pit stops feet but not bullets or eyes, and a barricade stops feet and bullets but not eyes. Circle collision with per-axis sliding, and Bresenham line of sight shared by fog and aggro. - MapGen: rooms and corridors generated from (seed, depth), with hand-authored boss arenas from Rooms stamped in first so a corridor can never carve through a designed fight. Reachability from spawn to boss is asserted over 40 seeds -- "usually connected" is the failure mode that ruins one run in twenty. - Dungeons are populated at creation, per room, instead of gating on waves. You explore and choose your fights; the run ends when the boss dies, not when the map is swept. Boss rooms have no lock, so walking out is always available. - Aggro: enemies need range AND line of sight, so a dungeon stays quiet until engaged and cover actually protects. - Hard fog, scrolling camera, and terrain rendering. Maps are streamed per peer in chunks around that peer's player, and the seed is deliberately NOT sent -- a client holding it could regenerate the whole dungeon, which is a map hack for free. Stream radius (900u) is wider than view radius (460u) because the client predicts movement against walls and simulates bullets that die on them; the accepted cost is a cheater seeing a little further than the fog, never the floor plan. Partial map knowledge means wall deaths must be announced rather than derived. A test caught the subtle half of that: out-of-bounds tiles read as WALL by design, so checking geometry before bounds reported every bullet leaving the map as a wall kill. 128 tests (was 103); check.sh, test.sh and smoke.sh pass. 0.26 ms/tick with 4 players and a boss, ~65x headroom.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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,
|
||||
}
|
||||
|
||||
## Legend characters that record a position rather than only painting a tile.
|
||||
const MARKERS := ["B", "D", "P", "S", "T"]
|
||||
|
||||
|
||||
## 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......#",
|
||||
"#...................#",
|
||||
"#..===.......===....#",
|
||||
"#...................#",
|
||||
"#####################",
|
||||
])
|
||||
|
||||
|
||||
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, and reproduced identically on
|
||||
## the client from this same function -- see MapGen.build().
|
||||
## P portal to the dungeons S player spawn T practice target
|
||||
static func lobby() -> PackedStringArray:
|
||||
return PackedStringArray([
|
||||
"#########################################",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#...................P...................#",
|
||||
"#.......................................#",
|
||||
"#.....o...........................o.....#",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#.........T.............................#",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#.....o...........................o.....#",
|
||||
"#...................S...................#",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#.......................................#",
|
||||
"#########################################",
|
||||
])
|
||||
Reference in New Issue
Block a user