# TODO Deferred work, in rough priority order. The current architecture (ShipAction/ShipController seam, Arena/GameMode split, code-driven spawning, group-tagged ball/goals) was chosen specifically so these bolt on without rework. ## AI opponent (reinforcement learning) The training pipeline is built — see `TRAINING.md` (self-play PPO via the vendored godot_rl_agents bridge, JSON policy export, in-game GDScript inference, eval ladder). Remaining: - [x] Promote a first tier: `curric-s6-unmask` copied into `Game/bots/promoted/easy.json` as the shipped "easy" bot (see TRAINING.md's "Promoted bots" section) — `match.tscn`/`spectate.tscn` now default there instead of `run05.json`. - [ ] Long training runs on the Linux/3090 box to produce actually-good bots; promote further checkpoints into `Game/bots/promoted/` as `medium`/`hard` tiers once they clear `easy.json` in `evaluate.py`. - [x] Staged curriculum (score → defend → avoid draws → full mechanics) via `train.py`'s `--opponent-mode`/`--draw-penalty`/`--attack-goal-bias`/`--vertical-ramp`/`--pitch-roll-ramp` flags — see TRAINING.md's "Curriculum training" section. `--opponent-mode=frozen` is a single-fixed-model slice of the league idea below, not the full sampled pool. - [ ] Frozen-opponent league: train the live policy against a *pool* of past exported checkpoints, sampled per-episode (today's `--opponent-mode=frozen` only supports one fixed model per run) to prevent self-play strategy collapse on long runs. - [ ] Richer state setter / curriculum: aerial states, wall plays, rebound scenarios as skill grows (beyond the score/defend/draw staging already in place). - [x] Main-menu difficulty picker: Easy/Medium/Hard presets in `main_menu.gd` set `bot_model_path`/`bot_reaction_ticks`/`bot_action_noise` via `GameSettings`. Raw-checkpoint testing and Spectate mode moved into a `DevSection` hidden outside debug builds. All three tiers currently reuse `promoted/easy.json` with different handicaps until `medium`/`hard` checkpoints are promoted. ## Multiplayer (long term) - [ ] `RemoteShipController extends ShipController` — feeds replicated `ShipAction`s from a network peer into the local ship simulation. - [ ] Networked `GameMode` subclass: per-peer ship spawning (MultiplayerSpawner or custom), authoritative server for ball/score. - [ ] C# backend / online servers per README roadmap (not started). - [ ] Possible v0.2 split-screen: spawn one `ship_camera_rig` + viewport per local player (camera is already outside the ship scene to allow this). ## General - [x] More arenas: `arena_02.tscn` (Nebula) and `arena_03.tscn` (Asteroid Field) added alongside `arena_01.tscn`, all following the same template (goals/spawns/`arena_boundary.tscn` unchanged) and listed in `scripts/arena_registry.gd`. 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.tscn` untouched. - [x] arena_02 (Nebula) redone RL-style: `arena_boundary.tscn`'s field material is now near-invisible glass (shared by all arenas), the sky is a baked photoreal-style equirect nebula texture (`assets/textures/sky_nebula.png`, procedurally generated offline — no copied astrophotography) instead of a procedural cell-shader, a `NebulaDust` `GPUParticles3D` gives a drifting-motes "weather" effect, and the `Decoration` node holds a station/debris/planet environment (Blender-modeled, `assets/blender_models/nebula_decoration.blend` → `assets/models/nebula_*.glb`) visible in multiple directions. `arena_01`/`arena_03` still use the older embedded-shader sky — same treatment for those is unstarted follow-up work. ## Visual quality pass (arena_02 critique follow-up) A subagent ran the game and critiqued arena_02 head-on against Rocket League 2 in Unreal Engine 5: verdict was "a flat-shaded, unlit-looking blockout dressed up with two nice noise textures" — not fundamentally unfixable in Godot (SDFGI/Nanite-tier GI and hero-sculpted geometry aside), but several concrete gaps. Work through these one at a time, in this order (cheapest/highest-impact first). Each item has a ready-to-use prompt — paste it into a fresh session to tackle just that piece. The texture-generation Python scripts used for `sky_nebula.png`/`planet_surface.png` currently only exist in an ephemeral scratchpad, not the repo — the first item that touches them should commit a copy into the repo (e.g. `tools/textures/`) so they're reproducible. - [x] **Post-processing pipeline** (no models needed — cheapest, highest-impact item on the list). Nothing currently sits on top of the raw render: no bloom/glow, no AA, no color grading, no vignette beyond the floor's baked fade. Prompt: "In Cosmic Clash (Godot 4.7), the arena Environment resources (start with `Game/scenes/arena_02.tscn`'s `Environment_nebula`, then apply consistently across `arena_01`/`arena_03` too) currently have no post-processing. Enable and tune Glow (bloom) so the nebula core, emissive accent strips on the station model, and bright stars actually bleed light; enable Adjustments (`adjustment_enabled`) for a subtle contrast/saturation grade. Godot's `Environment` has no built-in vignette property — if that's still wanted, it needs a small custom full-screen shader (a `CompositorEffect` or a screen-space `ColorRect` overlay), not a toggle; treat it as a separate, smaller sub-task rather than assuming it's free. Also enable FXAA and/or TAA via `project.godot`'s `rendering/anti_aliasing/quality/*` settings project-wide to fix the aliased hard edges visible on ship geometry. Motion blur and depth-of-field are deliberately out of scope here — Godot has no built-in equivalent, and faking either well needs a custom `CompositorEffect`, which is a much bigger task than this pass. Verify with before/after screenshots via godot-mcp (`run_project` on `free_play.tscn` with the Nebula arena selected) — don't just eyeball the editor, actually run the game." - [x] **De-duplicate the nebula sky's star sprites**. Every bright star in `sky_nebula.png` is the exact same diffraction-spike stamp at the same size/brightness, just relocated — called out as "the single most amateur-looking tell in the whole scene" once you look for more than a second. Fixed: the generator is now committed at `tools/textures/gen_nebula_sky.py`; the hero-star loop randomizes rotation, arm count (4 or 8), spike length, and brightness per star, and `sky_nebula.png` was regenerated/reimported and confirmed via in-game screenshot. Prompt: "Commit the nebula sky texture generator (currently only in an ephemeral scratchpad — recreate it if needed: numpy/Pillow script generating a 4096x2048 equirect nebula via layered FFT/domain-warped noise, color-graded, with drawn hero stars) into the repo at `tools/textures/gen_nebula_sky.py`. Fix the hero-star drawing loop so each star's diffraction-spike stamp gets randomized rotation, spike length, and brightness instead of reusing one identical stamp at every location. Regenerate `Game/assets/textures/sky_nebula.png`, reimport, and confirm via an in-game screenshot that the repeated-stamp tell is gone." - [x] **Real PBR lighting + materials, fresnel glass boundary**: SDFGI enabled and a `ReflectionProbe` added to all three arenas (`arena_01`/`02`/`03.tscn`); `arena_boundary.tscn`'s shared `StandardMaterial3D_field` converted from unshaded to shaded (roughness 0.05, rim-enabled fresnel highlight, alpha still near-invisible face-on). Verified via godot-mcp screenshots (60 FPS / 17ms frame time, no measurable cost) and zero-error headless runs of `free_play`/`match`/`spectate`/`training`. Prompt: "In Cosmic Clash, enable SDFGI and add reflection probes to the arena scenes (start with `arena_02.tscn`) so surfaces get real bounce lighting/reflections instead of flat ambient. Convert `arena_boundary.tscn`'s `StandardMaterial3D_field` from unshaded to a proper shaded material with a fresnel-based rim highlight (bright at grazing angles, near-invisible face-on) so it reads as glass rather than a tinted overlay — check the performance impact of moving it off unshaded, given it's a large always-visible surface. Verify visually via godot-mcp screenshots, and confirm headless runs (`free_play`/`match`/`spectate`/`training`) still show zero errors." - [x] **Greeble/detail pass on the station + debris models** (Blender remodel — explicit models to redo). Rebuilt via a new committed generator, `tools/blender/gen_nebula.py` (`nebula_decoration.blend` had no prior script, unlike ship/ball — this brings it in line): the station gets panel-line inset/extrude greeble across three passes and 3 separate emissive window strips (was 1), and debris gets its own `Mat_DebrisRock`/`Mat_DebrisScorch` materials plus per-vertex jitter and impact-crater gouges instead of cloning the station's `Hull_Metal_Dark`. The full normal-map/AO bake was skipped as too fragile to script reliably (per this item's own fallback allowance) — detail is geometry + material-only, no trim texture needed on top. Prompt: "Using the blender MCP, rebuild `nebula_station` (source in `Game/assets/blender_models/nebula_decoration.blend`) with actual surface detail: greebled panel-line insets via bmesh inset-and-extrude on selected faces (more scriptable and robust than chained boolean cuts — prefer this over booleans for the main detailing pass), and 2-3 emissive window/light strips distinct from the existing single accent strip. A full normal-map/AO bake pipeline (low-poly + high-poly pair, UV unwrap, bake settings) is the ideal AAA-style finish but is fragile to script end-to-end in one pass — attempt it, but if it proves too unreliable, fall back to the inset/extrude geometric detail alone plus a simple tiled trim-sheet-style texture rather than forcing a bake that doesn't work. Give `nebula_debris` chunks a rockier/damaged material distinct from the station's clean hull (darker, rougher, maybe scorch-mark variation) so they read as separate debris rather than clones of the station's material. Re-export both to `Game/assets/models/nebula_station.glb`/`nebula_debris.glb`, re-save the shared `.blend`, and re-verify placement/transforms in `arena_02.tscn` still look right (screenshot check)." - [x] **Richer nebula sky + planet surface textures** (extends the existing procedural generation — no new asset types). `sky_nebula.png` is essentially one noise-filter pass over a bright core; `planet_surface.png` is a flat gradient with a single vortex swirl and no bands, craters, or day/night terminator. Done: `tools/textures/gen_nebula_sky.py` gained two more dust-lane layers at different scales plus subtle patchy hue variation within the bright core; `tools/textures/gen_planet_surface.py` is a new generator (no prior script existed, despite the note below) producing latitude bands, 3 storm vortices, and a lit/unlit terminator baked from the sphere's exact UV convention (reverse-engineered from `nebula_planet.glb`'s vertex data) dotted against `arena_02.tscn`'s actual `DirectionalLight3D` direction — verified in-engine via godot-mcp screenshots showing a clean crescent terminator; both `Game/assets/textures/planet_surface.png` and the live-feeding sidecar `Game/assets/models/nebula_planet_planet_surface.png` were regenerated and reimported. Prompt: "Extend the nebula/planet texture generators (see `tools/textures/` once committed, per the star-sprite TODO item above) with more detail layers: for the sky, add a second/third dust-lane layer at a different scale plus subtler color variation within the bright core (real nebulae aren't one flat color); for the planet, add a proper lit/unlit terminator gradient (the side facing the arena's directional light should read brighter), more band variation at different latitudes, and a couple more storm-vortex features so it doesn't read as a single gradient with one twist. Regenerate both textures, reimport, and screenshot-verify." - [x] **Particle lighting response for the nebula dust ("weather") effect** (no models — particle material/shader only). `NebulaDust`'s `StandardMaterial3D` was replaced with a new `ShaderMaterial` (`Game/shaders/nebula_dust.gdshader`, the project's first particle/spatial shader): a fake per-pixel puff normal is derived from local UV on the true-billboarded quad and fed into a `light()` override (`LIGHT`/`LIGHT_COLOR`/`ATTENUATION`) so motes visibly catch the arena's directional light and pick up a fixed nebula-core color bias; alpha is multiplied by a depth-texture-based soft-particle fade so motes no longer hard-clip through boundary/decoration geometry; a per-particle flicker (hashed from `INSTANCE_CUSTOM.x` + `TIME`) adds subtle sparkle. Verified via godot-mcp screenshots from multiple angles (visible lit/unlit shading variation, smooth fade near geometry, per-mote sparkle) and zero-error headless runs of `free_play`/`match`/`spectate`/`training`. Prompt: "In `arena_02.tscn`'s `NebulaDust` particle system, replace the current unshaded glow-sprite material with a custom shader that fakes lighting response — note the sprites are camera-facing billboards, so they have no fixed world-space normal and true per-pixel PBR lighting won't behave like it would on a solid mesh. A practical approach: derive a fake per-pixel normal from the quad's local UV (as if each sprite were a small sphere/puff, same trick used for 2D lit particle effects) and light that against the directional light + a fixed 'glow color' bias toward the nebula core direction, rather than relying on the mesh's real (camera-facing) normal. Add soft-particle depth fade (compare particle depth to the depth buffer) so motes don't hard-clip through the boundary/decoration geometry, and add subtle per-particle brightness flicker via the color ramp or shader for sparkle. Verify visually via godot-mcp screenshots that the dust reads as lit rather than flat-glowing, from a couple of different camera angles (billboard lighting tricks can look wrong from some angles even when right from others)." - [x] **Ship + ball visual pass**: `Game/objects/ship.tscn`'s 5 primitives replaced with a Blender-greebled hull/nose/canopy/tailfin/twin nacelles (source `Game/assets/blender_models/ship.blend`, generator `tools/blender/gen_ship.py`); ball fully remodeled as a smooth round icosphere with a crossed emissive accent pattern (`ball.blend`, `gen_ball.py`) rather than just a material tweak, replacing the old flat-shaded `gold_ball`. `Nose`/`TailFin` node names preserved for `_apply_team_color()`; `CollisionShape3D`/`RigidBody3D` physics on both ship and ball untouched (verified). Each part's mesh is extracted to a standalone `.res` (`tools/blender/extract_meshes.gd`) rather than referenced via `glb::ArrayMesh_xxx`, which doesn't reliably resolve across scene files. Verified via in-game screenshots (both team colors, orientation) and headless runs of `free_play`/`training`/`match`. Prompt: "Using the blender MCP, model an actual low-poly-but-detailed ship hull (greebled fuselage panel lines, a proper cockpit canopy shape, 1-2 engine nacelles with emissive exhaust glow) to replace `Game/objects/ship.tscn`'s current 5 primitive `MeshInstance3D` parts (`Hull`/`Nose`/`Canopy`/`TailFin`/`EngineGlow`), built the same procedural way as the arena decoration (bpy/bmesh primitives + bevels + inset/extrude detail — kept original, no borrowed/asset-store art). Two hard constraints this must respect: (1) **`scripts/ship.gd`'s `_apply_team_color()` recolors two child nodes by exact name, `get_node_or_null(\"Nose\")` and `get_node_or_null(\"TailFin\")`, at runtime per-team** (team 0 blue, team 1 orange, set via `game_mode.gd`'s `ship.team = team`) — either keep two `MeshInstance3D` parts of the new model literally named `Nose` and `TailFin` so this keeps working untouched, or update `_apply_team_color()`'s node-name list in the same change if the new model's parts are named differently; don't let this silently break. (2) **Do not touch `ship.tscn`'s single `CollisionShape3D` (`BoxShape3D`, size `1 x 1 x 4`) or the `RigidBody3D`'s `mass = 5.0`/`inertia = (1,1,1)`/`physics_material_override`** — those are exactly what the trained RL bots and flight feel are tuned against (per CLAUDE.md). Keep the new visual mesh's silhouette reasonably close to that `1 x 1 x 4` box so the ship doesn't visually stick out of or float inside its own collision bounds. Separately, audit the existing gold ball's material (`Game/assets/models/gold_ball.glb`/`.res`, referenced from `Game/objects/ball.tscn`'s `MeshInstance3D` with no scene-level material override, so its look currently comes entirely from whatever's embedded in the imported mesh) and upgrade it to a proper metallic/emissive-accent PBR look if it turns out flat/unshaded once the lighting pass above lands — don't assume it needs work without checking first. Verify both visually (screenshot, including a look at team-color recoloring in Match/Spectate) and via a headless run of `free_play.tscn`/`training.tscn` to confirm physics/RL behavior is unaffected."