Remove the persistent blood-decal system
Strip only the floor/wall blood-splat decals added in 981ebf1, keeping the
older death-spray, spear-wound spurt, and "Blood & Gore" toggle:
- Delete blood_decals.gd + shader, the Gore autoload (gore.gd), and
tests/blood_decals_test.gd
- Drop the Gore autoload and the Blood DP section
- Remove all Gore.splat() call sites (matador.gd, bear.gd, player.gd);
the _blood_burst spray and _spawn_blood spurt stay
- game_shell no longer spawns a per-level decal pool
- Update level_switch test + run_tests.sh accordingly
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
extends SceneTree
|
||||
## Persistent blood splats (blood_decals.gd + the Gore autoload). Guards four contracts:
|
||||
## • a splat stamps quads onto the floor and onto a wall within reach of the hit;
|
||||
## • the ring buffer caps the instance count no matter how many hits land;
|
||||
## • Settings.gore = false spawns nothing;
|
||||
## • the pool is a plain node, so it frees with its parent (a level swap clears the stains).
|
||||
## blood_decals.gd references the DP autoload, so it's load()ed at runtime here (not a
|
||||
## top-level preload) — under --script a preload compiles before autoloads register.
|
||||
## Run: godot --headless --script tests/blood_decals_test.gd
|
||||
|
||||
var _pass := 0
|
||||
var _fail := 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _ok(c: bool, m: String) -> void:
|
||||
if c:
|
||||
_pass += 1
|
||||
print(" PASS: ", m)
|
||||
else:
|
||||
_fail += 1
|
||||
print(" FAIL: ", m)
|
||||
|
||||
|
||||
# Count instances actually placed on the map. Only the first `visible_instance_count` slots of
|
||||
# the ring buffer are drawn, so that's the live count once the buffer has wrapped.
|
||||
func _live_instances(pool: Node) -> int:
|
||||
var mm: MultiMesh = pool.multimesh
|
||||
if mm == null:
|
||||
return 0
|
||||
return mm.visible_instance_count
|
||||
|
||||
|
||||
func _static_box(size: Vector3, pos: Vector3) -> StaticBody3D:
|
||||
var body := StaticBody3D.new()
|
||||
body.collision_layer = 1 # PhysicsLayers.WORLD
|
||||
body.position = pos
|
||||
var shape := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = size
|
||||
shape.shape = box
|
||||
body.add_child(shape)
|
||||
return body
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var dp: Node = root.get_node("DP")
|
||||
var settings: Node = root.get_node("Settings")
|
||||
var cap := int(dp.f("blood_cap"))
|
||||
|
||||
# A minimal world: a floor plane and one vertical wall, both on WORLD so the splat rays hit.
|
||||
var world := Node3D.new()
|
||||
root.add_child(world)
|
||||
world.add_child(_static_box(Vector3(40, 1, 40), Vector3(0, -0.5, 0))) # floor
|
||||
world.add_child(_static_box(Vector3(1, 6, 40), Vector3(1.4, 3, 0))) # wall at x≈1.4
|
||||
|
||||
var pool: Node = (load("res://blood_decals.gd") as GDScript).new()
|
||||
world.add_child(pool)
|
||||
await create_timer(0.4).timeout
|
||||
|
||||
# Baked atlas exists and is one row of 8 shapes.
|
||||
var atlas: Texture2D = pool.material_override.get_shader_parameter("atlas")
|
||||
_ok(atlas != null and atlas.get_width() == 8 * 64, "splat atlas baked (8 cells)")
|
||||
|
||||
# A hit next to the wall stains both the floor beneath it and the wall.
|
||||
settings.gore = true
|
||||
pool.splat(Vector3(0.8, 0.1, 0.0), Vector3(1, 0, 0), 0.6)
|
||||
var after_one := _live_instances(pool)
|
||||
_ok(after_one > 0, "a splat stamps the floor (got %d quads)" % after_one)
|
||||
# Floor cluster is 1 + satellites; anything beyond that came from the wall fan.
|
||||
var floor_max := 1 + int(dp.f("blood_satellites"))
|
||||
_ok(
|
||||
after_one > floor_max,
|
||||
"a nearby wall also catches blood (%d > floor-only %d)" % [after_one, floor_max]
|
||||
)
|
||||
|
||||
# Ring buffer: hammer far past the cap; live count never exceeds it.
|
||||
for i in cap * 2:
|
||||
pool.splat(Vector3(0.0, 0.1, 0.0), Vector3(1, 0, 0), 0.4)
|
||||
_ok(_live_instances(pool) <= cap, "ring buffer holds at the cap (%d)" % cap)
|
||||
|
||||
# Gore off: no new stains. Route through the Gore autoload to prove the gate lives there.
|
||||
var before_off := _live_instances(pool)
|
||||
settings.gore = false
|
||||
root.get_node("Gore").splat(Vector3(5, 0.1, 0.0), Vector3.ZERO, 0.6)
|
||||
_ok(_live_instances(pool) == before_off, "gore off spawns nothing")
|
||||
|
||||
# Lifetime: the pool is a plain node, so freeing its parent (a level swap) clears it.
|
||||
world.queue_free()
|
||||
await create_timer(0.2).timeout
|
||||
_ok(not is_instance_valid(pool), "pool frees with its parent (no leak into next level)")
|
||||
|
||||
print("Results: %d passed, %d failed" % [_pass, _fail])
|
||||
quit(1 if _fail > 0 else 0)
|
||||
@@ -1 +0,0 @@
|
||||
uid://b8em8poj26a57
|
||||
@@ -53,13 +53,8 @@ func _run() -> void:
|
||||
# Shell survives and hosts exactly one level module.
|
||||
var level_root: Node = scene.get_node_or_null("LevelRoot")
|
||||
_ok(level_root != null, "shell has a LevelRoot")
|
||||
# LevelRoot holds the module plus the level's blood-splat pool; exactly one of them
|
||||
# is the geometry module (the other is BloodDecals).
|
||||
var module_children := 0
|
||||
if level_root != null:
|
||||
for child: Node in level_root.get_children():
|
||||
if not child.is_in_group(&"blood_decals"):
|
||||
module_children += 1
|
||||
# LevelRoot holds exactly the one level module.
|
||||
var module_children := level_root.get_child_count() if level_root != null else 0
|
||||
_ok(module_children == 1, "exactly one level module loaded")
|
||||
_ok(not get_nodes_in_group(&"player").is_empty(), "player (shell) present")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user