Reorganize into role-based folders and consolidate art

Move every script and scene into role-based directories (actors/, systems/, ui/,
levels/, props/) and all raw art under art/ (grouped by role, snake_case,
Extract-Textures GLBs), dropping Blender version-suffix noise. Filenames are
snake_case (PascalCase reserved for class_name) to avoid case-insensitive
filesystem aliasing on the web/macOS targets. Routes the bull's hits through the
typed DamageEvent pipeline and wires the bull's abilities to the Fx library.
Updates CLAUDE.md's folder-layout overview to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
This commit is contained in:
2026-09-08 14:56:22 +03:00
parent c0a022fed4
commit 09d4a60f43
244 changed files with 415 additions and 19337 deletions
+2 -2
View File
@@ -67,11 +67,11 @@ func _make_floor() -> StaticBody3D:
func _run() -> void:
root.add_child(_make_floor())
_bull = (load("res://Player.tscn") as PackedScene).instantiate()
_bull = (load("res://actors/player/player.tscn") as PackedScene).instantiate()
root.add_child(_bull)
_bull.global_position = Vector3(4, 1, 0)
_bear = (load("res://Bear.tscn") as PackedScene).instantiate()
_bear = (load("res://actors/bear/bear.tscn") as PackedScene).instantiate()
root.add_child(_bear)
_bear.global_position = Vector3(0, 1, 0)
await physics_frame
+2 -2
View File
@@ -1,6 +1,6 @@
extends SceneTree
## Bear attack-FX test. The four attack effects (crater, smash debris, left/right claw slash)
## live in Assets/fx/Bear_FX.glb as meshes that rest at scale 0 at Siim's baked positions. The
## live in art/fx/bear_fx.glb as meshes that rest at scale 0 at Siim's baked positions. The
## GLB carries its own AnimationPlayer whose clips scale one mesh up at its strike frame and
## back to 0 (self-hiding) while pinning the others at 0. bear.gd fires the matching clip on
## each strike (see _play_fx). This asserts each clip pops its own mesh, self-hides by the end,
@@ -27,7 +27,7 @@ func _ok(c: bool, m: String) -> void:
func _run() -> void:
var bear := (load("res://Bear.tscn") as PackedScene).instantiate()
var bear := (load("res://actors/bear/bear.tscn") as PackedScene).instantiate()
root.add_child(bear)
await physics_frame
+1 -1
View File
@@ -52,7 +52,7 @@ func _drive(bear: Node, fire: String) -> void:
func _run() -> void:
var bear := (load("res://Bear.tscn") as PackedScene).instantiate() as Node3D
var bear := (load("res://actors/bear/bear.tscn") as PackedScene).instantiate() as Node3D
root.add_child(bear)
bear.set_physics_process(false) # freeze the AI; we fire the colliders ourselves
await physics_frame
+1 -1
View File
@@ -56,7 +56,7 @@ func _ram_pass(bear: Node, bull: CharacterBody3D, speed: float, from: Vector3, t
func _run() -> void:
var bull := _make_bull()
var bear := (load("res://Bear.tscn") as PackedScene).instantiate() as Node3D
var bear := (load("res://actors/bear/bear.tscn") as PackedScene).instantiate() as Node3D
root.add_child(bear)
bear.global_position = Vector3.ZERO
bear.set_physics_process(false) # freeze the AI; we drive the ram sweep ourselves
+2 -2
View File
@@ -49,9 +49,9 @@ func _init() -> void:
func _run() -> void:
_dt = 1.0 / float(Engine.physics_ticks_per_second)
_dp = root.get_node_or_null("/root/DP")
_mat_scene = load("res://Matador.tscn") as PackedScene
_mat_scene = load("res://actors/matador/matador.tscn") as PackedScene
_scene = (load("res://scene.tscn") as PackedScene).instantiate()
_scene = (load("res://levels/game_shell.tscn") as PackedScene).instantiate()
root.add_child(_scene)
current_scene = _scene
await create_timer(0.5).timeout
+2 -2
View File
@@ -29,7 +29,7 @@ var _matador_scene: PackedScene = null
# preload of a matador scene compiles matador.gd before the DP autoload binds.
func _mat_scene() -> PackedScene:
if _matador_scene == null:
_matador_scene = load("res://Matador.tscn")
_matador_scene = load("res://actors/matador/matador.tscn")
return _matador_scene
@@ -40,7 +40,7 @@ func _init() -> void:
func _run() -> void:
DirAccess.make_dir_recursive_absolute(OUTPUT_DIR)
var scene_res := load("res://scene.tscn")
var scene_res := load("res://levels/game_shell.tscn")
if not scene_res:
push_error("gameplay_test: failed to load scene.tscn")
quit(1)
+1 -1
View File
@@ -44,7 +44,7 @@ func _run() -> void:
# Force the shell to build the Bear's Den regardless of the generated map.
run.debug_level_override = run.get_level(&"bear")
var scene: Node = load("res://scene.tscn").instantiate()
var scene: Node = load("res://levels/game_shell.tscn").instantiate()
root.add_child(scene)
current_scene = scene
# Let the shell instance the level and the spawner's deferred spawn run.
+3 -58
View File
@@ -30,7 +30,6 @@ func _run() -> void:
test_kick_spread()
test_gait_phase_crossing()
test_tail_verlet_constraint()
test_run_map()
print("=" .repeat(60))
print("Results: %d passed, %d failed" % [_passed, _failed])
@@ -172,60 +171,6 @@ func test_tail_verlet_constraint() -> void:
_assert_in_range(damping_factor, 0.85, 1.0, "tail damping factor is reasonable")
# ── Run map (Slay-the-Spire progression) ──────────────────────────────────────
func test_run_map() -> void:
print("\n-- test_run_map --")
var run: Node = root.get_node_or_null("/root/Run")
if run == null:
_assert_true(false, "Run autoload should exist")
return
run.start_new_run()
_assert_true(run.has_run(), "start_new_run builds a map")
_assert_true(run.map.size() >= 2, "map has multiple rows")
# Final row is a single boss node the map funnels toward.
var last: Array = run.map[run.map.size() - 1]
_assert_eq(last.size(), 1, "final row is a single boss node")
_assert_true(last[0].level.is_boss, "final node is a boss level")
# Connectivity: every node links forward, and every next-row node has a parent.
for r: int in run.map.size() - 1:
var nxt: Array = run.map[r + 1]
var reached := {}
for from_node in run.map[r]:
_assert_true(from_node.links.size() >= 1, "row %d node links forward" % r)
for c: int in from_node.links:
_assert_true(c >= 0 and c < nxt.size(), "link column in range")
reached[c] = true
for c: int in nxt.size():
_assert_true(reached.has(c), "row %d col %d has a parent" % [r + 1, c])
# From the start gate the whole first row is the frontier.
_assert_eq(run.reachable().size(), run.map[0].size(), "start frontier is row 0")
# Advancing follows the chosen node's links.
var first = run.map[0][0]
run.select_node(first)
run.complete_current_level()
_assert_eq(run.player_row, 0, "player advanced to row 0")
_assert_true(first.cleared, "cleared node is flagged")
_assert_eq(run.reachable().size(), first.links.size(),
"frontier follows the chosen node's links")
# Walking to a boss node latches run completion.
var guard := 0
while not run.run_complete() and guard < 64:
var opts: Array = run.reachable()
if opts.is_empty():
break
run.select_node(opts[0])
run.complete_current_level()
guard += 1
_assert_true(run.run_complete(), "reaching a boss node completes the run")
# ── Shared helpers ────────────────────────────────────────────────────────────
func _assert_in_range(val: float, lo: float, hi: float, desc: String) -> void:
@@ -245,8 +190,8 @@ func test_debug_params_defaults() -> void:
"mat_walk_speed default in valid range")
_assert_in_range(dp.f("mat_wander_radius"), 5.0, 50.0,
"mat_wander_radius default in valid range")
_assert_in_range(dp.f("mat_spawn_count"), 1.0, 30.0,
"mat_spawn_count default in valid range")
_assert_in_range(dp.f("mat_wave_size"), 1.0, 50.0,
"mat_wave_size default in valid range")
_assert_in_range(dp.f("mat_hit_threshold"), 1.0, 30.0,
"mat_hit_threshold default in valid range")
_assert_in_range(dp.f("mat_ragdoll_impulse"), 1.0, 50.0,
@@ -377,7 +322,7 @@ func test_matador_state_transitions() -> void:
print("\n-- test_matador_state_transitions --")
# Verify that the State enum values match expected constants
# Load the matador scene to check its script
var matador_scene := load("res://Matador.tscn")
var matador_scene := load("res://actors/matador/matador.tscn")
if matador_scene == null:
_assert_true(false, "Matador.tscn should load")
return
+1 -1
View File
@@ -43,7 +43,7 @@ func _make_bull() -> CharacterBody3D:
func _make_matador(_bull: CharacterBody3D) -> CharacterBody3D:
var mat := (load("res://Matador.tscn") as PackedScene).instantiate() as CharacterBody3D
var mat := (load("res://actors/matador/matador.tscn") as PackedScene).instantiate() as CharacterBody3D
root.add_child(mat)
mat.global_position = Vector3.ZERO
mat.set_physics_process(false) # we drive the swept gore ourselves, isolated from the AI
+3 -3
View File
@@ -7,7 +7,7 @@ extends SceneTree
# physics_layers.gd is pure constants (safe to preload); spear_projectile.gd references the
# Settings autoload, whose global identifier isn't registered when a --script entry compiles its
# preloads — so it's loaded at runtime inside _run instead, once the autoloads exist.
const PhysicsLayers = preload("res://physics_layers.gd")
const PhysicsLayers = preload("res://systems/combat/physics_layers.gd")
var _pass := 0
var _fail := 0
@@ -44,8 +44,8 @@ func _run() -> void:
await physics_frame
# Launch a spear flat, dead-on from 3 m out; low gravity so the short flight stays level.
var spear_script := load("res://spear_projectile.gd")
var mesh := (load("res://Spear.tscn") as PackedScene).instantiate() as Node3D
var spear_script := load("res://actors/matador/spear_projectile.gd")
var mesh := (load("res://actors/matador/spear.tscn") as PackedScene).instantiate() as Node3D
root.add_child(mesh)
var origin := Vector3(0.0, 0.0, -3.0)
var spear := spear_script.launch(root, mesh, origin, Vector3(0.0, 0.0, 12.0), 0.05) as RigidBody3D
+1 -1
View File
@@ -45,7 +45,7 @@ func _run() -> void:
root.add_child(bull)
bull.global_position = Vector3(0.0, 0.0, standoff + 3.0)
var mat := (load("res://Matador.tscn") as PackedScene).instantiate() as CharacterBody3D
var mat := (load("res://actors/matador/matador.tscn") as PackedScene).instantiate() as CharacterBody3D
root.add_child(mat) # _ready acquires the bull from the "player" group
mat.global_position = Vector3.ZERO
mat.set_physics_process(false) # we drive the attack tick ourselves, isolated from the AI
+1 -1
View File
@@ -38,7 +38,7 @@ func _make_bull() -> CharacterBody3D:
func _make_taunting_matador(_bull: CharacterBody3D) -> CharacterBody3D:
var mat := (load("res://Matador.tscn") as PackedScene).instantiate() as CharacterBody3D
var mat := (load("res://actors/matador/matador.tscn") as PackedScene).instantiate() as CharacterBody3D
root.add_child(mat)
mat.global_position = Vector3.ZERO
mat.set_physics_process(false) # we drive _tick_wander ourselves, no brain
+1 -1
View File
@@ -45,7 +45,7 @@ func _run() -> void:
run.ensure_run()
run.debug_level_override = run.get_level(&"arena")
var scene: Node = load("res://scene.tscn").instantiate()
var scene: Node = load("res://levels/game_shell.tscn").instantiate()
root.add_child(scene)
current_scene = scene
+1 -1
View File
@@ -45,7 +45,7 @@ func _run() -> void:
run.ensure_run() # build the run first — the shell's own ensure_run would wipe the override
run.debug_level_override = run.get_level(&"arena")
var scene_res := load("res://scene.tscn")
var scene_res := load("res://levels/game_shell.tscn")
if not scene_res:
push_error("performance_test: failed to load scene.tscn")
quit(1)
+1 -1
View File
@@ -65,7 +65,7 @@ func _spawn_one() -> Node3D:
func _run() -> void:
# Warm up: first instance pays one-time import/JIT/shader costs that would skew sample 1.
_matador = load("res://Matador.tscn") as PackedScene
_matador = load("res://actors/matador/matador.tscn") as PackedScene
var warm := _spawn_one()
await process_frame
warm.call(&"apply_ability_hit", Vector3(1.0, 0.0, 0.0), 12.0)
+1 -1
View File
@@ -21,7 +21,7 @@ func _init() -> void:
func _run() -> void:
DirAccess.make_dir_recursive_absolute(OUTPUT_DIR)
var scene_res := load("res://scene.tscn")
var scene_res := load("res://levels/game_shell.tscn")
if not scene_res:
push_error("ragdoll_throw_test: failed to load scene.tscn")
quit(1)
+2 -2
View File
@@ -7,7 +7,7 @@ extends SceneTree
## hides the original skinned meshes (so they never hit the invisible-on-Mali skinning path).
## Run: godot --headless --script res://tests/rigid_skin_test.gd
const RigidSkin = preload("res://rigid_skin.gd")
const RigidSkin = preload("res://systems/render/rigid_skin.gd")
var _pass := 0
var _fail := 0
@@ -43,7 +43,7 @@ func _surface_mats(mi: MeshInstance3D) -> Array:
func _run() -> void:
var inst: Node = (load("res://Assets/Matador.glb") as PackedScene).instantiate()
var inst: Node = (load("res://art/characters/matador.glb") as PackedScene).instantiate()
root.add_child(inst)
# Distinct materials on the skinned source meshes, before conversion.
+1 -1
View File
@@ -18,7 +18,7 @@ func _run() -> void:
DirAccess.make_dir_recursive_absolute(OUTPUT_DIR)
# Load and switch to the main scene
var scene_res := load("res://scene.tscn")
var scene_res := load("res://levels/game_shell.tscn")
if scene_res == null:
push_error("screenshot_test: Failed to load scene.tscn")
quit(1)
+1 -1
View File
@@ -73,7 +73,7 @@ func _run() -> void:
# Headless boots a 64x64 root viewport, which collapses every UI rect on top of each other.
# Give it a real phone-ish landscape size so the joystick zone / buttons lay out sanely.
root.size = Vector2i(1152, 648)
_tc = (load("res://touch_controls.gd") as Script).new()
_tc = (load("res://systems/input/touch_controls.gd") as Script).new()
root.add_child(_tc)
await process_frame # _ready builds _surface and runs the first _relayout
_tc._relayout() # ensure the layout reflects the size set above
+1 -1
View File
@@ -5,7 +5,7 @@ extends Node3D
## skinned mesh draws under a given engine build.
func _ready() -> void:
var bull := (load("res://Assets/bull.fbx") as PackedScene).instantiate() as Node3D
var bull := (load("res://art/characters/bull.fbx") as PackedScene).instantiate() as Node3D
bull.scale = Vector3(100.0, 100.0, 100.0)
add_child(bull)