Initial commit: Transcience MVP
Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated server with client-side prediction. Clients send input only; the server resolves every hit for both players and enemies (no PvP). - SimWorld: whole simulation as plain RefCounted objects (no nodes, no physics server), ~0.24ms/tick at peak load -- runs headless for free and drives 78 tests in under a second - BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn events rather than per-tick state - Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash enemies and bosses -- a new boss is data in src/content/content.gd, no simulation changes - The Warden of the Fold: stationary 4-phase boss built entirely on that format - Lobby hub with a portal into on-demand dungeon instances; one process hosts the hub plus every concurrent dungeon - Emergency escape: 3s server-owned channel, cancelled by damage - tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over ENet), bench.gd; git hooks wired to the same scripts - docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
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
|
||||
|
||||
|
||||
func _axis() -> Dictionary:
|
||||
match direction:
|
||||
1: return {"dir": Vector2.UP, "along": Vector2.RIGHT, "extent": SimConfig.ARENA_HALF.x, "edge": SimConfig.ARENA_HALF.y}
|
||||
2: return {"dir": Vector2.RIGHT, "along": Vector2.DOWN, "extent": SimConfig.ARENA_HALF.y, "edge": SimConfig.ARENA_HALF.x}
|
||||
3: return {"dir": Vector2.LEFT, "along": Vector2.DOWN, "extent": SimConfig.ARENA_HALF.y, "edge": SimConfig.ARENA_HALF.x}
|
||||
_: return {"dir": Vector2.DOWN, "along": Vector2.RIGHT, "extent": SimConfig.ARENA_HALF.x, "edge": SimConfig.ARENA_HALF.y}
|
||||
|
||||
|
||||
func fire(ctx: EmitContext) -> void:
|
||||
if count <= 0:
|
||||
return
|
||||
var ax := _axis()
|
||||
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 start := -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))
|
||||
Reference in New Issue
Block a user