Files
Bullosseum/CLAUDE.md
T
richard 5c1e881a53 Combat overhaul: charge/gore, bear ram, matador taunt reactions
Bull charge damage:
- Tunnel-proof swept detection (prev→now segment) for matador gore and bear
  ram, so a 66 m/s dash no longer skips clean over the thin HitArea.
- Horns-first cone (bull_gore_arc): running into an enemy only wounds it when
  charging roughly at it, not a sideways brush.
- Dash is a committed lunge: bypasses the horns cone AND the survival roll, so a
  dash connect is a guaranteed kill. Cruise charges keep a speed-scaled roll
  (mat_charge_pierce). Roll ability defers to its own pop (is_rolling guard).

Bear:
- Horns-first swept ram detection (edge-triggered: one charge = one wound);
  a tunnelling or point-blank charge now lands where body_entered wouldn't.
- Overhead smash lane extends faster (0.1s) and further (20 m).
- Leap slam launches the bull from anywhere in the circle (ring_frac 0).
- Removed stray-quote syntax errors that broke the script.

Matador:
- Collider-driven stab (BladeHitbox) with recovery beat + taunt-after-hit so it
  stops machine-gunning the bull.
- A taunting matador answers a charge: spear from range, sword up close.

Spear projectile masks the BULL layer so throws connect.

Tests: matador_charge/stab/spear/taunt_react, bear_ram/hitbox/fx (+ unified
_fake_bull stand-in). Trimmed verbose comments.

Note: bear claw-emitter/death work is still WIP (3 bear_boss_test failures).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-03 01:01:42 +03:00

14 KiB
Raw Permalink Blame History

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 The reusable shell loaded for every level: player, camera, HUD, PS1 filter, and an empty LevelRoot. Its game_shell.gd instances the active level's module into LevelRoot at runtime
levels/ The level system: per-level module scenes plus the code that defines, loads and sequences them (see rows below)
levels/game_shell.gd Shell controller — on load, frees any current level module and instances Run.active_level().level_scene into LevelRoot. Swapping the whole subtree (never hiding it) is what keeps one level's meshes/colliders/spawners from leaking into the next
levels/arena_level.tscn The arena level module: floor + wall colliders, arena meshes, MatadorSpawner (with child Node3D spawn points placed by the designer), lighting, audience (PublikNode). Instanced into the shell's LevelRoot
levels/bear_level.tscn The Bear's Den level module: bear-arena mesh, its own floor collider, lighting, and a BearSpawner (bear_spawn.gd) that places the bear at center. Instanced into the shell's LevelRoot
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)
levels/matador_spawn.gd Arena spawner — one matador per designer-placed child Node3D spawn point (each enters with a jump-roll toward center); falls back to a programmatic spread if no spawn points are present. Also handles the split-on-death hydra logic
levels/bear_spawn.gd Bear-level spawner — instantiates enemy_scene at the arena center; emits the same all_defeated / matador_killed signals as matador_spawn.gd
Bear.tscn / bear.gd Dark-Souls-style bear boss (CharacterBody3D). Routine state machine (wind-up → active → recovery) picking telegraphed moves by range band: Double Swipe / Overhead Smash (close), Leap Slam (mid-far), plus Stalk (2-leg walk-in) / Prowl (4-leg lope) positioning. Melee steps in during the swing (bear_lunge_speed) + tracks the bull; the leap is frame-synced to JUMP_SMASH so the paws hit ground on the impact frame (bear_leap_impact_frame); slams kick up dust + shake. Combos chain, hyper-armour mid-swing + poise (bear_stagger_cd), phase 2 enrage under bear_enrage_frac HP. Never damages the bull by colliding — only swings/slams. A horns-first bull charge wounds it (_sweep_bull_ram: swept prev→now so a tunnelling/point-blank charge still lands, gated by bear_ram_reach + bull_gore_arc, edge-triggered = one charge per wound). Joins &"matador" so bull abilities already hit it. Tuning under the Bear DP section
BearAttackHitboxes.tscn The bear's attack colliders as a self-contained sub-scene (instanced under bear_model): a LeapSlamHitbox (Hitbox + cylinder = ground circle) and SmashHitbox (Hitbox + box = forward shockwave lane), plus an AnimationPlayer whose leap_slam / smash_travel clips author each collider's size + travel — scrub to tweak, like Bear_FX. bear.gd only fires the clip, arms the knock-up shaping, and sweeps for overlaps while it runs (see _setup_attack_hitboxes / _start_attack_hitbox)
Sword.tscn / hitbox.gd The matador's sword wrapper (glb + editor-tweakable BladeHitbox). hitbox.gd (class_name Hitbox, Area3D) is the shared attack collider: activate()/deactivate() gate an active window, sweep() catches bodies already inside (AoE), and knock-up can ramp by distance (knock_up_min_dist/_max_dist) for the bear's outer-ring launch / further-hit-flies-higher
levels/run_state.gd Run progression autoload (Run) — Slay-the-Spire map + player position across fights
levels/level_def.gd LevelDef resource — a level's name / map colour / scene_path (always the shell) / level_scene (the level module instanced into the shell) / optional enemy_scene (arena wave vs. a boss like the bear)
levels/map_node.gd MapNode — one runtime node on the run map (position, level, links)
levels/MapScreen.tscn / levels/map_screen.gd Between-fights map screen; win → pick a node → next level
crowd_marker.gd CrowdMarker (@tool Node3D, class_name) — designer drops one per billboard spectator via the editor's Add-Node dialog; picks one of six 60°-spaced facings from the orientation dropdown. Draws an editor-only preview (slab + forward arrow, never serialised) and joins the &"crowd_marker" group
crowd_billboards.gd / crowd_billboard.gdshader MultiMeshInstance3D that harvests every CrowdMarker (group &"crowd_marker") into one draw call, using each marker's world position + chosen facing. Front/back sprite sheet baked by tools/bake_crowd_sheet.gd; falls back to a generated inward-facing ring when no markers are placed
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) → COGchesthead, collarbone_L/Rarm_L/Rforearm_L/Rhand_L/R, leg_L/Rshin_L/Rfoot_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 030° 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

# All tests (lint + logic + gameplay assertions):
bash run_tests.sh

# Individual tests:
gdlint *.gd levels/*.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
godot --headless --script tests/level_switch_test.gd  # no arena geometry leaks into the bear level, ~2 s
godot --headless --script tests/bear_boss_test.gd     # bear boss moveset: routines, leap landing, hyper-armour, ~10 s
godot --headless --script tests/bear_fx_test.gd       # bear attack-FX: each Bear_FX clip pops its mesh + self-hides, ~2 s
godot --headless --script tests/bear_hitbox_test.gd   # bear attack colliders: leap launches everywhere (ring_frac slider) + smash lane travel, ~3 s
godot --headless --script tests/bear_ram_test.gd      # a horns-first bull charge wounds the bear once; away/wide/slow spared, ~3 s
godot --headless --script tests/matador_stab_test.gd  # matador's BladeHitbox thrust gores a stationary bull, and a landed stab breaks it off to taunt (anti-spam), ~2 s
godot --headless --script tests/matador_taunt_react_test.gd # a taunting matador answers a charge: spear from range, sword up close, ignores non-charges, ~2 s
godot --headless --script tests/matador_charge_test.gd # bull's charge is swept (tunnel-proof) so a fast dash still gores; a committed charge pierces the survival roll, ~2 s
godot --headless --script tests/matador_spear_test.gd # thrown spear masks the BULL layer and hits a stationary bull, ~2 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