7af439341d
ci / verify (push) Successful in 46s
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.
53 lines
2.0 KiB
GDScript
53 lines
2.0 KiB
GDScript
class_name WallGapEmitter
|
|
extends BulletEmitter
|
|
## A curtain of bullets sweeping across the arena with a survivable gap. The gap
|
|
## moves by [member gap_step] every shot so players cannot camp one lane.
|
|
|
|
## 0 = travels down, 1 = up, 2 = right, 3 = left.
|
|
@export_enum("Down", "Up", "Right", "Left") var direction: int = 0
|
|
@export var count: int = 18
|
|
## Slots left empty. Width 2-3 is tight but fair at default bullet radius.
|
|
@export var gap_width: int = 3
|
|
@export var gap_index: int = 6
|
|
## How far the gap slides each shot. Coprime-ish values feel least predictable.
|
|
@export var gap_step: int = 5
|
|
## Randomise the gap instead of stepping it.
|
|
@export var randomize_gap: bool = false
|
|
|
|
|
|
## Direction of travel, the axis the curtain is strung along, and how far the
|
|
## curtain reaches -- all measured from the room the pattern is filling rather
|
|
## than from a fixed arena.
|
|
func _axis(bounds: Rect2) -> Dictionary:
|
|
var half := bounds.size * 0.5
|
|
match direction:
|
|
1: return {"dir": Vector2.UP, "along": Vector2.RIGHT, "extent": half.x, "edge": half.y}
|
|
2: return {"dir": Vector2.RIGHT, "along": Vector2.DOWN, "extent": half.y, "edge": half.x}
|
|
3: return {"dir": Vector2.LEFT, "along": Vector2.DOWN, "extent": half.y, "edge": half.x}
|
|
_: return {"dir": Vector2.DOWN, "along": Vector2.RIGHT, "extent": half.x, "edge": half.y}
|
|
|
|
|
|
func fire(ctx: EmitContext) -> void:
|
|
if count <= 0:
|
|
return
|
|
var ax := _axis(ctx.bounds)
|
|
var dir: Vector2 = ax["dir"]
|
|
var along: Vector2 = ax["along"]
|
|
var extent: float = ax["extent"]
|
|
var edge: float = ax["edge"]
|
|
|
|
var gap := gap_index
|
|
if randomize_gap and ctx.rng != null:
|
|
gap = ctx.rng.randi_range(0, maxi(count - gap_width, 0))
|
|
else:
|
|
gap = posmod(gap_index + gap_step * ctx.shot_index, maxi(count - gap_width + 1, 1))
|
|
|
|
var centre := ctx.bounds.get_center()
|
|
var start := centre - dir * (edge + 8.0)
|
|
var angle := dir.angle()
|
|
for i in count:
|
|
if i >= gap and i < gap + gap_width:
|
|
continue
|
|
var t := (float(i) / float(maxi(count - 1, 1))) * 2.0 - 1.0
|
|
emit_shot(ctx, angle, start + along * (t * extent))
|