Files
CosmicClash/TODO.md
T
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

8.7 KiB

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:

  • 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.
  • 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).

Correctness

Bugs found in an adversarial review. None are gameplay- or physics-affecting, so all are safe to land against the current Game/bots/ checkpoints.

  • VideoSettings.apply_to_environment() (scripts/video_settings.gd) compounds on every arena load: env.glow_intensity *= glow_scale mutates an Environment that is a [sub_resource] of the arena scene, and Godot shares sub-resources across instantiations of a cached PackedScene. Glow at 50% becomes 25% then 12.5% across repeat entries. Fix by duplicating the Environment in Arena._ready() (scripts/arena.gd). Regression test: set glow to 50%, enter/leave Free Play three times, confirm it's still 50%.
  • Collapse the four disagreeing team palettes into one source of truth — ship.gd, HUDController.gd and goal.gd each declare TEAM_COLORS, arena_boundary.gd exports team0_tint/team1_tint, and arena_deck.gdshader defaults to a fifth pair. Three of them disagree, so nose, goal rim, end zone and scoreboard are all different blues.
  • Goal scoring volume (3.5 x 1.5, objects/goal.tscn) is smaller than the drawn mouth (3.7 x 1.65, ArenaBoundary.GOAL_APERTURE_*) — a ball crossing the visible edge doesn't score. Derive the aperture constants from the goal's collision shape, the way goal.gd:53 already measures its own visuals.
  • match_mode.gd: full time can fire mid-kickoff-countdown, and the stalled coroutine resumes into the dying scene (can re-emit kickoff_countdown / unfreeze bodies for a frame). Guard _run_kickoff_countdown with a match-over flag.
  • match_mode.gd emits timer_updated every frame for a value that changes once a second; the HUD re-formats and re-shapes the label each time. Emit only on change, matching ship.gd's threshold-gated telemetry discipline.
  • HUDController binds to get_first_node_in_group("ship") in a group that always has 2+ members — works only because the player ship happens to spawn first. Have the game mode hand the HUD its target ship.
  • Reuse a member ShipAction in ship.gd (controllerless path) and player_ship_controller.gd instead of allocating one per physics tick; ai_ship_controller.gd already does this correctly.
  • Delete the duplicate 1 MB texture — assets/textures/planet_surface.png and assets/models/nebula_planet_planet_surface.png are byte-identical.

Performance

  • Shared per-team materials instead of Ship._apply_team_color() allocating a fresh StandardMaterial3D and assigning it as material_override (and running at least twice per ship — once from _ready, once from the team setter). Removes the allocation and lets same-team ships batch.
  • Merge each goal's visuals into one ArrayMesh with a hull surface and a net surface — currently 11 MeshInstance3Ds and 4 materials per goal, ~22 draw calls for a static prop. arena_boundary.gd:333 already demonstrates the SurfaceTool technique in this codebase.
  • Merge the four non-tinted ship meshes (Hull/Canopy/EngineGlowL/R) into one — 6 draw calls per ship down to 3. Irrelevant at 1v1; 36 calls before VFX at 3v3.
  • Cache the camera in ArenaBoundary._process instead of a get_viewport().get_camera_3d() tree lookup every frame (ship_camera.gd already caches the ball this way).
  • Name collision layers in project.godot and assign them — nothing configures collision_layer/collision_mask today, so every body tests against every other.
  • Measure nebula_dust.gdshader's per-fragment depth-texture sample across 500 large soft billboards before adding more particle work.
  • Bake ArenaBoundary's ~160 runtime-generated CollisionShape3D nodes into the scene. Costs a load hitch on every arena entry and repeats in every parallel headless training env. Blocked on the trained-bot decision belowarena_boundary.gd:235 notes this geometry is what the shipped policies were fitted against, so the bake must be verified byte-identical.

Presentation / AAA polish

The largest gap between this and a AAA-feeling product is presentation, not code. Sequenced after the above for pragmatic reasons, but this is the highest impact per hour.

  • Audio — there is none. Zero sound files, zero AudioStreamPlayer nodes, no bus layout. Needs: engine hum pitched to throttle, turbo whoosh, ball impacts scaled by collision impulse, wall scrapes, goal explosion, crowd bed, UI clicks, countdown beeps, music. Can be driven off Ship's existing telemetry signals.
  • SSAO/SSIL in the arena Environments — cheapest single perceived-quality win available; grounds the ships against the deck and gives the fillets and goal recesses real depth.
  • VFX on anything that moves: EngineGlowL/R are static meshes that don't respond to throttle. No thruster plume, turbo flame, ball trail, impact sparks or goal burst. arena_02 is the only scene with any particles at all.
  • Impact feedback — screen shake, hit-stop, flash, controller rumble on ball contact.
  • Camera feel in ship_camera.gd — fixed distance/height/FOV today. Speed-based FOV widening, turbo kick, impact shake.
  • Goal celebration sequence: today it's a print() and a label scale-pop. Wants an explosion, team-tinted screen flash, camera cut, slow-mo, title card. HUDController's ResultOverlay animation is a reasonable template.
  • Local lighting / LightmapGI bake — two directional lights and low ambient, no local lights anywhere. arena_deck.gdshader's hull_fill uniform is explicitly a "stand-in for bounce light", i.e. the shader is compensating for lighting that isn't there. The arena is fully static, so a bake is viable; retire hull_fill afterwards.
  • Dress arena_03 — its Decoration node is empty, while arena_02 has stations, debris, a planet and volumetric dust.
  • Custom font + a real Theme resource for the HUD. The procedural instruments are well-engineered, but ThemeDB.fallback_font at 10-13 px reads as a debug overlay.
  • Post-processing beyond glow: DoF, motion blur, vignette, chromatic aberration on turbo.

Open decision — trained-bot compatibility

Four items collide with the checkpoints in Game/bots/. Decide the policy before scheduling any of them; everything in Correctness and DRY above is safe either way.

  • Per-tick drag (ship.gd): state.linear_velocity *= drag_coefficient and angular_velocity *= 0.9 aren't delta-scaled, and project.godot never pins physics/common/physics_ticks_per_second. Correct at 60 Hz, silently different at any other rate. Zero-risk option: pin the tick rate to 60 and document the dependency. Correct-but-breaking option: delta-scale it and retrain.
  • Goal aperture — cutting a real opening in the end walls so the ball visibly enters the net changes collision geometry the policies were fitted against. Today the walls are solid and the pocket/net are unreachable decoration.
  • Collider bake (see Performance above) — safe only if byte-identical to the current generated output.
  • Beyond 1v1ai_ship_controller.gd takes the first non-self ship as "the opponent" and ship_observations.gd has room for exactly one. The observation space is the hardest thing to change later, so decide whether 2v2/3v3 is in scope before more training time is spent.

Multiplayer (long term)

  • RemoteShipController extends ShipController — feeds replicated ShipActions 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).