Files
CosmicClash/CLAUDE.md
T

7.6 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Important rule: never create co-authored commits. Never mention Claude in commits.

Project overview

Cosmic Clash is an open-source, physics-based "vehicle soccer" game (a spiritual successor to Rocket League) built in Godot 4.7, using space ships instead of cars. The project is GDScript/Godot only right now — the "C# backend" mentioned in README.md is planned but not yet started. There is no server-side code; the MVP is local-only play against bots.

Because the gameplay concept (vehicle soccer) can't be copyrighted but specific expression can, all code/art/assets must be original — this is why the project uses Godot instead of Unreal/Unity and ships instead of cars. Keep this in mind when writing code or pulling in assets: don't port or closely mirror Rocket League's actual implementation.

Godot MCP server

This repo vendors godot-mcp as a git submodule at mcp/godot-mcp and registers it in .mcp.json. Prefer the godot-mcp tools over manual file edits or shell commands when the task involves inspecting or modifying the Godot project — reading/editing scenes, nodes, scripts, running the project, or interacting with a live Godot editor/runtime instance. It understands Godot's scene tree and .tscn/.gd structures directly, which is more reliable than hand-parsing them.

Setup after cloning (submodules aren't checked out by default):

git submodule update --init --recursive
cd mcp/godot-mcp
npm install
npm run build

GODOT_PATH (env var in .mcp.json) is left blank to auto-detect the Godot executable; set it explicitly if auto-detection fails on your machine.

Commands

There is no build step, linter, or automated test suite for the GDScript project itself — Godot projects run directly from source.

  • Open the project: open Game/ as a project in the Godot 4.7 editor, or run godot --path Game from the repo root.
  • Run the game: press Play in the editor, or godot --path Game res://scenes/main_menu.tscn.
  • Headless smoke test (RL/CI precondition — the game must run without rendering): godot --headless --path Game res://scenes/free_play.tscn.
  • The mcp/godot-mcp submodule is a separate Node/TypeScript project with its own npm install / npm run build (see above) — it is tooling, not part of the game itself.

Architecture

The structure was deliberately chosen so an RL-trained AI opponent and, later, multiplayer bolt on without rework (see TODO.md for the deferred work). The three load-bearing seams are the controller abstraction, the arena/game-mode split, and code-driven spawning.

  • Scene flow: scenes/main_menu.tscn (main_menu.gd, one handler per mode) → scenes/free_play.tscn (practice: no timer, R resets ball) or scenes/match.tscn (150s timer, per-team score, kickoff resets). Esc returns to the menu from either mode.
  • Controller seam (do not bypass): Ship (scripts/ship.gd, RigidBody3D) never reads Input. Each physics tick, _integrate_forces pulls one ShipAction (scripts/ship_action.gd: thrust Vector3, rotation Vector3, turbo bool, each axis -1..1) from its ShipController child (scripts/ship_controller.gd, base returns a zero action). PlayerShipController reads input actions; a future AIShipController (RL policy) or network-replication controller implements the same get_action() interface. A ship with no controller is inert but simulated. The ShipAction shape is the future RL action space — change it deliberately.
  • Arena vs game mode: scenes/arena_01.tscn (scripts/arena.gd, group "arena") is a stateless stadium — terrain, lighting, two Goal instances (team 0 and 1), BallSpawn and SpawnsTeam0/1 Marker3Ds — queried via get_ball_spawn()/get_ship_spawns(team)/get_goals(). Game modes extend GameMode (scripts/game_mode.gd, group "game"): the mode's scene contains an Arena + HUD, and the mode spawns ball/ships/controllers/camera in code (spawn_ship(team, index, controller) etc.) so ship counts and controller mixes stay flexible. free_play.gd and match_mode.gd override _start() and _on_goal_scored(conceding_team).
  • Goals are dumb sensors: scripts/goal.gd (Area3D, group "goal", @export team) only emits goal_scored(team) when a body in group "ball" enters; GameMode debounces it (_handle_goal_scored) and modes decide consequences. Never put scoring/reset logic in the goal.
  • Ship physics: all movement is force/torque-based (_integrate_forces), not kinematic — inputs become world-space forces/torques relative to ship orientation, with manual drag and speed clamps per tick. Physics formulas are commented inline; see FLIGHT_MANUAL.md for the player-facing flight model. Physics properties (mass, inertia, friction material) live in objects/ship.tscn, not in _ready overrides — keep the scene truthful; RL tuning depends on it.
  • Camera (scenes/ship_camera_rig.tscn, scripts/ship_camera.gd, group "ship_camera") is spawned by the game mode and given a target ship — ships have no camera/HUD dependency, so headless RL runs work (godot --headless).
  • HUD / telemetry pattern: Ship emits flight data via signals only when values change past thresholds (_last_* fields, *_THRESHOLD constants). HUDController (scripts/HUDController.gd on scenes/HUD.tscn, instanced by each mode's scene) discovers the ship, camera rig, and game mode via groups ("ship", "ship_camera", "game"), connects to signals, and only updates labels — no polling. Follow this discovery-by-group + signal-push pattern for new instruments or cross-node communication, not hardcoded get_node paths or per-frame polling.
  • Input actions are defined in Game/project.godot under [input] (move_forward, turn_left, turbo, reset_ball, etc.) and read only by PlayerShipController (plus mode-level _unhandled_input for reset_ball/ui_cancel) — add new controls there rather than hardcoding key checks.
  • Physics engine is Jolt (Game/project.godot, [physics] 3d/physics_engine="Jolt Physics").

Reinforcement learning / AI bots

See TRAINING.md for the full workflow (training, exporting, evaluating, difficulty tiers). Architecture summary:

  • scenes/training.tscn (scripts/training_mode.gd, extends GameMode) is the headless self-play environment: two ships driven by RLShipControllers, with ShipAIController (extends the vendored plugin's AIController3D) as the only class touching godot_rl types. The plugin is vendored (not a submodule) at Game/addons/godot_rl_agents — see its VENDORED.md; its C#/ONNX files are unused.
  • scripts/ship_observations.gd is the shared observation builder used by both training and in-game inference — never fork or diverge these two paths. Team 1's observations are mirrored (180° about Y) so one policy plays both sides.
  • In-game bots: scripts/ai_ship_controller.gd (a ShipController) runs the exported policy JSON via scripts/policy_network.gd (pure-GDScript MLP) — no .NET/ONNX/Python at runtime. Models live in Game/bots/; Match mode's bot_model_path/bot_reaction_ticks/bot_action_noise exports configure the opponent.
  • Python side lives in training/ (venv, not committed): train.py (SB3 PPO, launches parallel headless Godot instances from source), export_policy.py (checkpoint → JSON with parity check), evaluate.py (head-to-head eval, appends training/eval_history.json).
  • The flattened action space is Box(7): thrust xyz, rotation xyz, turbo (>0 = on) — this is ShipAction verbatim; change either only deliberately and together.