Implements tasks 2.1-2.7: NetworkedMatch spawns a deterministic slot
layout from the lobby roster, the server drives each connected peer's
ship via RLShipController fed by decoded client input and broadcasts
60Hz snapshots, and the client renders everything (including its own
ship) from a per-body NetInterpolator with no local prediction yet.
Dual-time remote entities split collider updates (present-time, for
correct contacts) from $Visual updates (interp-delayed, for smoothness).
Camera/HUD wiring and remote engine-flame VFX fell out of the existing
Ship API for free once snapshots were flowing.
Three real bugs found and fixed while getting a two-process test
green: an RPC method named _input collided with Node's built-in
_input virtual and broke the whole MatchSim autoload from loading;
networked_match.gd never called NetworkManager.poll(), so nothing
sent via RPC in this scene reached the wire despite Phase 1's manual
polling being wired up everywhere else; and a match_config
request/response fallback (added to close a startup race) could
double-deliver once polling was fixed, requiring an idempotency guard.
Verified with tests/networked_match_smoke: a real headless two-process
host+client run shows the client rendering 31m of server-authoritative
movement from a held forward-thrust input, with thrust_z=1.0 confirmed
on the interpolated snapshot mid-drive and camera/HUD both wired.
Full Phase 1 regression suite re-run clean alongside it.
Task 2.8 (net_sim.gd latency/jitter/loss decorator) is not yet done;
Phase 2's own gate needs it before it's fully met.
Lands tasks 1.0-1.8 of multiplayer-todo.md: the pure-function test runner,
net_codec (wire format quantizers/pack-unpack), NetworkManager (ENet
transport, manual polling, min-RTT clock sync), MatchNet (handshake,
protocol/tick-rate gating, roster with team+ready state), lobby.tscn (team
columns, switch team, ready toggle), server_boot.tscn (headless dedicated
server with structured logging and an overrun watchdog), and main_menu.gd's
Host/Join-by-IP UI (connecting overlay, cancel, bounded failure path).
Followed by an adversarial review (Opus subagent) that found and fixed two
real bugs - an unvalidated player_name broadcast that let one client's
oversized name head-of-line-block the reliable channel for everyone, and a
server-side roster leak across a host/re-host cycle - plus three gaps in
the test suite itself where a claim of "verified" wasn't actually backed
by what the test checked. All five two-process smoke tests plus the
pure-function suite are green with the strengthened assertions in place.
match.tscn had picked up team_size=3 from an earlier diagnostic dry run,
which would have made every normal Match spawn 3v3 instead of 1v1 -
reverted to the scene's intended default.
multiplayer-todo.md: task 0.15b's real blocker turned out to be measuring
on a Mac (Apple Silicon's tile-based GPU architecture gave a misleading,
undifferentiated cost profile). Re-ran the same 6-ship-match profiling
harness on reference hardware (RTX 3090) via a real GPU-bound X session -
results in §5.5.2 show the game comfortably clears 500+fps with every
effect on, and SDFGI/SSIL dominate the (now tiny) effects budget as
originally expected. This closes 0.28 (physics threading) as unnecessary
- there's no frame-time variance problem on reference hardware to fix -
and reframes 0.26 (bake GI) as a real but smaller win than assumed, worth
revisiting on lower-end hardware. Also corrected two stale/inaccurate
task rows (0.13, 0.17) found while reconciling the doc against what
actually landed.
Lands the non-networked Phase 0 tasks from multiplayer-todo.md (ship/camera/
arena refactors, sim constants, background FPS handling) plus a first pass
at exposing graphics/performance settings (presets, resolution scaling,
vsync, FPS cap, perf overlay) and a GPU profiling harness for the
real-hardware follow-up in task 0.15b.
Retraining from scratch removes the constraint that blocked these: the
existing checkpoints in Game/bots/promoted/ no longer need byte-identical
geometry.
- Fix the goal aperture constants (were 1.85/1.65, now match
objects/goal.tscn's actual sensor exactly at 1.75/1.5) so a ball crossing
the visible edge can't fail to score.
- Cut a real navigable hole in each end wall's collision
(_build_end_wall_colliders), replacing the previous solid box — the ball
now genuinely enters the net instead of triggering the sensor a hair
before hitting a solid wall. Correct for both FLOOR and ELEVATED goal
modes via _goal_surround_bounds. A separate, slightly wider
GOAL_VISUAL_APERTURE_* pair keeps the collision hole exact while still
giving goal.gd's bezel/rim frame clearance to be seen against the hull cut.
- Resize the play volume 1.5x (INNER_HALF_X/Z/HEIGHT 12/18/12 -> 18/27/18) to
comfortably fit a 5v5 roster: updates every hand-authored literal in
arena_boundary.tscn/arena_base.tscn/the elevated arena variants that
doesn't derive from those constants, adds 5 spawn markers per team, and
rescales ship_observations.gd's normalization scales and arena_02's
cosmetic decoration/nebula-dust shader uniforms to match.
- Bake the ~170 runtime-generated CollisionShape3D nodes into the scene via
a new bake_colliders()/tools/bake_arena_boundary.gd instead of rebuilding
them on every load — a real load-time cost repeated in every parallel
headless training env. Colliders must be direct children of the
StaticBody3D to register at all, so bake_colliders() parents them onto
self and tags them with a group for idempotent re-baking, rather than
grouping them under an intermediate container node. _ready() self-heals:
it skips regenerating only when an existing bake's goal_mode metadata
matches the current one, so the FLOOR-mode bake in the shared scene is
never silently reused by an ELEVATED arena variant.
ship.gd, HUDController.gd, and goal.gd each declared their own TEAM_COLORS,
arena_boundary.gd/arena_deck.gdshader had a third pair, and HUD.tscn baked
in a fourth (hardcoded "BLUE"/"ORANGE" labels) — nose, goal rim, end zone,
and scoreboard all rendered different blues. New scripts/team_colors.gd
(class_name TeamColors) is now the single source every one of those reads
from, and team identity moves to purple/green.
hud_gauge, hud_attitude_indicator, and hud_heading_tape now skip
queue_redraw() when the newly-lerped value hasn't moved past a small
epsilon, instead of redrawing every frame forever. Angle-wrapping
values (heading, attitude roll) use a new shared angle_delta_deg
helper on HudInstrument so the wrap boundary doesn't read as a false
jump.
HUD.tscn's Instruments node now sets process_mode = 1 (PAUSABLE),
overriding the inherited ALWAYS mode from the HUD root so instruments
stop processing during the post-match pause freeze, while sibling
ResultOverlay keeps running its win-screen tween.
Arenas inherit from a new arena_base.tscn instead of restating ~40 shared
lines each; only sky/ambient/glow/tint/decoration vary, exposed via new
Arena exports since nested Environment properties aren't overridable
through scene inheritance. Bot construction and score-keeping move onto
GameMode, shared by match and spectate modes while preserving their
differing GameSettings-override behavior and the HUD's score-row
duck-typing. HUD instruments share a HudInstrument base for the
smoothing-weight calc and angle-lerp helper. Also dedupes
MAIN_MENU_SCENE_PATH into ScenePaths and documents why DIFFICULTIES
tiers share one checkpoint.
SDFGI ran with default cascades in a volume that was almost entirely
transparent, so its low-resolution probes had little solid geometry to
capture and contributed blotching across large flat surfaces. With the
boundary now an opaque deck plus an unshaded additive field, it earns
nothing, and the existing ReflectionProbe and ambient carry the lighting.
Each arena also gains a shadowless fill light opposite its key light so the
deck is not lit from a single direction, and passes its own tint through to
the boundary's containment field so the three arenas read differently.
use_debanding was absent. Glow runs at hdr_scale 2.0 with adjustment_contrast
above 1.0, which amplifies 8-bit quantisation on exactly the kind of smooth
gradients the new shell is made of.
The remaining project.godot churn is Godot's own key reordering on save.
Each of the three arenas gains an ELEVATED sibling scene that inherits the
base arena and overrides the boundary's goal_mode, the two goal transforms
and the ship spawns, so the goal sits at mid-wall height instead of flush
with the deck. Registered in ArenaRegistry alongside the floor-level arenas,
plus a training_elevated scene for self-play on them.
TrainingMode reads the arena's goal_mode once in _start() and widens the
ball-placement height range to match the goal's real position; on FLOOR
arenas the bound is a no-op, so floor-level training is unchanged. It is read
in _start() rather than _ready() because TrainingMode has no _ready()
override and GameMode._ready() is what discovers the arena first.
Policies trained against floor-level goals are not expected to score on an
elevated one, so the two are kept as separate arenas rather than a variant of
the same entry.
Replace NebulaDust's flat unshaded glow material with a custom
ShaderMaterial: a fake per-pixel puff normal on the billboarded quad
feeds a light() override so motes catch the directional light and a
nebula-core color bias, alpha gets a depth-texture soft-particle fade
so motes no longer hard-clip through boundary/decoration geometry,
and a per-particle hash adds subtle sparkle.
Adds sdfgi_enabled and a ReflectionProbe to all three arenas' Environment
setup so surfaces get real bounce lighting/reflections instead of flat
ambient. Converts arena_boundary.tscn's shared glass field material from
unshaded to a shaded, rim-lit material so it reads as glass (dim face-on,
highlighted at grazing angles) rather than a flat tinted overlay.
Enable glow/bloom, color adjustments, and MSAA+FXAA across all three
arenas so emissive ship/station accents actually bleed light and edges
read cleanly. Expose the player-facing knobs (anti-aliasing mode, glow
intensity, brightness) through a new Settings screen off the main menu,
persisted via a VideoSettings autoload that scales each arena's own
tuned Environment values on load rather than overwriting them.
Introduce arena_02 (Nebula) and arena_03 (Asteroid Field) alongside
arena_01, listed in a new ArenaRegistry (scripts/arena_registry.gd) as
the single source of truth for available arenas. Free Play lets the
player pick an arena from the main menu; Match/Spectate each pick one
at random per session; Training keeps its own fixed arena_01.
Nebula gets an RL-style visual treatment: a near-invisible glass
boundary material shared by all arenas, a baked equirect nebula sky
texture, a drifting-dust particle system, and a Blender-modeled
station/debris/planet decoration set. GameMode gains a
_get_arena_scene_path() hook so modes can instantiate their arena in
code instead of hardcoding it in the scene.
Replace the raw checkpoint dropdown with curated Easy/Medium/Hard presets
that drive GameSettings' bot model/reaction_ticks/action_noise overrides.
Move raw-checkpoint testing and Spectate mode into a dev-only section
hidden via OS.is_debug_build() so they disappear from release exports.