OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / teleport_trigger.gd

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