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:
@@ -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)
|
||||
Reference in New Issue
Block a user