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]