Add blood decals, gore, mobile HUD, web start gate + touch/perf tests

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
This commit is contained in:
2026-09-04 14:07:57 +03:00
parent 01d6ecf475
commit 981ebf1910
32 changed files with 1536 additions and 113 deletions
+97
View File
@@ -0,0 +1,97 @@
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)