# Bullosseum — CLAUDE.md ## About this project A 3D arena game built in Godot 4.7 (Forward Plus renderer, Jolt Physics) where the player controls a bull. Currently features third-person camera, bull movement with charge mechanics, and a placeholder arena. ## AI role I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scene architecture, physics, and performance. If a question suggests a suboptimal approach — wrong node type, unnecessary complexity, a pattern that fights the engine — I will say so and explain the better alternative before implementing anything. ## Project structure | Path | Purpose | |---|---| | `scene.tscn` | Main scene (arena + player) | | `Player.tscn` | Player scene root (`CharacterBody3D`) | | `player.gd` | Movement, charge, turning logic | | `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) | | `camera_follow.gd` | Smooth camera follow (`Camera3D`) | | `matador.gd` | Matador AI (wander / ragdoll state machine) | | `matador_spawn.gd` | Spawns N matadors at random arena positions | | `debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`) | | `Assets/` | Raw 3D assets (`.glb`, `.fbx`) | | `Blender/` | Blender source files, animation scripts, FBX exports | | `Blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX | | `Blender/render_anim_preview.py` | Renders animation preview PNGs for visual verification | | `Blender/preview/` | Output directory for animation preview images | | `tests/` | Headless test scripts (screenshot capture, logic validation) | | `addons/rider-plugin/` | JetBrains Rider IDE integration | ## Tech choices - **Physics:** Jolt Physics (not the default Godot Physics) - **Renderer:** Forward Plus - **Input map:** WASD + arrow keys, Shift = charge, Space = jump, scroll = zoom, middle-click = toggle mouse capture - **IDE:** JetBrains Rider via the rider-plugin addon ## GDScript conventions - Typed GDScript everywhere (`var x: float`, return types on functions) - `@onready` for node references; never `get_node()` strings when avoidable - Constants in `SCREAMING_SNAKE_CASE`, variables in `snake_case` - One script per scene root — keep scripts focused - Signal names in `snake_case`; connect via `signal.connect()` not the legacy string form - Prefer `_physics_process` for physics/movement, `_process` for visuals/camera, `_unhandled_input` for input that shouldn't bubble - No comments that restate what the code already says; only comment non-obvious constraints or workarounds ## Best practices to enforce - Use `CharacterBody3D` for player-controlled characters, not `RigidBody3D` (unless the design specifically needs physics simulation) - Use `SpringArm3D` for third-person cameras to get free collision avoidance - Prefer `move_and_slide()` with `velocity` over manual collision queries - Export variables (`@export`) for any value a designer might tune; keep magic numbers out of logic - Scene composition over inheritance — build behaviour from small focused scenes - Use `autoload` (singletons) sparingly: only for truly global state (e.g. GameManager, AudioBus); not as a shortcut for passing data - Keep `_physics_process` deterministic and frame-rate independent (always multiply by `delta`) - Prefer signals over direct node references for decoupling ## Animation & bone conventions ### Blender rig (matador_v02) - **Armature bones:** `matador` (root) → `COG` → `chest` → `head`, `collarbone_L/R` → `arm_L/R` → `forearm_L/R` → `hand_L/R`, `leg_L/R` → `shin_L/R` → `foot_L/R` - **Rotation mode:** All animated bones use **XYZ Euler** (set in `create_matador_anims.py`) - **Bone-local axes:** Swing (forward/back) is on **local X**. Arm lowering from T-pose is on **local Y** (positive = left arm down, negative = right arm down). Knee bend is **positive X** (shins only bend backward). - **Walk cycle:** 30 frames at 30 fps = 1 second loop. Legs swing ±25° X, knees bend 0–30° X (only when leg is back), arms swing ±15° X opposite phase to legs, arms lowered 55° Y from T-pose, elbows constant 25° X bend. - **Idle pose:** Arms lowered 55° Y, elbows 25° X, everything else at rest. ### FBX export settings - `primary_bone_axis = 'Y'`, `secondary_bone_axis = 'X'` - `apply_scale_options = 'FBX_SCALE_ALL'` - `add_leaf_bones = False` - `bake_anim_use_all_actions = True`, `bake_anim_use_nla_strips = False` ### Godot animation names After FBX import, animations appear as `Armature|walk` and `Armature|idle` in AnimationPlayer. ### Verification workflow 1. Edit bones/animations in Blender (`create_matador_anims.py`) 2. Run render preview: `blender --background Blender/matador_v02.blend --python Blender/render_anim_preview.py` 3. Inspect PNGs in `Blender/preview/` — check bone orientations **before** exporting to Godot 4. Export FBX (done automatically by `create_matador_anims.py`) 5. Re-import in Godot and test in-game ## Running tests ```bash # All tests (lint + logic + gameplay assertions): bash run_tests.sh # Individual tests: gdlint *.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv) godot --headless --script tests/logic_test.gd # pure logic, ~2 s godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s # Gameplay test with real rendering (inspect frame strip visually): godot --script tests/gameplay_test.gd # opens a window, saves real screenshots # Output: tests/output/gameplay/motion_00..04.png, ragdoll_trigger.png, ragdoll_result.png # Difficulty simulation (balance tool, ~2 min): godot --headless --script tests/difficulty_sim.gd # drives a bot bull vs one matador ``` ### Difficulty simulation (`tests/difficulty_sim.gd`) A balance tool, not a pass/fail test. It drives a kinematic "bot bull" against one real matador across behaviour profiles (CHARGER / KITER / PASSIVE) and reports the matador's win rate (how often it gores the bull). Use it to tune matador difficulty from data. The bot bull has NO abilities (it only kills by ramming), so it can't use roll/slam/kick — real skilled play beats the matador more than these numbers imply; read the rates as "how well the matador threatens each movement pattern." A rough target is CHARGER high (blind charging is punished), KITER moderate (throws are a dodgeable threat), PASSIVE high (standing still dies). Note: as a `--script` entry it reads the `DP` autoload via `/root/DP`, not the global identifier. ### What the gameplay test catches | Check | What it detects | |---|---| | Tail Verlet chain spread | Tail bunching — consecutive nodes collapsed to same position | | Ragdoll bone positions finite + within 15 m | Matador limbs exploding after ragdoll impulse | | Ragdoll Y position > −10 | Bones falling through the floor | | Frame strip (visual, manual) | Leg IK quality, animation glitches, anything that looks wrong in motion | Headless screenshots will be blank (Forward Plus has no display). Run without `--headless` to get real frame strips. ### What the performance test catches Loads the full scene with `STRESS_MATADORS` (20) matadors, samples per-frame time during normal play and during a mass-ragdoll spike, and fails if the average exceeds 16 ms or any single frame exceeds 100 ms. Budgets are generous regression guards (not a target frame rate); the measured avg/peak/fps print every run so a gradual creep shows up before it trips the ceiling. ## Godot 4.x specifics - `wrapf` / `wrap` instead of manual modulo for angles - `lerp_angle` for smooth rotation interpolation (handles wrap-around correctly) - `move_toward` for speed ramps without overshooting - `PackedStringArray`, `PackedVector3Array`, etc. for performance-sensitive arrays - `@tool` scripts for editor helpers only — don't use in gameplay scripts - Resource (`extends Resource`) for shared data/config; no plain `Dictionary` for structured data - `StringName` (`&"action_name"`) for input action lookups in hot paths