Commit Graph

167 Commits

Author SHA1 Message Date
Josh Creek 401d882ff0 fix(assets): remove unreferenced duplicate planet_surface texture
assets/textures/planet_surface.png was byte-identical to and unused
in favor of assets/models/nebula_planet_planet_surface.png, the
sidecar Godot's glTF importer actually extracted from nebula_planet.glb
and uses at runtime. Deleted the orphan (+ its .import) and stopped
gen_planet_surface.py from writing it.

Also measured nebula_dust.gdshader's per-fragment depth-texture sample
cost (~0.07ms/frame at 500 particles, within noise) -- negligible, so
no budgeting concern for further particle work.
2026-08-05 09:51:33 +01:00
Josh Creek 9bdeb73fa7 docs: mark trained-bot compatibility items resolved in TODO.md
Records the drag/aperture/collider-bake/beyond-1v1 items as done, with a
short note on what actually shipped (scoped up to 5v5 mid-implementation)
and the explicit non-goals (no 2v2+ curriculum, no team-size UI) so it's
clear what's still open.
2026-08-05 09:18:08 +01:00
Josh Creek 3049c42867 feat(training): support N-vs-M matches with persistent per-ship spawn IDs
Extends ShipObservations beyond the old self+1-opponent layout to padded
teammate/opponent arrays (MAX_TEAMMATES=4, MAX_OPPONENTS=5, SIZE=83),
zero-filling slots past the real roster size the same way the old single-
opponent slot was zero-filled when absent.

Slot stability across ticks requires a persistent identity: Ship gains
spawn_index (set once by GameMode.spawn_ship, never reassigned — there's no
despawn path anywhere in this codebase, so a roster is fixed for the whole
episode/match). ai_ship_controller.gd's opponent discovery is rewritten from
"first non-self ship" to classify every other ship by team and sort by
spawn_index; training_mode.gd/ship_ai_controller.gd carry the equivalent
sorted lists through the training path so both agree on slot assignment for
the same roster.

training_mode.gd and match_mode.gd both gain a team_size export (default 1,
so every existing curriculum script and match keeps today's 1v1 behaviour
unchanged). This is plumbing only: no 2v2+ curriculum or reward design, and
no match-mode UI to pick team size, has been done yet. The two checkpoints
in Game/bots/promoted/ are fitted to the old 35-float layout and are not
migrated — expected to go stale until the next training run.
2026-08-05 09:17:56 +01:00
Josh Creek 18fdb0f232 feat(arena): resize for 5v5, cut a real goal-wall hole, and bake collision geometry
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.
2026-08-05 09:17:13 +01:00
Josh Creek a02e0770af fix(training): avoid ship-ship overlap when placing a multi-ship roster
_place_ships_random/_place_air_drill sampled each ship's randomized episode-
start position independently, so a team_size > 1 roster could spawn
interpenetrating (ships are ~1x1x4). Both now resample (up to 20 attempts,
matching the existing corner/fillet rejection-sampling pattern) against
every ship already placed that reset, rejecting anything within
MIN_SHIP_SEPARATION (4.5m, matching the arena spawn-marker spacing) of one.
2026-08-05 09:16:35 +01:00
Josh Creek 661c588fef fix(game-mode): recover ships/ball that escape through an open goal in every mode
The goal mouths are now a real navigable hole in the end walls, sized to the
ball rather than the ship — a ship's 1x1 cross-section fits through it, and
there's nothing behind the net to stop it. The escape failsafe previously
only existed in TrainingMode (where a physics regression just wastes
training time); now that any ship can genuinely fly out through an open
goal, every mode needs it or a stray ship/ball falls into the void with no
way back short of quitting. Moved up to GameMode as the shared default
_physics_process, removing TrainingMode's now-duplicate copy.
2026-08-05 09:16:00 +01:00
Josh Creek 0f7603d3cb fix(ship): delta-scale drag so it stays correct at any physics tick rate
drag_coefficient/angular_drag/the idle angular-drag multiplier were applied
once per physics tick with no delta scaling, correct only because
project.godot never pins physics/common/physics_ticks_per_second and
Godot's default happens to be 60. _tick_scaled(k, state.step) makes the
decay rate invariant to tick rate instead. Also promotes the previously
hardcoded 0.9 idle angular-drag literal to an export, matching its sibling.
2026-08-05 09:13:52 +01:00
Josh Creek cab2fa6391 fix(training): always emit flight-telemetry info keys, not just when nonzero
VecMonitor's info_keywords does a bare info[key] lookup on a completed
episode's terminal info dict and raises KeyError -- crashing the whole
training run -- if a key is ever absent. get_info() was only including
airborne_fraction/mean_altitude/vertical_thrust_mean when _telemetry_ticks
was nonzero and air_touch_fraction when _touches was nonzero; an episode
with zero ball touches (common, especially early in training) crashed on
the very first rollout in a smoke-test run. All four now always default to
0.0 rather than being conditionally present.

Found via TRAINING.md's Generation 4 validation ladder (rung 2, a 60k-step
smoke run) -- confirmed fixed by rerunning the same smoke run clean, then
export/evaluate parity (rungs 2-3) against Game/bots/promoted/easy.json.
2026-08-04 23:35:17 +01:00
Josh Creek 0e046aa9f1 chore(training): scrap generation 1-3 training data for generation 4
All checkpoints/logs/exported policies here are a continuous-Gaussian,
31-input action/observation shape that generation 4's MultiDiscrete
redesign is structurally incompatible with -- nothing to resume from (see
the prior commit and TRAINING.md's "Generation 4" section). Game/bots/promoted/
(easy.json, and the new reference-grounded.json copied from
curric-s5-aggression before this) is untouched -- both remain valid,
playable evaluation opponents forever via PolicyNetwork's format-versioned
JSON despite their own checkpoints/generation being gone.

- training/checkpoints/*, training/logs/* removed (~3.5GB of working tree,
  all generation 1-3 experiment runs).
- Game/bots/*.json flat dump removed (superseded exports; main_menu.gd's
  Spectate dropdown will just be empty until the first generation-4 export).
- curriculum_state.json -> curriculum_state_gen3.json, archived alongside
  the existing _gen1/_gen2 logs (all three are referenced as postmortem
  evidence in TRAINING.md/curriculum.py). A fresh curriculum_state.json
  will be created on the next curriculum.py run (load_state() already
  handles a missing file).

training/eval_history.json is deliberately NOT reset -- it's the one
continuous cross-generation progress record.

NOT YET PUSHED: this needs the remote Linux training box quiesced first
(kill any active tmux session, confirm it's synced to origin) so its own
run_training.sh doesn't race a still-running job's final commit against
this deletion.
2026-08-04 23:28:57 +01:00
Josh Creek 1811e9333e feat(training): curriculum generation 4 — MultiDiscrete action space redesign
Three curriculum generations (2026-07-21 through 2026-08-04) all tried
gating *when* the policy could use vertical thrust/pitch-roll on top of a
continuous Gaussian action space, and all three failed the same way: PPO's
action-distribution std collapsed within ~10% of steps and never recovered,
landing at a 15-32% win rate vs the grounded reference regardless of
mechanism (hard mask, then a gradual ramp). Generation 3's final attempt
just landed at 24% — the worst of the three.

Root cause, verified against this project's own physics: hovering this ship
requires *holding* thrust.y ~= 0.408 continuously (mass 5.0, vertical_thrust
120, gravity 9.8). A collapsed near-zero-mean Gaussian can brush that value
but never sustain it long enough to earn the reward gradient that would
move the mean — no amount of gating *when* the axis acts fixes a problem in
*how* the policy represents a decision on it. This also independently found
and fixes a real bug: godot_rl never marks an episode timeout as a
truncation, so PPO was bootstrapping V(s)=0 on every 30s draw in every
generation to date.

- Game/scripts/ship_action_codec.gd (new): single source of truth for a
  per-axis MultiDiscrete action space (7 heads, nvec [5,5,5,5,5,5,2]) shared
  by training and in-game inference, replacing the continuous Gaussian.
  thrust_y's bins are deliberately asymmetric so a random policy drifts
  through the volume instead of floor-pinning. Legacy continuous decode
  (ai_ship_controller.gd's old logic) preserved verbatim so every
  pre-generation-4 export (e.g. Game/bots/promoted/easy.json) keeps working
  unchanged via an optional "action_space" JSON field.
- ship_observations.gd: append own contact state (SIZE 31 -> 35, append-only)
  so the value function can see what wall_contact_penalty fires on.
- ship_ai_controller.gd: action space/decode via the codec; drop the
  vertical_ramp/pitch_roll_ramp mechanism entirely; tilt_penalty default
  lowered 4x (aerial approaches require pitching); flight telemetry
  (airborne_fraction, mean_altitude, air_touch_fraction, vertical_thrust_mean)
  and truncation-snapshot fields on get_info().
- training_mode.gd: new air_drill_chance state-setter branch (ball spawned
  high, ships low, kept clear of walls) so aerial practice is forced by the
  environment instead of relying on reward-driven exploration alone; snapshot
  terminal observations before a timeout reset for the truncation fix.
- cosmic_env.py: remap ShipAIController's truncated/terminal_obs info into
  SB3's TimeLimit.truncated/terminal_observation keys.
- train.py: --reset-logits (+ --reset-logits-heads) replaces the
  now-meaningless --reset-std; new EntropyFloorCallback (a persistent
  per-rollout ent_coef controller replacing the one-shot std-reset shock)
  and per-head entropy logging; FlightTelemetryCallback; --air-drill-chance/
  --tilt-penalty flags; optional AbortIfCallback kill-criterion.
- export_policy.py: writes the action_space block for MultiDiscrete models;
  index-level parity check (argmax per head) instead of comparing floats.
- curriculum.py: full rewrite — 3 stages (bootstrap/selfplay/gauntlet), no
  grounded stage, full action space live from step 1; deletes generation
  1-3's checkpoint-lineage machinery (nothing to resume from); final report
  evaluates against both promoted/easy.json and the new
  promoted/reference-grounded.json (a copy of curric-s5-aggression, the
  strongest grounded-era artifact, kept as a fixed yardstick).
- run_training.sh/.gitignore: commit only final.zip, not the ~2400
  intermediate checkpoint files a single stage was writing (~500MB ->
  ~0.2MB per run); requirements.txt pinned (behaviour here now depends on
  specific library internals, not just public APIs).
- test_action_space.py (new): offline rung-0 check catching a head-order
  mismatch before it silently corrupts 24h of training.

Validated: GDScript compiles clean (Godot --headless --import + script
validation), free_play.tscn and training.tscn both boot headless without
errors, offline action-space assertions pass. Not yet run: the actual
smoke-training/A-B validation ladder steps in TRAINING.md's "Generation 4"
section, before committing to the full ~32h curriculum.

See TRAINING.md's "Generation 4" section for the full design writeup.
2026-08-04 23:27:57 +01:00
Josh Creek 8551d9e835 perf(ship): reuse member ShipAction instead of allocating per tick
ship.gd's controllerless path and player_ship_controller.gd each
allocated a fresh ShipAction every physics tick; ai_ship_controller.gd
and rl_ship_controller.gd already avoid this via a persistent member.
Convert both to reuse a member instance, matching the existing
full-field-overwrite convention (rather than +=/-= off a fresh zero).

Also drop the completed items from TODO.md.
2026-08-04 22:23:02 +01:00
CosmicClash Training Bot 6198dc67fc chore(training): Add 20260803-1829-curric-s4-unmask-retry2 checkpoints, logs, and exported policy 2026-08-04 22:06:04 +01:00
Josh Creek 884b7799a0 fix(hud): have game mode hand HUD its target ship instead of group lookup
HUDController found its ship via get_first_node_in_group("ship"), a group
that has 2+ members once a match has an AI opponent — it only worked
because the player ship happened to spawn first. spawn_camera_rig now
wires the HUD's ship the same way it already wires the camera rig's
target.
2026-08-04 19:54:55 +01:00
Josh Creek 5193776d86 perf(match): emit timer_updated only when the displayed second changes
match_mode.gd fired the signal every frame while only the once-a-second
value is displayed, forcing the HUD to re-format and re-shape the label
each frame. Gate emission on the whole-second value changing, matching
ship.gd's threshold-gated telemetry discipline.
2026-08-04 19:41:37 +01:00
Josh Creek c96325144a fix(match): guard kickoff countdown against a post-match resume
The coroutine stalls mid-countdown while the tree is paused for the
results screen, but _end_match unpauses before the deferred scene
change actually tears things down — letting it resume for a frame and
re-emit kickoff_countdown / unfreeze bodies in the dying scene.
2026-08-04 19:35:41 +01:00
Josh Creek fa9590d9c3 refactor(team-colors): collapse disagreeing team palettes into one source of truth
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.
2026-08-04 19:18:45 +01:00
Josh Creek ff2e40198f fix(arena): duplicate shared Environment before per-arena mutation
WorldEnvironment's Environment sub-resource was shared across every
instantiate() of a cached arena PackedScene, so glow/brightness/sky
tweaks in Arena._ready() compounded further each time a player
re-entered an arena instead of applying fresh.
2026-08-04 18:56:22 +01:00
Josh Creek c75e142c8a perf(physics): name and assign collision layers
Adds a [layer_names] section to project.godot (Ships/Ball/Arena/
GoalSensor) and sets collision_layer/collision_mask on the 4 physics
body roots (Ship, Ball, Goal Area3D, ArenaBoundary StaticBody3D),
which previously all sat on the default layer 1 / mask 1 so every
body broadphase-tested against every other.

Ships collide with ships/ball/arena but no longer test against the
goal sensor; the goal's mask (Ball only) is the physics-level fix for
what goal.gd's is_in_group("ball") check was doing defensively in
code (left in place, now a no-op guard). ArenaBoundary's runtime-
generated CollisionShape3D children inherit layer/mask from the
StaticBody3D root automatically.

Verified live via godot-mcp under Jolt Physics: ship-vs-ship and
ship-vs-ball collisions still transfer momentum, the ball still
bounces off arena walls, a ball entering a goal still fires
goal_scored (score updates / HUD reset), and a ship teleported into a
goal recess produces zero overlapping bodies on the goal Area3D (was
reachable before, physics-level fix confirmed, not just the code
guard). Headless smoke test (free_play.tscn) is clean.
2026-08-04 18:32:54 +01:00
Josh Creek 96ff503fa8 perf(ship): merge non-tinted hull meshes into one node
Hull/Canopy/EngineGlowL/EngineGlowR are runtime-baked into one ArrayMesh
in _ready via SurfaceTool.append_from, dropping 4 MeshInstance3D children
to 1 (Nose/TailFin stay separate, they're retinted per-team). Skipped in
headless mode like the goal/arena_boundary visual builds, since physics
only cares about CollisionShape3D.

All 6 source surfaces (hull, canopy, engine_l x2, engine_r x2) carry
distinct materials, so this doesn't literally cut draw calls 6 to 3 as
TODO.md assumed — Godot still issues one draw call per surface regardless
of node count. The real win is scene-tree/transform overhead, not batching.
2026-08-04 18:16:02 +01:00
Josh Creek 7b086fbd8f perf(ship): share per-team accent material instead of allocating per ship
_apply_team_color() allocated a fresh StandardMaterial3D on every call, and
ran at least twice per ship (once from _ready at the default team, once from
the team setter when the game mode assigns the real team). Cache one
StandardMaterial3D per team in a static dict on Ship and reuse it across
every ship on that team.
2026-08-04 17:53:15 +01:00
Josh Creek 0603452264 perf(goal): merge goal visuals into one ArrayMesh
Goal._build_visuals() built 10 MeshInstance3D nodes across 4 materials
(1 pocket + 1 net + 4 bezel-ring + 4 rim-ring boxes) per goal. Replaced
with a single MeshInstance3D wrapping one ArrayMesh with 4 SurfaceTool-
committed surfaces (pocket, net, bezel, rim), one material per surface
via surface_set_material — 10 nodes down to 1, same 4 materials.

Kept 4 surfaces rather than collapsing further: the rim is a tuned
team-tinted emitter, the net carries its own discard shader, and the
pocket/bezel differ in albedo/metallic/roughness. Merging those into a
shared material would be a visible regression, not a free win.

Added a local box-to-SurfaceTool helper (6 quads via arena_boundary.gd's
_add_quad/_add_tri winding-correction trick, copied in rather than
shared since that file's geometry is collision-adjacent). The pocket's
old cull_mode = CULL_FRONT trick is replaced by emitting its geometry
with inverted winding; the net's cull_front stays material-driven since
goal_net.gdshader's own render_mode depends on that winding convention.

Verified in the editor: both team-tinted goals render an intact pocket,
net, bezel and glowing rim with no backface/winding artifacts, and the
headless free_play smoke test still runs clean.
2026-08-04 17:48:39 +01:00
Josh Creek b01d5a68d9 perf(arena): cache the active camera lookup in ArenaBoundary
_process() called get_viewport().get_camera_3d() every frame to drive
the containment field's camera-side fade. Cache it the same way
ship_camera.gd caches the ball, revalidating with is_instance_valid
since exactly one camera rig is spawned per game-mode run today.
2026-08-04 17:36:37 +01:00
Josh Creek bb56f69edc perf(hud): stop flight instruments redrawing when values have settled
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.
2026-08-04 17:33:52 +01:00
Josh Creek fbb783b947 perf(ship): gate telemetry emission on listeners and headless mode
Ship._emit_telemetry_data() ran get_euler()+trig every physics tick
for every ship regardless of whether a HUD was watching, wasting work
on AI ships and every headless training instance. Disable
_physics_process outright when headless, and skip emission the rest
of the time unless a signal actually has a listener.
2026-08-04 15:15:43 +01:00
Josh Creek 08a0f74391 refactor(*): DRY up arena scenes, game modes, and HUD instruments
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.
2026-08-04 15:07:33 +01:00
Josh Creek b285d012dc chore(*): Update todos 2026-08-04 14:14:15 +01:00
Josh Creek c7cd740f23 chore(assets): VRAM-compress arena textures, BC7 for the nebula sky
Move the nebula sky, planet surface and particle glow textures to VRAM
compression with mipmaps, so they stop being uploaded uncompressed.

The sky panorama is forced to high quality (BC7). It is a 5 MB image of
smooth nebula gradients seen through the arena walls, and BC1/BC3's 5:6:5
endpoint interpolation bands visibly across exactly that kind of content.
2026-08-04 13:36:43 +01:00
Josh Creek a836a96d13 feat(weather): keep nebula dust clear of the play volume
The dust emitter's box fully encloses the arena, so motes drifted through the
pitch and hazed the surfaces the player is trying to read.

Fade each mote by the Chebyshev distance of its world-space origin outside
the play volume, taken from MODEL_MATRIX's translation column so it is
unaffected by the billboard rewrite of VERTEX. Motes inside the box are fully
hidden and fade in over arena_clear_distance beyond it, so the effect reads as
weather drifting around the station rather than through the match.

Extents default to ArenaBoundary's play volume and are exposed as uniforms.

Also adds the missing .uid sidecar for the shader.
2026-08-04 13:36:43 +01:00
Josh Creek e62d844c1d fix(render): drop SDFGI, add fill lights and enable debanding
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.
2026-08-04 13:36:30 +01:00
Josh Creek df3e168b31 feat(arena): add elevated-goal arena variants
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.
2026-08-04 13:36:19 +01:00
Josh Creek 66b6215fdd feat(goal): recess goals into the hull behind a netted pocket
The goal was a single flat translucent slab. Because the chase camera sits
behind the goal line at kickoff it rendered as a large blue rectangle across
the lower screen, and the hull beside the mouth was translucent field panel,
so the pocket behind it showed straight through the wall and the net appeared
a second time alongside the frame.

The boundary now carries an opaque bulkhead around each mouth, which occludes
properly. On top of that the goal becomes a pocket sunk into the wall: a dark
machined bezel lining the opening, a thin team-coloured emissive rim flush
with the wall face, and netting from a single box viewed inside-out, giving a
five-sided pocket instead of a flat panel across the back.

The net is cut with discard rather than alpha blending so it still writes
depth and sorts against the frame and the containment field like solid
geometry; a blended net would join the transparent queue and sort per object
against the boundary shell, which is the artefact this whole pass removed.

goal.tscn drops from 18 node/sub-resource blocks to 3 — it is now just the
Area3D, its CollisionShape3D and the script. All geometry is built in code
from two loops, and the mouth is measured off the collision shape rather than
restated, so the frame cannot drift from the volume that actually scores.

The sensor, its collision shape and the goal_scored signal are unchanged, and
visuals are skipped under --headless.
2026-08-04 13:36:07 +01:00
Josh Creek 02df09e40d feat(arena): draw the play volume as one non-overlapping surface shell
Every visible surface of the enclosure shared one alpha-blended material and
was drawn as several overlapping layers: floor, ceiling and four walls as
solid BoxMeshes, plus corner curves and fillets generated on top of the box
faces they eased into. Alpha-blended surfaces don't write depth and sort per
object, so the perimeter composited that tint twice and the corners three
times, giving hard-edged trapezoidal patches that re-sorted as the camera
moved. The floor box also overhung the walls by 1 m and showed through them.

Replace all of it with a single generated mesh covering the inner surface
exactly once, every piece cut to meet its neighbours edge-on and only
inward-facing triangles emitted. It carries two surfaces: an opaque hull
(deck, base fillets, goal bulkheads) and the translucent containment field
(walls, corners, ceiling fillets, ceiling).

The field shader is additive and unshaded rather than alpha-blended: additive
cannot double-darken and composites order-independently, so overlap is
structurally invisible, and being unshaded it no longer picks up per-arena
light and GI gradients across a 28x40 m panel. Fresnel replaces the old
StandardMaterial3D rim, which was a lit effect and the wrong tool. The deck
shader draws plating and field markings procedurally from the boundary's own
constants, so markings cannot drift from the collision geometry.

Per-face MeshInstance3D visibility toggling is gone; the field shader fades
facets the camera has crossed outside of per-pixel from one uniform, which is
what allows a single merged mesh.

Also adds ELEVATED goal mode and the ceiling fillets, and skips the mesh build
entirely under --headless, where training spawns instances that never render.

Collision is untouched: 166 collider shapes in FLOOR mode and 158 in ELEVATED,
verified byte-identical to before, so trained policies in Game/bots/ are
unaffected.
2026-08-04 13:35:55 +01:00
Josh Creek 6f5ce488a9 feat: lit particle shader for nebula dust weather effect
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.
2026-08-04 07:42:34 +01:00
Josh Creek 73fb83a0da feat: richer nebula sky and planet surface textures
Extend gen_nebula_sky.py with two more dust-lane layers at different
scales and subtle hue variation within the bright core. Add
gen_planet_surface.py (no prior generator existed) producing latitude
bands, storm vortices, and a lit/unlit terminator baked from the
planet mesh's actual UV convention against arena_02's directional
light, verified in-engine via screenshots.
2026-08-04 07:18:51 +01:00
Josh Creek 3257f5cbcc feat: greeble/detail pass on nebula station and debris models
Add tools/blender/gen_nebula.py (previously nebula_decoration.blend had
no generator script, unlike ship/ball) to rebuild the station with
inset/extrude panel-line greeble and three separate emissive window
strips, and give debris its own rockier, damage-scarred materials
instead of cloning the station's hull material.
2026-08-03 23:09:50 +01:00
Josh Creek 11d81a910e chore: add missing .uid sidecars for settings_menu.gd and video_settings.gd 2026-08-03 22:41:55 +01:00
Josh Creek 1eb5a3188d feat: replace ship and ball placeholder meshes with Blender-modeled assets
Ship gets a greebled hull, tapered nose, swept canopy, twin engine nacelles,
and tail fin (built via the vendored Blender MCP, generator committed at
tools/blender/gen_ship.py) in place of the 5 flat primitives. The ball is
fully remodeled as a smooth round sphere with a crossed emissive accent
pattern (gen_ball.py), replacing the old flat-shaded gold_ball rather than
just tweaking its material.

Node names (Nose/TailFin) are preserved for Ship._apply_team_color(), and
the RigidBody3D/CollisionShape3D physics on both ship.tscn and ball.tscn are
untouched so RL-trained bots and flight feel stay valid. Each part's mesh is
extracted to a standalone .res (tools/blender/extract_meshes.gd) rather than
referenced via glb::ArrayMesh_xxx sub-paths, which don't reliably resolve
across scene files and were silently rendering both models invisible.
2026-08-03 22:39:46 +01:00
Josh Creek 50c2014361 feat: enable SDFGI/reflection probes and fresnel glass boundary
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.
2026-08-03 21:29:59 +01:00
Josh Creek 1c08ab3566 fix: de-duplicate hero star diffraction-spike stamps in nebula sky
Every bright star in sky_nebula.png reused the exact same diffraction-
spike stamp, just relocated. Commit the generator (recovered from an
ephemeral scratchpad) to tools/textures/gen_nebula_sky.py, randomize
each hero star's rotation, arm count, spike length, and brightness,
and regenerate the texture.
2026-08-03 20:04:36 +01:00
Josh Creek 3fdf270aa9 feat: add post-processing pass and video settings menu
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.
2026-08-03 19:53:56 +01:00
Josh Creek 171cd4a840 feat: add Nebula and Asteroid Field arenas
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.
2026-08-03 19:11:13 +01:00
Josh Creek 1af3e0410d chore: vendor blender-mcp as a git submodule alongside godot-mcp
Both mcp/godot-mcp and mcp/blender-mcp were already cloned locally and
registered in .gitmodules/.mcp.json/CLAUDE.md, but neither submodule's
gitlink had actually been committed, so a fresh clone wouldn't pull
either down. Stages the two gitlinks so `git submodule update --init
--recursive` works as documented.
2026-08-03 19:11:13 +01:00
Josh Creek 4507b6dc1b feat: add main-menu difficulty picker for Match mode
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.
2026-08-03 19:11:13 +01:00
CosmicClash Training Bot 20f6be7e28 chore(training): curriculum progress after 20260802-1458-curric-s4-unmask-retry1 2026-08-03 18:29:13 +01:00
CosmicClash Training Bot 5fe53b2406 chore(training): Add 20260802-1458-curric-s4-unmask-retry1 checkpoints, logs, and exported policy 2026-08-03 18:27:21 +01:00
CosmicClash Training Bot 62dc0a2981 chore(training): curriculum progress after 20260801-1131-curric-s4-unmask 2026-08-02 14:58:25 +01:00
CosmicClash Training Bot f62ddde369 chore(training): Add 20260801-1131-curric-s4-unmask checkpoints, logs, and exported policy 2026-08-02 14:56:35 +01:00
CosmicClash Training Bot fa53d72f63 chore(training): curriculum progress after 20260801-0658-curric-s3-unmask-ramp75 2026-08-01 11:31:57 +01:00
CosmicClash Training Bot e5a0df63c5 chore(training): Add 20260801-0658-curric-s3-unmask-ramp75 checkpoints, logs, and exported policy 2026-08-01 11:31:48 +01:00
CosmicClash Training Bot be1bf37e0b chore(training): curriculum progress after 20260801-0223-curric-s2-unmask-ramp50 2026-08-01 06:58:20 +01:00