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:
@@ -290,3 +290,39 @@ static func decode_roster(data: PackedByteArray) -> Array[Dictionary]:
|
||||
"alive": b.get_u8() == 1,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
# --- Map chunks -------------------------------------------------------------
|
||||
|
||||
static func encode_map_chunks(map: MapGrid, ids: Array) -> PackedByteArray:
|
||||
var b := StreamPeerBuffer.new()
|
||||
b.big_endian = false
|
||||
b.put_u8(mini(ids.size(), 255))
|
||||
for id in ids:
|
||||
b.put_u16(int(id))
|
||||
b.put_data(map.encode_chunk(int(id)))
|
||||
return b.data_array
|
||||
|
||||
|
||||
## Applies straight into [param map]. Chunk sizes come from the map's own
|
||||
## dimensions, which the client learned at enter_instance, so a truncated or
|
||||
## hostile packet cannot make it read past the end.
|
||||
static func decode_map_chunks_into(map: MapGrid, data: PackedByteArray) -> int:
|
||||
if data.size() < 1:
|
||||
return 0
|
||||
var b := StreamPeerBuffer.new()
|
||||
b.big_endian = false
|
||||
b.data_array = data
|
||||
var count := b.get_u8()
|
||||
var applied := 0
|
||||
for _i in count:
|
||||
if b.get_available_bytes() < 2:
|
||||
break
|
||||
var id := b.get_u16()
|
||||
var r := map.chunk_rect(id)
|
||||
var n := r.size.x * r.size.y
|
||||
if n <= 0 or b.get_available_bytes() < n:
|
||||
break
|
||||
map.apply_chunk(id, b.get_data(n)[1])
|
||||
applied += 1
|
||||
return applied
|
||||
|
||||
Reference in New Issue
Block a user