Update CLAUDE.md paths + folder-layout overview for the reorg
Repoint the project-structure table and lint command at the new role-based tree, and add a folder-layout summary up top. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
This commit is contained in:
@@ -10,34 +10,54 @@ I am a **Godot 4.6+ expert**. I follow current best practices for GDScript, scen
|
|||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
|
Files are grouped by **role**, not by type. Scripts and scenes are `snake_case`;
|
||||||
|
`PascalCase` is reserved for `class_name` identifiers (this avoids case-insensitive
|
||||||
|
filesystem aliasing on the web/macOS targets).
|
||||||
|
|
||||||
|
```
|
||||||
|
actors/{player,matador,bear}/ the three characters (script + scene together)
|
||||||
|
systems/combat/ hitbox.gd, damage_event.gd, physics_layers.gd
|
||||||
|
systems/fx/ Fx autoload + effect scenes (Siim's authoring dir)
|
||||||
|
systems/input/ controls_manager.gd, touch_controls.gd
|
||||||
|
systems/debug/ debug_params.gd (DP), debug_draw.gd, console.gd
|
||||||
|
systems/render/ rigid_skin.gd, ps1_filter (post-process)
|
||||||
|
systems/crowd/ billboard audience
|
||||||
|
systems/config/ settings.gd (Settings autoload)
|
||||||
|
ui/ hud, mobile_hud, main_menu, ui_theme/fonts, overlays
|
||||||
|
props/ barrels, torch
|
||||||
|
levels/ game_shell.tscn + level modules
|
||||||
|
tools/ tests/ editor/bake scripts, headless tests
|
||||||
|
Assets/ Animations/ fonts/ HUD/ raw art (not yet consolidated under art/)
|
||||||
|
```
|
||||||
|
|
||||||
| Path | Purpose |
|
| 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/game_shell.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/` | 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/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/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` |
|
| `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`) |
|
| `actors/player/player.tscn` | Player scene root (`CharacterBody3D`) |
|
||||||
| `player.gd` | Movement, charge, turning logic |
|
| `actors/player/player.gd` | Movement, charge, turning logic |
|
||||||
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
| `camera_spring_arm.gd` | Mouse-look pivot (`Node3D` + `SpringArm3D`) |
|
||||||
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
| `camera_follow.gd` | Smooth camera follow (`Camera3D`) |
|
||||||
| `matador.gd` | Matador AI (wander / ragdoll state machine) |
|
| `actors/matador/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/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` |
|
| `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 |
|
| `actors/bear/bear.tscn` / `actors/bear/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`) |
|
| `actors/bear/bear_attack_hitboxes.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`. `actors/bear/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 |
|
| `actors/matador/sword.tscn` / `systems/combat/hitbox.gd` | The matador's sword wrapper (glb + editor-tweakable `BladeHitbox`). `systems/combat/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/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/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/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 |
|
| `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 |
|
| `systems/crowd/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 |
|
| `systems/crowd/crowd_billboards.gd` / `systems/crowd/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`). Mobile-relevant flags: `force_touch_controls`, `touch_debug` (also enabled on-device with `?debug=1` in the page URL — shows the live profiler overlay in `touch_controls.gd`), and `use_rigid_skin` (default **off** — native GPU skinning everywhere now that the 4.7 web templates fixed the Mali/ANGLE invisible-skinning bug; `rigid_skin.gd` is a retained opt-in fallback). Rigid-skin is gated through `Controls.rigid_skin_enabled()`, which a page URL param overrides for on-device A/B with no console: `?skin=1` forces the workaround on (for a device where native skinning still fails), `?noskin=1` forces native off. Turning the workaround off also removes its per-spawn SurfaceTool rebuild, which was the arena's matador-spawn lag |
|
| `systems/debug/debug_params.gd` | Runtime-tunable parameter registry (autoload `DP`). Mobile-relevant flags: `force_touch_controls`, `touch_debug` (also enabled on-device with `?debug=1` in the page URL — shows the live profiler overlay in `systems/input/touch_controls.gd`), and `use_rigid_skin` (default **off** — native GPU skinning everywhere now that the 4.7 web templates fixed the Mali/ANGLE invisible-skinning bug; `systems/render/rigid_skin.gd` is a retained opt-in fallback). Rigid-skin is gated through `Controls.rigid_skin_enabled()`, which a page URL param overrides for on-device A/B with no console: `?skin=1` forces the workaround on (for a device where native skinning still fails), `?noskin=1` forces native off. Turning the workaround off also removes its per-spawn SurfaceTool rebuild, which was the arena's matador-spawn lag |
|
||||||
| `touch_controls.gd` | On-screen thumb UI for touch/web (`CanvasLayer`, instanced by `hud.gd`, self-gating on `Controls.use_touch_ui()`): a floating left-side joystick feeding the `move_*` actions + a right-side ability cluster, drawn/hit-tested in one IGNORE-filtered Control from raw `_input()`. The joystick self-heals — the latest left-zone touch re-acquires it (`_stick_index`), and a relayout / focus-out / vanished touch resets state so a dropped touchend can't strand a stuck stick. Ability buttons use grown hit-rects + a press flash. `_draw_debug()` (gated on `Controls.debug_overlay()` — DP `touch_debug` or `?debug=1`) is a live on-device profiler reading Godot's `Performance` monitors: FPS/frame-ms with a bottleneck verdict, `process`(main-thread GDScript) vs `physics`(ragdoll/Jolt) ms with 2 s peak-hold, draw calls/objects/primitives, node count, render resolution, and `crossOriginIsolated` — so a lag spike is pinned to CPU-script vs CPU-physics vs GPU-fill instead of guessed |
|
| `systems/input/touch_controls.gd` | On-screen thumb UI for touch/web (`CanvasLayer`, instanced by `ui/hud.gd`, self-gating on `Controls.use_touch_ui()`): a floating left-side joystick feeding the `move_*` actions + a right-side ability cluster, drawn/hit-tested in one IGNORE-filtered Control from raw `_input()`. The joystick self-heals — the latest left-zone touch re-acquires it (`_stick_index`), and a relayout / focus-out / vanished touch resets state so a dropped touchend can't strand a stuck stick. Ability buttons use grown hit-rects + a press flash. `_draw_debug()` (gated on `Controls.debug_overlay()` — DP `touch_debug` or `?debug=1`) is a live on-device profiler reading Godot's `Performance` monitors: FPS/frame-ms with a bottleneck verdict, `process`(main-thread GDScript) vs `physics`(ragdoll/Jolt) ms with 2 s peak-hold, draw calls/objects/primitives, node count, render resolution, and `crossOriginIsolated` — so a lag spike is pinned to CPU-script vs CPU-physics vs GPU-fill instead of guessed |
|
||||||
| `web_start_gate.gd` | Autoload `WebStartGate` — "TAP TO PLAY" panel shown on web+touch before play; the tap drives `Controls.request_fullscreen_landscape()` so the fullscreen/orientation churn happens with no in-flight touch to lose. Re-armable: polls browser fullscreen state and returns if the player leaves it (iPhone Safari can't fullscreen, so it stops nagging once a request never takes) |
|
| `ui/web_start_gate.gd` | Autoload `WebStartGate` — "TAP TO PLAY" panel shown on web+touch before play; the tap drives `Controls.request_fullscreen_landscape()` so the fullscreen/orientation churn happens with no in-flight touch to lose. Re-armable: polls browser fullscreen state and returns if the player leaves it (iPhone Safari can't fullscreen, so it stops nagging once a request never takes) |
|
||||||
| `mobile_hud.gd` | Mobile-only health readouts (`CanvasLayer`, instanced by `hud.gd` only on touch): big bull pip row top-left + a Souls-style bear boss bar top-centre (discovered via group `&"bear"`, tracks `health_changed`). The desktop bottom pip cluster is hidden on touch |
|
| `ui/mobile_hud.gd` | Mobile-only health readouts (`CanvasLayer`, instanced by `ui/hud.gd` only on touch): big bull pip row top-left + a Souls-style bear boss bar top-centre (discovered via group `&"bear"`, tracks `health_changed`). The desktop bottom pip cluster is hidden on touch |
|
||||||
| `orientation_guard.gd` | Autoload `OrientationGuard` — full-screen "rotate your device" prompt shown on touch devices while portrait |
|
| `ui/orientation_guard.gd` | Autoload `OrientationGuard` — full-screen "rotate your device" prompt shown on touch devices while portrait |
|
||||||
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
| `Assets/` | Raw 3D assets (`.glb`, `.fbx`) |
|
||||||
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
| `Blender/` | Blender source files, animation scripts, FBX exports |
|
||||||
| `Blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX |
|
| `Blender/create_matador_anims.py` | Creates walk + idle animations and exports FBX |
|
||||||
@@ -106,7 +126,7 @@ After FBX import, animations appear as `Armature|walk` and `Armature|idle` in An
|
|||||||
bash run_tests.sh
|
bash run_tests.sh
|
||||||
|
|
||||||
# Individual tests:
|
# Individual tests:
|
||||||
gdlint *.gd levels/*.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
gdlint actors systems ui props levels tests # GDScript lint (gdtoolkit, installed via uv)
|
||||||
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
||||||
godot --headless --script tests/harness_selftest.gd # shared test_helpers: seeded RNG + approx + tally, ~1 s
|
godot --headless --script tests/harness_selftest.gd # shared test_helpers: seeded RNG + approx + tally, ~1 s
|
||||||
godot --headless --script tests/damage_pipeline_test.gd # bull's hit reception (take_sword_hit + typed receive_damage/DamageEvent), ~3 s
|
godot --headless --script tests/damage_pipeline_test.gd # bull's hit reception (take_sword_hit + typed receive_damage/DamageEvent), ~3 s
|
||||||
|
|||||||
Reference in New Issue
Block a user