OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / entity_links_gate.gd

145 lines · gd ref:
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)