# 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 ``` ### 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