Reorganize the codebase into role-based folders (snake_case)

Move the 40-odd root-level scripts and scenes into a tree that groups by role,
per the approved roadmap (WS2/WS3): actors/{player,matador,bear}, systems/{combat,
fx,input,debug,render,crowd,config}, ui/, props/ (was tscn_s/). Rename the shell
scene.tscn -> levels/game_shell.tscn and every PascalCase scene to snake_case
(Player.tscn -> player.tscn, MainMenu.tscn -> main_menu.tscn, etc.) so the
case-insensitive web/macOS filesystems can't alias two files together.

Done as a scripted move: each file travels with its .uid; every quoted res:// path
across scenes, scripts, .tres, and project.godot's 9 autoloads is rewritten; the
.godot uid/class cache is rebuilt from scratch. Scenes still resolve by uid, so the
rewrite is belt-and-suspenders. Lint globs (run_tests.sh + both CI workflows) now
walk the new dirs; an orphan animation_tree.gd.uid and 7 pre-existing trailing-
whitespace lints (barrel/torch, never covered by the old root-only glob) are cleaned.

Full suite green on both sides. Asset consolidation (Assets/Animations/fonts ->
art/) is deferred — moving imported .glb/.png/.ttf is safer done in the editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
This commit is contained in:
2026-09-06 00:04:41 +03:00
parent b839252fe0
commit 7b6a141767
141 changed files with 199 additions and 158 deletions
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
uid://bvfa7xi0luysk
+28
View File
@@ -0,0 +1,28 @@
[gd_scene format=3 uid="uid://d0te4mwbh33ag"]
[ext_resource type="Script" uid="uid://bvfa7xi0luysk" path="res://actors/matador/matador.gd" id="1_matgd"]
[ext_resource type="PackedScene" uid="uid://eu2lqjhowj4t" path="res://Assets/Matador.glb" id="2_matfbx"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
radius = 0.25
height = 1.2
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hit"]
radius = 0.45
height = 1.4
[node name="Matador" type="CharacterBody3D" unique_id=1781857630]
script = ExtResource("1_matgd")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=2101651042]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
shape = SubResource("CapsuleShape3D_body")
[node name="HitArea" type="Area3D" parent="." unique_id=745037545]
monitorable = false
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitArea" unique_id=1497356275]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
shape = SubResource("CapsuleShape3D_hit")
[node name="matador2" parent="." unique_id=1487256381 instance=ExtResource("2_matfbx")]
+92
View File
@@ -0,0 +1,92 @@
class_name MatadorRagdoll
extends RefCounted
## Builds the matador's physical-bone ragdoll. Body joints are cone-twist with
## limited swing/twist (DP-tunable) so the corpse keeps its silhouette instead of
## folding into a flat "deflated" pile; the cape stays free (PIN) so it drapes.
const PhysicsLayers = preload("res://systems/combat/physics_layers.gd")
# [bone, radius] → sphere collider; [bone, radius, height] → capsule.
const _BONES: Array = [
["matador", 0.18],
["COG", 0.12],
["chest", 0.13, 0.30],
["head", 0.14],
["collarbone_L", 0.06, 0.18],
["collarbone_R", 0.06, 0.18],
["arm_L", 0.07, 0.22],
["arm_R", 0.07, 0.22],
["forearm_L", 0.06, 0.22],
["forearm_R", 0.06, 0.22],
["hand_L", 0.05, 0.12],
["hand_R", 0.05, 0.12],
["leg_L", 0.10, 0.32],
["leg_R", 0.10, 0.32],
["shin_L", 0.08, 0.28],
["shin_R", 0.08, 0.28],
["foot_L", 0.07, 0.18],
["foot_R", 0.07, 0.18],
["Bone", 0.04, 0.30],
["Bone.001", 0.04, 0.34],
["Bone.002", 0.04, 0.30],
]
# Reuse an existing simulator if the rig already has one, else build the bodies.
static func build(skeleton: Skeleton3D) -> PhysicalBoneSimulator3D:
for child: Node in skeleton.get_children():
if child is PhysicalBoneSimulator3D:
(child as PhysicalBoneSimulator3D).active = false
return child as PhysicalBoneSimulator3D
var sim := PhysicalBoneSimulator3D.new()
sim.active = false
skeleton.add_child(sim)
var swing := DP.f("mat_joint_swing")
var twist := DP.f("mat_joint_twist")
var damp := DP.f("mat_ragdoll_damp")
for entry: Array in _BONES:
var bname: String = entry[0]
if skeleton.find_bone(bname) == -1:
continue
var pb := PhysicalBone3D.new()
pb.bone_name = bname
pb.mass = 0.4
pb.linear_damp = 0.1
# The ragdoll only simulates once the matador is dead, so it lives on the CORPSE
# layer and masks to CORPSE alone: it rests on the ground/walls (which also carry
# CORPSE) but the bull — which collides only with WORLD — passes straight through
# the bodies instead of tripping over them.
pb.collision_layer = PhysicsLayers.CORPSE
pb.collision_mask = PhysicsLayers.CORPSE
if bname.begins_with("Bone"):
# Cape: floppy cloth — free swing, light, draggy, one-way collision (layer 0
# so nothing collides with it; it still drapes on the CORPSE-masked ground).
pb.joint_type = PhysicalBone3D.JOINT_TYPE_PIN
pb.mass = 0.15
pb.linear_damp = 0.5
pb.angular_damp = 0.7
pb.collision_layer = 0
else:
# Body: cone-twist joints cap how far each bone bends from its rest
# pose, so the silhouette holds; extra angular damp settles the flail.
pb.joint_type = PhysicalBone3D.JOINT_TYPE_CONE
pb.angular_damp = damp
pb.set(&"joint_constraints/swing_span", swing)
pb.set(&"joint_constraints/twist_span", twist)
var cs := CollisionShape3D.new()
if entry.size() >= 3:
var cap := CapsuleShape3D.new()
cap.radius = entry[1]
cap.height = entry[2]
cs.shape = cap
else:
var sph := SphereShape3D.new()
sph.radius = entry[1]
cs.shape = sph
pb.add_child(cs)
sim.add_child(pb)
return sim
+1
View File
@@ -0,0 +1 @@
uid://dmf04b5exnejn
+15
View File
@@ -0,0 +1,15 @@
[gd_scene format=3 uid="uid://c8spearwrap01"]
[ext_resource type="PackedScene" uid="uid://jepc74w8opbu" path="res://Assets/spear.glb" id="1_spearglb"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_shaft"]
radius = 0.13
height = 2.2
[node name="Spear" type="Node3D"]
[node name="spear" parent="." instance=ExtResource("1_spearglb")]
[node name="HitShape" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0.79)
shape = SubResource("CapsuleShape3D_shaft")
+253
View File
@@ -0,0 +1,253 @@
class_name SpearProjectile
extends RigidBody3D
# A thrown spear. It flies a ballistic arc (gravity does the work — the launch
# velocity is solved by the matador to land on the bull) and keeps its shaft aimed
# along its own velocity, so it noses over at the apex and drives in point-first.
# On impact it embeds where it struck: the tip buried just past the contact point
# with the shaft trailing back along the flight line, so it reads as stuck-in rather
# than floating alongside. A hit on the bull leaves the spear riding it like a spine
# (see shed_all, which the bull's slam calls to fling them all off); other hits pin
# it in place until the despawn timer clears it.
const PhysicsLayers = preload("res://systems/combat/physics_layers.gd")
const _DESPAWN_SEC: float = 8.0
const _STUCK_GROUP: StringName = &"stuck_spear"
# Shaft geometry in spear.glb local space: tip at +Z, butt behind the origin.
const _TIP_Z: float = 1.915
const _EMBED_DEPTH: float = 0.72 # how far past the surface the tip buries
# Bone the bull's spears ride on. The collision spheres sit outside the hide, so a
# spear stuck to the body root reads as floating; anchoring to COG also lets it bob
# and rotate with the animated body instead of only the character root.
const _COG_BONE: String = "COG"
const _ANCHOR_NAME: String = "SpearAnchor"
var _mesh: Node3D = null
var _stopped: bool = false
var _flight_dir: Vector3 = Vector3.ZERO # last pre-impact travel direction
# Build, seat and launch a spear from an already-instantiated mesh (the one that was
# in the matador's hand). `origin` is the release point; `velocity` is the full
# ballistic launch vector; `gravity` is the RigidBody gravity_scale for the arc.
static func launch(parent: Node, mesh: Node3D, origin: Vector3, velocity: Vector3,
gravity: float) -> SpearProjectile:
var s := SpearProjectile.new()
s.gravity_scale = gravity
s.collision_layer = 0
# Mask WORLD so a miss sticks in the arena, and BULL so it can actually hit the
# bull — the bull's body is on layer BULL, not WORLD, so a WORLD-only mask flew
# straight through it (every throw whiffed, even against a stationary bull).
s.collision_mask = PhysicsLayers.WORLD | PhysicsLayers.BULL
s.contact_monitor = true
s.max_contacts_reported = 6
parent.add_child(s)
# Seat the body so its local +Z (the shaft, see spear.glb) points along the throw.
s.global_transform = Transform3D(_aim_basis(velocity), origin)
# Take the editor-tweakable collider from the spear wrapper (Spear.tscn's HitShape)
# before reparenting, then drop the now-nested template.
var collider := _collider_from(mesh)
mesh.reparent(s, false)
mesh.transform = Transform3D.IDENTITY
s._mesh = mesh
var tmpl := mesh.get_node_or_null("HitShape")
if tmpl != null:
tmpl.queue_free()
s.add_child(collider)
s.linear_velocity = velocity
_despawn(s)
return s
# Shed every spear stuck in `bull` — pop each off as a tumbling body that flies clear
# and clears itself on the despawn timer. Called by the bull's slam.
static func shed_all(bull: Node) -> void:
if not (bull is Node3D):
return
var scene: Node = bull.get_tree().current_scene
if scene == null:
scene = bull
var center: Vector3 = (bull as Node3D).global_position
# Stuck spears now live under a BoneAttachment3D deep in the skeleton, so walk the
# whole subtree by group rather than only the bull's direct children.
for node in bull.get_tree().get_nodes_in_group(_STUCK_GROUP):
if not (bull as Node).is_ancestor_of(node):
continue
var mesh := node as Node3D
var gx := mesh.global_transform
var rb := RigidBody3D.new()
rb.collision_layer = 0
rb.collision_mask = 1
scene.add_child(rb)
rb.global_transform = gx
mesh.reparent(rb, false)
mesh.transform = Transform3D.IDENTITY
mesh.remove_from_group(_STUCK_GROUP)
rb.add_child(_shaft_collider())
# Fling outward from the bull with a bit of lift and spin.
var out := gx.origin - center
out.y = 0.0
out = out.normalized() if out.length() > 0.01 else Vector3(randf() - 0.5, 0.0, randf() - 0.5)
rb.linear_velocity = out * randf_range(3.0, 6.0) + Vector3.UP * randf_range(3.0, 5.0)
rb.angular_velocity = Vector3(randf_range(-10.0, 10.0),
randf_range(-10.0, 10.0), randf_range(-10.0, 10.0))
_despawn(rb)
# Clone the wrapper's editor-tweakable HitShape (Spear.tscn) into a fresh CollisionShape3D
# for the projectile body; falls back to the built-in shaft collider if it's missing.
static func _collider_from(source: Node) -> CollisionShape3D:
var tmpl := source.get_node_or_null("HitShape") as CollisionShape3D
if tmpl != null and tmpl.shape != null:
var cs := CollisionShape3D.new()
cs.shape = tmpl.shape
cs.transform = tmpl.transform
return cs
return _shaft_collider()
static func _shaft_collider() -> CollisionShape3D:
# Collider along local +Z (mesh runs z≈-0.34 … 1.92), fattened so a fast throw
# can't tunnel past the bull's collision spheres.
var cap := CapsuleShape3D.new()
cap.radius = 0.13
cap.height = 2.2
var cs := CollisionShape3D.new()
cs.shape = cap
cs.position = Vector3(0.0, 0.0, 0.79)
cs.rotation_degrees = Vector3(90.0, 0.0, 0.0)
return cs
# Continuous bleed from a spear lodged in the bull: a steady spurt of dark-red gobs out
# of the entry wound. Parented to the stuck mesh so it tracks the wound as the bull
# moves; world-space particles (local_coords off) so they arc off and fall behind as a
# trail. Dies with the mesh when the spear is shed or despawned.
static func _wound_emitter() -> CPUParticles3D:
var ramp := Gradient.new()
ramp.set_color(0, Color(0.5, 0.02, 0.02, 1.0))
ramp.set_color(1, Color(0.22, 0.0, 0.0, 0.0))
var sc := Curve.new()
sc.add_point(Vector2(0.0, 1.0))
sc.add_point(Vector2(0.6, 0.7))
sc.add_point(Vector2(1.0, 0.0))
var p := CPUParticles3D.new()
# Seat at the wound (where the shaft meets the hide) and spray back out along the
# shaft: mesh local -Z is the exit direction, rotated into world by the node basis.
p.position = Vector3(0.0, 0.0, _TIP_Z - _EMBED_DEPTH)
p.emitting = true
p.amount = 22
p.lifetime = 0.6
p.explosiveness = 0.0
p.randomness = 0.6
p.local_coords = false
p.direction = Vector3(0.0, 0.55, -1.0).normalized()
p.spread = 32.0
p.gravity = Vector3(0.0, -12.0, 0.0)
p.initial_velocity_min = 1.6
p.initial_velocity_max = 4.0
p.scale_amount_min = 0.5
p.scale_amount_max = 1.4
p.mesh = _blood_mesh()
p.color_ramp = ramp
p.scale_amount_curve = sc
return p
# Small unshaded, vertex-coloured sphere for blood gobs.
static func _blood_mesh() -> SphereMesh:
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.vertex_color_use_as_albedo = true
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
var s := SphereMesh.new()
s.radius = 0.045
s.height = 0.09
s.radial_segments = 4
s.rings = 2
s.material = mat
return s
static func _despawn(node: Node) -> void:
node.get_tree().create_timer(_DESPAWN_SEC).timeout.connect(func() -> void:
if is_instance_valid(node):
node.queue_free()
)
# Basis whose local +Z points along `v` (looking_at points -Z at its target).
static func _aim_basis(v: Vector3) -> Basis:
var dir := v.normalized()
if dir.length_squared() < 0.0001:
return Basis.IDENTITY
var up := Vector3.UP if absf(dir.dot(Vector3.UP)) < 0.99 else Vector3.FORWARD
return Basis.looking_at(-dir, up)
# While flying, keep the shaft aligned with the velocity; on the first contact freeze
# and hand off to _embed so the spear sticks where it struck instead of bouncing off.
func _integrate_forces(state: PhysicsDirectBodyState3D) -> void:
if _stopped:
return
if state.get_contact_count() > 0:
_stopped = true
# Use the remembered pre-impact heading — state.linear_velocity is already the
# bounced (post-collision) velocity by now, which would aim the shaft wrong.
var contact := state.get_contact_collider_position(0)
var other := state.get_contact_collider_object(0)
state.linear_velocity = Vector3.ZERO
state.angular_velocity = Vector3.ZERO
_embed.call_deferred(other, contact, _flight_dir)
return
var v := state.linear_velocity
if v.length_squared() > 0.04:
_flight_dir = v.normalized()
var t := state.transform
t.basis = _aim_basis(v)
state.transform = t
state.angular_velocity = Vector3.ZERO
# Compose the stuck pose (tip buried _EMBED_DEPTH past the contact, shaft along the
# flight line) and plant the spear. A bull hit hands the mesh to the bull so it rides
# along as a spine; anything else pins the body in place for the despawn timer.
func _embed(other: Object, contact: Vector3, flight: Vector3) -> void:
var f := flight.normalized()
if f.length_squared() < 0.0001:
f = global_transform.basis.z.normalized()
var pose := Transform3D(_aim_basis(f), contact + f * (_EMBED_DEPTH - _TIP_Z))
var bull := other as Node
if bull != null and bull.is_in_group(&"player"):
DamageEvent.deal(bull, DamageEvent.Cause.THROWN, contact, self)
if is_instance_valid(_mesh) and is_instance_valid(bull):
_mesh.reparent(_spear_anchor(bull), false)
_mesh.global_transform = pose
_mesh.add_to_group(_STUCK_GROUP)
if Settings.gore:
_mesh.add_child(_wound_emitter()) # keep bleeding from the wound while lodged
queue_free()
return
freeze = true
global_transform = pose
# Node embedded spears ride on the bull: a BoneAttachment3D pinned to the COG bone so
# they follow the animated body (bob, turn) rather than only the character root. One
# anchor is shared by every spear; falls back to the bull root if there's no skeleton.
static func _spear_anchor(bull: Node) -> Node3D:
var existing := bull.find_child(_ANCHOR_NAME, true, false)
if existing is Node3D:
return existing as Node3D
var skel := bull.find_child("Skeleton3D", true, false) as Skeleton3D
if skel == null:
return bull as Node3D
var ba := BoneAttachment3D.new()
ba.name = _ANCHOR_NAME
skel.add_child(ba)
ba.bone_name = _COG_BONE
return ba
+1
View File
@@ -0,0 +1 @@
uid://dwjakh1is1jtj
+20
View File
@@ -0,0 +1,20 @@
[gd_scene format=3 uid="uid://c8swordwrap01"]
[ext_resource type="PackedScene" uid="uid://xyvank512l5n" path="res://Assets/sword.glb" id="1_swordglb"]
[ext_resource type="Script" path="res://systems/combat/hitbox.gd" id="2_hitbox"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_blade"]
radius = 0.18
height = 1.3
[node name="Sword" type="Node3D"]
[node name="sword" parent="." instance=ExtResource("1_swordglb")]
[node name="BladeHitbox" type="Area3D" parent="."]
script = ExtResource("2_hitbox")
damage_cause = "gored"
[node name="CollisionShape3D" type="CollisionShape3D" parent="BladeHitbox"]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0.8)
shape = SubResource("CapsuleShape3D_blade")