OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / entity_links.gd

131 lines · gd ref:
extends Node3D
class_name EntityLinks
## THE WIRING — see which entity points at which, hyper-games #31.
##
## A level's connections have no appearance. A trigger_teleport is a touch volume
## the renderer draws nothing for; the info_teleport_destination it sends you to is
## a point entity with no geometry at all. START.MAP has 8 triggers across 4
## destinations, all correctly wired since the import work, and none of it is
## visible: standing on a pad tells you nothing about where it goes, and two pads
## sharing one destination look exactly like two pads with different ones.
##
## TrenchBroom draws these as EntityLinkRenderer, target -> targetname. This is
## that, over the wiring we already hold — the spawned TeleportTrigger nodes carry
## `dest_pos` because the teleport itself needs it, so nothing is re-parsed and
## nothing can disagree with what the game will actually do to the player.
##
## DRAWN THROUGH GEOMETRY, deliberately. Both ends of a link are usually in
## different rooms with a wall between them — that is what a teleporter is for — so
## a depth-tested line would be hidden exactly when it is worth seeing. This is the
## same choice BrushView makes for the aimed brush and rejects for the whole-level
## wireframe: an x-ray of one thing you asked about is a diagnosis, an x-ray of
## everything is a haystack.
##
## REBUILT ON DEMAND. The links change when the level is imported or re-imported,
## not per frame.

## Source end. Warm, because it is where you are standing when you ask.
const COLOR_LINK := Color(1.0, 0.55, 0.15, 0.85)
## The destination end gets a marker: a line arriving at a point in mid-air reads
## as an unfinished line without one.
const COLOR_DEST := Color(0.3, 1.0, 0.5, 0.95)
const DEST_SIZE := 9.0
## Half-height of the upright tick drawn at each end, in metres. Enough to read as
## a marker at across-the-room distance without becoming scenery.
const TICK := 0.6

var _lines: MeshInstance3D = null
var _dests: MeshInstance3D = null
var _link_count := 0


func _ready() -> void:
	# Vertices are WORLD coordinates — a trigger's global_position and the
	# destination the importer resolved — so this node must not apply a transform
	# of its own on top of them. top_level says so explicitly rather than relying
	# on whichever parent it is attached to happening to sit at the origin.
	top_level = true
	_lines = _make_layer(COLOR_LINK, Mesh.PRIMITIVE_LINES)
	_dests = _make_layer(COLOR_DEST, Mesh.PRIMITIVE_POINTS)


func _make_layer(color: Color, primitive: int) -> MeshInstance3D:
	var mi := MeshInstance3D.new()
	mi.mesh = ImmediateMesh.new()
	var m := StandardMaterial3D.new()
	m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	m.albedo_color = color
	m.no_depth_test = true
	if primitive == Mesh.PRIMITIVE_POINTS:
		# Without an explicit size a point renders as one pixel — invisible at the
		# distance these are meant to be read from.
		m.use_point_size = true
		m.point_size = DEST_SIZE
	mi.material_override = m
	mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
	add_child(mi)
	return mi


## Rebuild from the teleporters spawned under `container` (QuakeEntities puts them
## in "QuakeTeleporters"). Safe to call before they exist — an empty container
## clears the lines rather than leaving stale ones from a previous map.
func refresh(container: Node) -> void:
	_link_count = 0
	var segs := PackedVector3Array()
	var pts := PackedVector3Array()
	if container != null and is_instance_valid(container):
		for child in container.get_children():
			if not (child is TeleportTrigger):
				continue                    # the swirly pads are visual-only volumes
			var t := child as TeleportTrigger
			# global_position, not position: the container hangs off the level, which
			# the map lifts, so a link drawn in local space would sit at the wrong
			# depth by exactly the embed offset.
			var from: Vector3 = t.global_position
			var to: Vector3 = t.dest_pos
			segs.push_back(from)
			segs.push_back(to)
			# An upright tick at each end, so a link that runs nearly along the view
			# direction still shows both of its ends rather than one dot.
			segs.push_back(from - Vector3(0, TICK, 0))
			segs.push_back(from + Vector3(0, TICK, 0))
			segs.push_back(to - Vector3(0, TICK, 0))
			segs.push_back(to + Vector3(0, TICK, 0))
			pts.push_back(to)
			_link_count += 1
	_fill(_lines, segs, Mesh.PRIMITIVE_LINES)
	_fill(_dests, pts, Mesh.PRIMITIVE_POINTS)


func _fill(mi: MeshInstance3D, pts: PackedVector3Array, primitive: int) -> void:
	if mi == null or not is_instance_valid(mi):
		return                              # refresh() before _ready()
	var im: ImmediateMesh = mi.mesh
	im.clear_surfaces()
	if pts.is_empty():
		return
	im.surface_begin(primitive)
	for p: Vector3 in pts:
		im.surface_add_vertex(p)
	im.surface_end()


## How many links are drawn. The count a caller reports, and what the gate asserts —
## "the lines are there" is not checkable from a mesh anyone can see.
func link_count() -> int:
	return _link_count


## The segments as actually built, in pairs. Exposed because the count alone does
## not distinguish a link drawn to the right destination from one drawn to the
## wrong one, and that is the failure this renderer would make convincing.
func line_vertices() -> PackedVector3Array:
	if _lines == null or not is_instance_valid(_lines):
		return PackedVector3Array()
	var im: ImmediateMesh = _lines.mesh
	if im == null or im.get_surface_count() == 0:
		return PackedVector3Array()
	return im.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]