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:
@@ -27,6 +27,9 @@ static var listen: bool = false
|
||||
## Server-side dev switch: new dungeons open straight onto the boss, skipping
|
||||
## the trash waves. For iterating on a pattern without clearing two waves first.
|
||||
static var boss_rush: bool = false
|
||||
## Dev switch: depth of newly opened dungeons, which drives map size. Depth
|
||||
## progression is a later-stage concern; this makes big maps testable now.
|
||||
static var dungeon_depth: int = 1
|
||||
static var parsed: bool = false
|
||||
|
||||
|
||||
@@ -51,6 +54,10 @@ static func parse(argv: PackedStringArray = PackedStringArray()) -> void:
|
||||
listen = true
|
||||
"--boss-rush":
|
||||
boss_rush = true
|
||||
"--depth":
|
||||
i += 1
|
||||
if i < argv.size():
|
||||
dungeon_depth = int(argv[i])
|
||||
"--port":
|
||||
i += 1
|
||||
if i < argv.size():
|
||||
|
||||
+22
-9
@@ -12,11 +12,15 @@ static func sanitize_move(raw: Vector2) -> Vector2:
|
||||
|
||||
|
||||
## One tick of player locomotion. Returns the new position.
|
||||
static func step_player(pos: Vector2, move: Vector2, speed: float, bounds: Vector2) -> Vector2:
|
||||
var next := pos + sanitize_move(move) * speed * SimConfig.TICK_DELTA
|
||||
next.x = clampf(next.x, -bounds.x, bounds.x)
|
||||
next.y = clampf(next.y, -bounds.y, bounds.y)
|
||||
return next
|
||||
##
|
||||
## Collision resolution lives in [MapGrid.slide_circle], and the client runs
|
||||
## this exact call during prediction and reconciliation -- so the map the client
|
||||
## holds has to match the server's, which is why maps are replicated in full
|
||||
## even where fog hides them.
|
||||
static func step_player(pos: Vector2, move: Vector2, speed: float, map: MapGrid,
|
||||
radius: float = SimConfig.PLAYER_RADIUS) -> Vector2:
|
||||
var delta := sanitize_move(move) * speed * SimConfig.TICK_DELTA
|
||||
return map.slide_circle(pos, delta, radius)
|
||||
|
||||
|
||||
## Circle-vs-circle overlap test used for every hit in the game.
|
||||
@@ -25,7 +29,16 @@ static func circles_overlap(a: Vector2, ar: float, b: Vector2, br: float) -> boo
|
||||
return a.distance_squared_to(b) <= r * r
|
||||
|
||||
|
||||
## True when a point has drifted outside the arena plus the cull margin.
|
||||
static func outside_arena(p: Vector2, margin: float = SimConfig.BULLET_CULL_MARGIN) -> bool:
|
||||
return absf(p.x) > SimConfig.ARENA_HALF.x + margin \
|
||||
or absf(p.y) > SimConfig.ARENA_HALF.y + margin
|
||||
## Outside the map's extent. Derivable on both sides from the map's dimensions
|
||||
## alone, so it needs no announcement even when tiles have not been streamed.
|
||||
static func outside_map(p: Vector2, map: MapGrid) -> bool:
|
||||
if map == null:
|
||||
return false
|
||||
return not map.world_rect().grow(SimConfig.BULLET_CULL_MARGIN).has_point(p)
|
||||
|
||||
|
||||
## Outside the map, or stopped by geometry.
|
||||
static func bullet_stopped(p: Vector2, map: MapGrid) -> bool:
|
||||
if map == null:
|
||||
return false
|
||||
return outside_map(p, map) or map.bullet_blocked(p)
|
||||
|
||||
+17
-3
@@ -13,8 +13,7 @@ const SNAPSHOT_INTERVAL := 3
|
||||
const INTERPOLATION_DELAY_TICKS := 6
|
||||
|
||||
# --- Arena ------------------------------------------------------------------
|
||||
const ARENA_HALF := Vector2(620.0, 340.0)
|
||||
## Bullets are culled once they leave the arena by this margin.
|
||||
## Bullets are culled once they leave the map by this margin.
|
||||
const BULLET_CULL_MARGIN := 64.0
|
||||
|
||||
# --- Player -----------------------------------------------------------------
|
||||
@@ -113,6 +112,21 @@ const DUNGEON_FORMING_TICKS := 300
|
||||
## player through the portal opens a fresh instance.
|
||||
const DUNGEON_CLEARED_EXIT_TICKS := 1800 # 30 seconds
|
||||
|
||||
# --- Fog of war -------------------------------------------------------------
|
||||
## How far a player can see. Comfortably inside MAP_STREAM_RADIUS so the client
|
||||
## always holds the terrain it is about to reveal.
|
||||
const FOG_VIEW_RADIUS := 460.0
|
||||
|
||||
# --- Map streaming ----------------------------------------------------------
|
||||
## How far around a player the server streams map tiles. Comfortably wider than
|
||||
## the viewport so movement prediction and bullet simulation always run against
|
||||
## known geometry, and comfortably narrower than a dungeon so a modified client
|
||||
## still cannot draw the whole thing.
|
||||
const MAP_STREAM_RADIUS := 900.0
|
||||
## Chunks sent to one peer per snapshot tick. Caps the burst when a player
|
||||
## arrives or sprints into virgin territory.
|
||||
const MAP_CHUNKS_PER_TICK := 6
|
||||
|
||||
# --- Portal -----------------------------------------------------------------
|
||||
const PORTAL_POS := Vector2(0.0, -220.0)
|
||||
## Position is per-world now (SimWorld.portal_pos), taken from the hub's map.
|
||||
const PORTAL_RADIUS := 60.0
|
||||
|
||||
Reference in New Issue
Block a user