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
+59
View File
@@ -0,0 +1,59 @@
# FX — the effect library
Gameplay code fires a **named** effect; the **scene** decides how it looks. That split is the whole
point: a programmer writes `Fx.spawn(&"blood_burst", hit_transform)` once, and from then on every
change to how a hit *feels* is a `.tscn` Siim edits in the inspector — no code, no recompile.
```gdscript
# anywhere in gameplay code
Fx.spawn(&"blood_burst", global_transform) # at a node
Fx.spawn(&"slam_dust", Transform3D(Basis(), point)) # at a world point
```
## Siim's workflow
1. **Open the gallery.** Run `fx/effect_gallery.tscn`. It loops through every effect on a sand-lit
stage with the effect's name on screen, so you see each one the way it reads in play.
2. **Pick an effect** `.tscn` in `fx/` (`blood_burst`, `slam_dust`, `spark`, `hit_flash`) and open it.
3. **Tune it in the inspector.** Everything lives on the `CPUParticles3D` node and its material:
- **Colour** → the node's *Color Ramp* (start colour → faded/transparent end).
- **Shape of the burst over time** → *Scale Amount Curve*.
- **Amount / Lifetime / Explosiveness / Spread / Velocity / Gravity** → the node's emission group.
- The viewport previews particles live as you drag — no play needed.
4. **Save.** That's the whole edit. Re-run the gallery to compare.
New effect? Add a `fx/<name>.tscn` (root = `CPUParticles3D` with `one_shot_effect.gd` attached — copy
an existing one), then register its name → path in `fx/fx.gd`.
## House style (keep effects reading as one game)
**Palette** — pull from these, don't invent per-effect colours:
| Role | Start | End (fades to alpha 0) |
|-------|---------------------------|-----------------------------|
| Blood | `#FF0505` | `#400000` |
| Dust | `#B89966` (tan) | `#8C7326` |
| Ember | `#FFF299` → `#FF8C1A` | transparent |
| Flash | `#FFFFFF` | transparent |
**Timing budgets** — hits read best short: strike/flash effects ≤ **0.4 s**, dust/settle ≤ **1.1 s**.
A one-shot effect frees itself `lifetime + 0.5 s` after spawn (`one_shot_effect.gd`), so keep
`lifetime` honest.
**PS1 look** — materials are **unshaded**, **alpha-blended**, **vertex-colour** (colour comes from the
ramp), cull disabled. Prefer few, chunky, low-poly billboarded quads over dense fine particles; the
game runs the PS1 filter over everything, so subtlety is lost anyway.
**Mobile** — effects are **`CPUParticles3D`, never `GPUParticles3D`**. The web/GLES3/ANGLE target
compiles particle shaders badly (see the notes in `player.gd`); CPU particles are just as tunable in
the inspector and safe on-device. Keep this constraint when adding effects.
## Files
| File | What it is |
|---|---|
| `fx.gd` | The `Fx` autoload — `spawn(name, xform, parent=null)`; name→path registry |
| `one_shot_effect.gd` | Root helper: fires the emitter(s) once, frees the scene when done |
| `blood_burst.tscn` etc. | The effects themselves — **edit these** |
| `effect_gallery.tscn` | Preview stage (run it to review) |
| `_seed_placeholders.gd` | One-off generator that wrote the first-draft defaults; not used at runtime |
+49
View File
@@ -0,0 +1,49 @@
extends CPUParticles3D
## One-shot blood spray. Add as a child of whatever bleeds, then call burst().
func _ready() -> void:
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 sphere := SphereMesh.new()
sphere.radius = 0.07
sphere.height = 0.14
sphere.radial_segments = 4
sphere.rings = 2
sphere.material = mat
var ramp := Gradient.new()
ramp.set_color(0, Color(1.0, 0.02, 0.02, 1.0))
ramp.set_color(1, Color(0.25, 0.0, 0.0, 0.0))
var scale_curve := Curve.new()
scale_curve.add_point(Vector2(0.0, 1.0))
scale_curve.add_point(Vector2(0.6, 0.5))
scale_curve.add_point(Vector2(1.0, 0.0))
mesh = sphere
color_ramp = ramp
scale_amount_curve = scale_curve
one_shot = true
explosiveness = 0.9
amount = 25
lifetime = 1.1
randomness = 0.4
local_coords = false
spread = 80.0
gravity = Vector3(0.0, -7.0, 0.0)
initial_velocity_min = 4.0
initial_velocity_max = 11.0
scale_amount_min = 0.5
scale_amount_max = 1.6
emitting = false
# Fire a burst at a world position, sprayed along dir (biased upward).
func burst(at: Vector3, dir: Vector3) -> void:
global_position = at
direction = (dir * 0.6 + Vector3.UP * 0.4).normalized()
restart()
+1
View File
@@ -0,0 +1 @@
uid://cnhmhom4w5o3i
+42
View File
@@ -0,0 +1,42 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="1_l68fm"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_okatq"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="SphereMesh_n7uw4"]
material = SubResource("StandardMaterial3D_okatq")
radius = 0.07
height = 0.14
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="Curve_a71nq"]
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.6, 0.5), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="Gradient_mhqc0"]
colors = PackedColorArray(1, 0.02, 0.02, 1, 0.25, 0, 0, 0)
[node name="blood_burst" type="CPUParticles3D" unique_id=1553421910]
emitting = false
amount = 25
lifetime = 1.1
one_shot = true
explosiveness = 0.9
randomness = 0.4
mesh = SubResource("SphereMesh_n7uw4")
direction = Vector3(0, 1, 0)
spread = 80.0
gravity = Vector3(0, -7, 0)
initial_velocity_min = 4.0
initial_velocity_max = 11.0
scale_amount_min = 0.5
scale_amount_max = 1.6
scale_amount_curve = SubResource("Curve_a71nq")
color_ramp = SubResource("Gradient_mhqc0")
script = ExtResource("1_l68fm")
+30
View File
@@ -0,0 +1,30 @@
extends Node3D
## Live preview stage for the FX library. Cycles through every registered effect on a loop against
## arena-ish lighting so Siim sees each one as it reads in-game. Open fx/effect_gallery.tscn and
## press Play, then edit any effect .tscn in the inspector and re-run to compare. See fx/README.md.
const _CYCLE_SEC := 1.6
const _EFFECTS: Array[StringName] = [&"blood_burst", &"slam_dust", &"spark", &"hit_flash"]
@onready var _label: Label = $UI/Label
# Reached by path rather than the Fx global so the placeholder-seeder can compile and attach this
# script headless (autoload globals aren't resolvable during an on-demand compile in a --script run).
@onready var _fx: Node = get_node("/root/Fx")
var _index := 0
func _ready() -> void:
var timer := Timer.new()
timer.wait_time = _CYCLE_SEC
timer.autostart = true
add_child(timer)
timer.timeout.connect(_fire_next)
_fire_next()
func _fire_next() -> void:
var effect := _EFFECTS[_index % _EFFECTS.size()]
_index += 1
_label.text = "%s (%d/%d)" % [effect, (_index - 1) % _EFFECTS.size() + 1, _EFFECTS.size()]
_fx.call("spawn", effect, global_transform.translated_local(Vector3(0.0, 0.25, 0.0)))
+1
View File
@@ -0,0 +1 @@
uid://bu2r8vtpfpwe7
+32
View File
@@ -0,0 +1,32 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://systems/fx/effect_gallery.gd" id="1_opqwa"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_lt3bb"]
albedo_color = Color(0.62, 0.53, 0.36, 1)
[sub_resource type="PlaneMesh" id="PlaneMesh_2rj6n"]
material = SubResource("StandardMaterial3D_lt3bb")
size = Vector2(12, 12)
[node name="EffectGallery" type="Node3D" unique_id=530454952]
script = ExtResource("1_opqwa")
[node name="Camera3D" type="Camera3D" parent="." unique_id=1818153475]
transform = Transform3D(1, 0, 0, 0, 0.95105654, 0.309017, 0, -0.309017, 0.95105654, 0, 2, 4.5)
[node name="Sun" type="DirectionalLight3D" parent="." unique_id=1218164313]
transform = Transform3D(0.8660254, 0.38302222, -0.32139382, 0, 0.64278764, 0.76604444, 0.5, -0.66341394, 0.5566704, 0, 0, 0)
[node name="Ground" type="MeshInstance3D" parent="." unique_id=178415463]
mesh = SubResource("PlaneMesh_2rj6n")
[node name="UI" type="CanvasLayer" parent="." unique_id=1848100773]
[node name="Label" type="Label" parent="UI" unique_id=1347149836]
offset_left = 24.0
offset_top = 20.0
offset_right = 24.0
offset_bottom = 20.0
theme_override_font_sizes/font_size = 28
text = "FX gallery"
+51
View File
@@ -0,0 +1,51 @@
extends Node
## Fx — the one place gameplay code fires a visual effect. Code names WHICH effect and WHERE;
## the .tscn owns HOW it looks (colour, curves, emission — all inspector-editable, no recompile).
## Autoload singleton. Usage: Fx.spawn(&"blood_burst", hit_transform)
##
## Scenes are loaded lazily (not preloaded) so this autoload compiles and boots even before the
## placeholder .tscn are seeded — see tools/seed_fx_placeholders.gd.
const _PATHS: Dictionary = {
&"blood_burst": "res://systems/fx/blood_burst.tscn",
&"slam_dust": "res://systems/fx/slam_dust.tscn",
&"spark": "res://systems/fx/spark.tscn",
&"hit_flash": "res://systems/fx/hit_flash.tscn",
}
var _cache: Dictionary = {}
# Spawn a named effect at `xform`, parented to `parent` (or the current scene). Returns the
# instance, or null if the effect is unknown or its scene is missing.
func spawn(effect: StringName, xform: Transform3D, parent: Node = null) -> Node:
var scene := _scene(effect)
if scene == null:
return null
var host := parent if parent != null else _default_host()
if host == null:
push_warning("Fx.spawn(%s): no parent and no current scene to attach to" % effect)
return null
var inst := scene.instantiate() as Node3D
host.add_child(inst)
inst.global_transform = xform
return inst
func _scene(effect: StringName) -> PackedScene:
if _cache.has(effect):
return _cache[effect]
if not _PATHS.has(effect):
push_warning("Fx.spawn: unknown effect '%s' (add it to fx/fx.gd)" % effect)
return null
var scene := load(_PATHS[effect]) as PackedScene
if scene == null:
push_warning("Fx.spawn: scene missing for '%s' at %s" % [effect, _PATHS[effect]])
return null
_cache[effect] = scene
return scene
func _default_host() -> Node:
var tree := get_tree()
return tree.current_scene if tree != null else null
+1
View File
@@ -0,0 +1 @@
uid://b223163l8yasu
+41
View File
@@ -0,0 +1,41 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="1_6wb3q"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n65j7"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="SphereMesh_ih2se"]
material = SubResource("StandardMaterial3D_n65j7")
radius = 0.05
height = 0.1
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="Curve_kfjqk"]
_data = [Vector2(0, 0.2), 0.0, 0.0, 0, 0, Vector2(0.3, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="Gradient_8o2om"]
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0)
[node name="hit_flash" type="CPUParticles3D" unique_id=454164430]
emitting = false
amount = 6
lifetime = 0.18
one_shot = true
explosiveness = 1.0
randomness = 0.2
mesh = SubResource("SphereMesh_ih2se")
spread = 180.0
gravity = Vector3(0, 0, 0)
initial_velocity_min = 0.5
initial_velocity_max = 1.5
scale_amount_min = 0.8
scale_amount_max = 1.4
scale_amount_curve = SubResource("Curve_kfjqk")
color_ramp = SubResource("Gradient_8o2om")
script = ExtResource("1_6wb3q")
+33
View File
@@ -0,0 +1,33 @@
extends CPUParticles3D
## Root helper for a one-shot effect scene: fires this emitter (and any CPUParticles3D children)
## once when spawned, then frees the whole scene after the longest-lived particle has died. This
## is what lets an effect be nothing but a tunable CPUParticles3D scene — no per-effect code, so
## Siim owns the look entirely in the inspector. Fired by the Fx autoload; see fx/README.md.
const _FREE_MARGIN := 0.5
func _ready() -> void:
var ttl := _arm(self)
for emitter: CPUParticles3D in _child_emitters():
ttl = maxf(ttl, _arm(emitter))
get_tree().create_timer(ttl + _FREE_MARGIN).timeout.connect(queue_free)
func _arm(p: CPUParticles3D) -> float:
p.one_shot = true
p.emitting = true
return p.lifetime
func _child_emitters() -> Array[CPUParticles3D]:
var out: Array[CPUParticles3D] = []
_gather(self, out)
return out
func _gather(node: Node, out: Array[CPUParticles3D]) -> void:
for child: Node in node.get_children():
if child is CPUParticles3D:
out.append(child)
_gather(child, out)
+1
View File
@@ -0,0 +1 @@
uid://dkoudbnrkfiaw
+44
View File
@@ -0,0 +1,44 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="1_88uyl"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1tr47"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="SphereMesh_bw8nk"]
material = SubResource("StandardMaterial3D_1tr47")
radius = 0.16
height = 0.32
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="Curve_xgm72"]
_data = [Vector2(0, 0.4), 0.0, 0.0, 0, 0, Vector2(0.35, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 3
[sub_resource type="Gradient" id="Gradient_l8m5g"]
colors = PackedColorArray(0.72, 0.6, 0.4, 0.9, 0.55, 0.45, 0.3, 0)
[node name="slam_dust" type="CPUParticles3D" unique_id=82833428]
emitting = false
amount = 36
lifetime = 0.9
one_shot = true
explosiveness = 1.0
randomness = 0.5
mesh = SubResource("SphereMesh_bw8nk")
emission_shape = 1
emission_sphere_radius = 0.5
direction = Vector3(0, 1, 0)
spread = 75.0
gravity = Vector3(0, -6, 0)
initial_velocity_min = 2.5
initial_velocity_max = 6.5
scale_amount_min = 0.6
scale_amount_max = 1.4
scale_amount_curve = SubResource("Curve_xgm72")
color_ramp = SubResource("Gradient_l8m5g")
script = ExtResource("1_88uyl")
+40
View File
@@ -0,0 +1,40 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://systems/fx/one_shot_effect.gd" id="1_kk3a7"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tjifq"]
transparency = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="SphereMesh_pv2r6"]
material = SubResource("StandardMaterial3D_tjifq")
radius = 0.03
height = 0.06
radial_segments = 4
rings = 2
[sub_resource type="Curve" id="Curve_l33bb"]
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 2
[sub_resource type="Gradient" id="Gradient_pnkyn"]
colors = PackedColorArray(1, 0.95, 0.6, 1, 1, 0.55, 0.1, 0)
[node name="spark" type="CPUParticles3D" unique_id=69954641]
emitting = false
amount = 14
lifetime = 0.35
one_shot = true
explosiveness = 1.0
randomness = 0.6
mesh = SubResource("SphereMesh_pv2r6")
direction = Vector3(0, 1, 0)
gravity = Vector3(0, -9, 0)
initial_velocity_min = 3.0
initial_velocity_max = 7.0
scale_amount_min = 0.4
scale_amount_curve = SubResource("Curve_l33bb")
color_ramp = SubResource("Gradient_pnkyn")
script = ExtResource("1_kk3a7")