981ebf1910
Remove tools/fstest.html scratch page used to probe browser fullscreen/orientation APIs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EznnY8rH2dXhtono1kwsXg
98 lines
3.5 KiB
GDScript
98 lines
3.5 KiB
GDScript
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)
|