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:
+26
-2
@@ -20,6 +20,10 @@ var team := PackedByteArray()
|
||||
var kind := PackedByteArray()
|
||||
var alive := PackedByteArray()
|
||||
|
||||
## The geometry bullets die against. Set by the owning SimWorld; identical on
|
||||
## server and client, which is what keeps the replica in step.
|
||||
var map: MapGrid = null
|
||||
|
||||
var live_count := 0
|
||||
## Exclusive upper bound over slots that have ever been used, so the hot loops
|
||||
## do not walk the whole 4096-slot table while the arena is nearly empty.
|
||||
@@ -33,6 +37,13 @@ var _next_uid := 1
|
||||
## arrive from the wire already.
|
||||
var spawn_log := PackedInt32Array()
|
||||
|
||||
## uids of bullets killed by hitting solid geometry this tick. Lifetime and
|
||||
## out-of-bounds deaths are NOT recorded: both sides can derive those from the
|
||||
## spawn event alone. A wall death cannot be derived by a client that has not
|
||||
## been streamed that wall yet, so the server announces those explicitly --
|
||||
## otherwise partial map knowledge shows bullets sailing through walls.
|
||||
var wall_kill_log := PackedInt32Array()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var n := SimConfig.MAX_BULLETS
|
||||
@@ -55,6 +66,7 @@ func clear() -> void:
|
||||
uid[i] = 0
|
||||
_free.clear()
|
||||
spawn_log.clear()
|
||||
wall_kill_log.clear()
|
||||
live_count = 0
|
||||
high_water = 0
|
||||
|
||||
@@ -97,6 +109,7 @@ func spawn(p: Vector2, v: Vector2, r: float, lifetime: int, dmg: int,
|
||||
|
||||
func clear_spawn_log() -> void:
|
||||
spawn_log.clear()
|
||||
wall_kill_log.clear()
|
||||
|
||||
|
||||
func despawn(slot: int) -> void:
|
||||
@@ -134,7 +147,17 @@ func step() -> void:
|
||||
var p: Vector2 = pos[i] + v * dt
|
||||
pos[i] = p
|
||||
life[i] -= 1
|
||||
if life[i] <= 0 or Movement.outside_arena(p):
|
||||
if life[i] <= 0:
|
||||
despawn(i)
|
||||
continue
|
||||
# Bounds first: out-of-bounds tiles read as WALL by design (that is what
|
||||
# seals the world), so testing geometry first would report every bullet
|
||||
# that simply left the map as a wall kill and announce it needlessly.
|
||||
if Movement.outside_map(p, map):
|
||||
despawn(i)
|
||||
continue
|
||||
if map != null and map.bullet_blocked(p):
|
||||
wall_kill_log.append(uid[i])
|
||||
despawn(i)
|
||||
|
||||
|
||||
@@ -156,6 +179,7 @@ func advance_slot(slot: int, ticks: int) -> void:
|
||||
vel[slot] = v
|
||||
pos[slot] = pos[slot] + v * dt
|
||||
life[slot] -= 1
|
||||
if life[slot] <= 0 or Movement.outside_arena(pos[slot]):
|
||||
if life[slot] <= 0 or Movement.bullet_stopped(pos[slot], map):
|
||||
despawn(slot)
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user