extends SceneTree
## Entity link lines are drawn where the game will actually send you — #31.
##
## "$HYPER_TPS_GODOT" --headless --script scenes/test/entity_links_gate.gd
##
## The renderer is over data we already hold, so the thing that can go wrong is
## not "is the wiring correct" — the import gate covers that — but whether the
## lines are drawn over THAT wiring rather than something adjacent to it. So this
## reads the mesh back and matches every segment against the live TeleportTrigger
## nodes: same start, same end, the same count.
##
## A line to the wrong destination is the exact failure the renderer exists to
## expose, and it would look completely convincing on screen.
const MAP_SCENE := "res://scenes/maps/map_dc_world.tscn"
const IMPORT_TIMEOUT_MS := 60000
## Per link: the segment itself, plus an upright tick at each end.
const VERTS_PER_LINK := 6
const EPS := 0.001
var _map: Node = null
var _failures: Array[String] = []
func _check(label: String, ok: bool, detail: String) -> void:
if not ok:
_failures.append("%s (%s)" % [label, detail])
print(" %-50s %s %s" % [label, "OK " if ok else "FAIL", detail])
func _initialize() -> void:
_run.call_deferred()
func _await_import(budget_ms: int) -> int:
var start := Time.get_ticks_msec()
while Time.get_ticks_msec() - start < budget_ms:
await process_frame
var lvl: Object = _map.get("level")
if lvl != null and lvl.has_method("get_stats"):
if int((lvl.get_stats() as Dictionary).get("brushes", 0)) > 0:
return Time.get_ticks_msec() - start
return -1
func _run() -> void:
print("\n=== entity link lines ===")
var ps: PackedScene = load(MAP_SCENE)
if ps == null:
print("RESULT: FAIL — cannot load %s" % MAP_SCENE)
quit(1)
return
_map = ps.instantiate()
root.add_child(_map)
# Assigned first, NOT `if await f() < 0`: await binds to the whole expression,
# so the comparison would be awaited instead of the call.
var imported: int = await _await_import(IMPORT_TIMEOUT_MS)
if imported < 0:
print("RESULT: SKIP — the level never imported (no START.MAP on this machine?)")
quit(0)
return
var tele := _map.get_node_or_null("QuakeTeleporters")
if tele == null:
print("RESULT: SKIP — no teleporters spawned (no WAD/palette on this machine?)")
quit(0)
return
# What the game will do to the player, straight off the live nodes.
var wired: Array[Dictionary] = []
for child in tele.get_children():
if child is TeleportTrigger:
wired.append({"from": (child as TeleportTrigger).global_position,
"to": (child as TeleportTrigger).dest_pos})
_check("START.MAP has wired teleporters", wired.size() > 0, "%d trigger(s)" % wired.size())
if wired.is_empty():
_finish()
return
var links := _map.get_node_or_null("EntityLinks")
_check("the map built a link renderer", links != null,
"EntityLinks %s" % ("present" if links != null else "MISSING — nothing draws the wiring"))
if links == null:
_finish()
return
_check("one link per wired teleporter", links.link_count() == wired.size(),
"%d link(s) for %d trigger(s)" % [links.link_count(), wired.size()])
# --- read the mesh back ----------------------------------------------------
# Through the renderer's own accessor rather than by walking its children:
# which child holds which surface is its business, and ImmediateMesh does not
# answer surface_get_primitive_type anyway.
var verts: PackedVector3Array = links.line_vertices()
_check("the line mesh was built", verts.size() > 0, "%d vertices" % verts.size())
if verts.is_empty():
_finish()
return
_check("%d vertices for %d links" % [verts.size(), wired.size()],
verts.size() == wired.size() * VERTS_PER_LINK,
"want %d" % (wired.size() * VERTS_PER_LINK))
# --- and every segment must be a real teleport -----------------------------
# Matched as a SET, not pairwise by index: the drawing order is the renderer's
# business, and asserting it would make this gate fail on a harmless change.
var unmatched := 0
for i in wired.size():
var seg_from: Vector3 = verts[i * VERTS_PER_LINK]
var seg_to: Vector3 = verts[i * VERTS_PER_LINK + 1]
var found := false
for w in wired:
if seg_from.distance_to(w["from"]) < EPS and seg_to.distance_to(w["to"]) < EPS:
found = true
break
if not found:
unmatched += 1
if unmatched == 1:
print(" first unmatched: %s -> %s" % [str(seg_from), str(seg_to)])
_check("every line runs trigger -> its own destination", unmatched == 0,
"%d segment(s) match no teleporter" % unmatched)
# The point of the renderer: two pads sharing a destination must be
# distinguishable from two pads with different ones. That is only true if the
# destinations are actually read per trigger rather than assumed.
var dests := {}
for w in wired:
dests[str(w["to"])] = true
print(" %d trigger(s) across %d distinct destination(s)" % [wired.size(), dests.size()])
_finish()
func _finish() -> void:
print("")
if _failures.is_empty():
print("RESULT: PASS")
quit(0)
else:
for f in _failures:
print(" FAILED: %s" % f)
print("RESULT: FAIL")
quit(1)