class_name TeleportTrigger
extends Area3D
## A Quake trigger_teleport touch volume. On the LOCAL player entering, it moves them to the
## linked info_teleport_destination (origin + angle) and zeroes velocity — classic Quake
## teleport behaviour. Each client teleports its OWN player; the 18-byte state sync (with its
## SNAP_DISTANCE) propagates the jump to peers. Telefrag is deferred to a later session.
## Quake detected teleport touches with the player's full AABB (32x32x56u); our capsule is
## smaller, so the player visibly clips the pad before a tight trigger fires. Enlarge the
## detection box by this margin so it triggers as the player reaches the pad.
const TRIGGER_MARGIN := Vector3(0.6, 0.8, 0.6)
## A brief global cooldown after ANY teleport — prevents chains and double-fires when triggers
## overlap (START has two teleporters sharing one spot) or a destination sits near a trigger.
static var _cooldown_until_ms: int = 0
var dest_pos: Vector3 = Vector3.ZERO
var dest_yaw: float = 0.0
## Build from an importer teleport_triggers entry + its resolved destination
## trigger = {aabb_min, aabb_max, target} dest = {pos: Vector3, yaw: float}
static func create(trigger: Dictionary, dest: Dictionary) -> TeleportTrigger:
var amin: Vector3 = trigger.get("aabb_min", Vector3.ZERO)
var amax: Vector3 = trigger.get("aabb_max", Vector3.ZERO)
var t := TeleportTrigger.new()
t.dest_pos = dest.get("pos", Vector3.ZERO)
t.dest_yaw = float(dest.get("yaw", 0.0))
t.name = "Teleport_%s" % String(trigger.get("target", ""))
t.position = (amin + amax) * 0.5
t.monitoring = true
t.monitorable = false
t.collision_mask = 0xFFFFF # detect the player body on any of its usual layers
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
var sz := (amax - amin) + TRIGGER_MARGIN * 2.0
box.size = Vector3(maxf(sz.x, 0.1), maxf(sz.y, 0.1), maxf(sz.z, 0.1))
cs.shape = box
t.add_child(cs)
return t
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node) -> void:
if not (body is CharacterBody3D):
return
# Only the client that owns this player moves it (authority); peers get it via state sync.
if body.has_method("is_local_player") and not body.is_local_player():
return
var now := Time.get_ticks_msec()
if now < _cooldown_until_ms:
return # just teleported — ignore chained/overlapping triggers
_cooldown_until_ms = now + 500
print("[Teleport] %s: %s -> %s" % [name, str(body.global_position.round()), str(dest_pos.round())])
body.global_position = dest_pos
if "velocity" in body:
body.velocity = Vector3.ZERO
var head := body.get_node_or_null("CameraHead")
if head:
head.rotation.y = dest_yaw
var isync := body.get_node_or_null("InputSync")
if isync != null and "camera_rotation_y" in isync:
isync.camera_rotation_y = dest_yaw