extends Node
## GATE — DCLevel's collision rebuild at SCALE (2026-10-05: brush edits took ~1.1 s
## with 2480 brushes, and DCLevel.get_stats() put 98% of it in ONE phase: ms_collide
## 1149 of 1172 ms — the clip was 8.6 ms).
##
## Cause (engine source, modules/jolt_physics/objects/jolt_shaped_object_3d.cpp): a
## Jolt body that is IN A SPACE rebuilds its whole compound shape on every add_shape
## (add_shape -> _shapes_changed -> commit_shapes -> build_shapes). _upload_collision
## put the body in the space FIRST and then added one shape per brush: n compound
## builds of up to n shapes — quadratic.
##
## 1. with 2000 brushes, the collision phase of one rebuild is under 150 ms
## 2. it is still CORRECT: a ray onto each of 25 sampled brushes hits the level, and
## get_brush_id_from_shape(shape) names THAT brush (the shape order is the brush
## order picking relies on)
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_level_collision_scale.tscn
const N := 2000
var fails := 0
var checks := 0
func _ok(what: String, cond: bool, note: String = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", note])
func _box(lo: Vector3, hi: Vector3) -> Array:
return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]
func _ready() -> void:
var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
gs.route("world:level:*", 1, "peer")
var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
if keet != null:
keet.current_member_id = "gate-author"
var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
await _run(map)
if checks < 8:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE level_collision_scale: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _run(map: Node3D) -> void:
var persist: Node = map.get("_persist")
var level: Node = map.get("level")
# 2000 1 m boxes on a grid 3 m apart, 40 per row, 100 m away from map_test's own.
var keys: Array = []
persist.begin_gesture("Grid")
for i in N:
var x := 200.0 + float(i % 40) * 3.0
var z := 200.0 + float(i / 40) * 3.0
keys.append(persist.record("brush.create", {"planes": _box(Vector3(x, 0, z), Vector3(x + 1, 1, z + 1)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false}))
persist.end_gesture()
persist.refold()
var st: Dictionary = level.get_stats()
var ms := float(st.get("ms_collide", -1.0))
_ok("1. collision phase at %d+ brushes under 150 ms" % N, ms >= 0.0 and ms < 150.0,
"%.1f ms (whole rebuild %.1f ms, clip %.1f ms)" % [ms, float(st.get("build_ms", -1)), float(st.get("ms_clip", -1))])
for i in 3:
await get_tree().physics_frame
var space: PhysicsDirectSpaceState3D = (level as Node3D).get_world_3d().direct_space_state
var hits := 0
var right := 0
var bad: Array = []
for s in 25:
var i := (s * 79) % N
var want: int = level.brush_by_key(String(keys[i]))
var x := 200.0 + float(i % 40) * 3.0 + 0.5
var z := 200.0 + float(i / 40) * 3.0 + 0.5
var q := PhysicsRayQueryParameters3D.create(Vector3(x, 5, z), Vector3(x, -1, z))
var hit: Dictionary = space.intersect_ray(q)
if hit.is_empty() or hit.get("collider") != level:
bad.append("%d: no level hit" % i)
continue
hits += 1
var got: int = level.get_brush_id_from_shape(int(hit["shape"]))
if got == want:
right += 1
else:
bad.append("%d: shape -> brush %d, want %d" % [i, got, want])
_ok("2. rays onto 25 sampled brushes hit the level", hits == 25, "%d/25" % hits)
_ok("2. and each hit names THAT brush", right == 25, "%d/25 %s" % [right, str(bad.slice(0, 3))])
# 3. THE WIREFRAME follows every brush fold; it filled its mesh one vertex per
# GDScript call (~60k calls at 2000 boxes, ~35 ms). One array upload instead.
var bv: Node = map.call("_ensure_brush_view")
var best := INF
for k in 3:
var t0 := Time.get_ticks_usec()
bv.refresh()
best = minf(best, (Time.get_ticks_usec() - t0) / 1000.0)
var segs: int = bv.segment_count()
_ok("3. the wireframe refresh at %d+ brushes is under 12 ms" % N, best < 12.0, "%.1f ms" % best)
_ok("3. and it still draws every edge (12 per box)", segs >= N * 12, "%d segments" % segs)
# 4. THE EDGE CACHE (dc-terrain, keyed by each brush's planes) never serves a stale
# outline: move a brush and delete another with the cache warm, then compare the
# whole-level edges with a fresh per-brush computation (get_brush_edges_of).
persist.record("brush.translate", {"brush_key": String(keys[7]), "delta": [0.0, 2.0, 0.0]})
persist.record("brush.remove", {"brush_key": String(keys[8])})
persist.refold()
var cached: PackedVector3Array = level.get_brush_edges()
var fresh := PackedVector3Array()
for id: int in (level.get_live_brush_ids() as PackedInt32Array):
if not bool((level.get_brush(id) as Dictionary).get("clearance", false)):
fresh.append_array(level.get_brush_edges_of(id))
_ok("4. after a move and a delete the cached edges equal a fresh computation",
cached == fresh, "%d vs %d points" % [cached.size(), fresh.size()])
# 5. THE BOT NAV GRAPH, built on every map entry from every brush as a box. Each
# chunk tested every box for every sample: 25.1 s at 1522 boxes (the starter-map
# load hitch, 2026-10-05). dc-terrain now hands each chunk only the boxes near it.
var tn := Time.get_ticks_usec()
var populated: int = map.nav_rebuild()
var nav_ms := (Time.get_ticks_usec() - tn) / 1000.0
_ok("5. the nav graph over %d+ brushes builds in under 2 s" % N, nav_ms < 2000.0 and populated > 0,
"%.0f ms, %d chunks" % [nav_ms, populated])
# 6. DENSE: 1500 more boxes 1.2 m apart (the layout of a big built-up Home), so a
# chunk holds hundreds. Per-chunk filtering alone left each sample testing all of
# them (758 ms at 3500 brushes); dc-terrain's BoxGrid buckets them (170 ms).
persist.begin_gesture("Dense")
for i in 1500:
var x := -40.0 - float(i % 50) * 1.2
var z := -40.0 - float(i / 50) * 1.2
persist.record("brush.create", {"planes": _box(Vector3(x, 0, z), Vector3(x + 1, 1, z + 1)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false})
persist.end_gesture()
persist.refold()
var td := Time.get_ticks_usec()
var populated2: int = map.nav_rebuild()
var dense_ms := (Time.get_ticks_usec() - td) / 1000.0
# 350 ms: ~3x what it measures (115-127 ms), well under the 758 ms before BoxGrid.
_ok("6. DENSE: the nav graph over %d brushes (1500 packed) builds in under 350 ms" % (N + 1500),
dense_ms < 350.0 and populated2 > 0, "%.0f ms, %d chunks" % [dense_ms, populated2])