OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / teleporter / teleporter.gd

94 lines · gd ref:
extends Area3D
## Teleporter — either switches levels or moves within the same level.
## If target_map is set, triggers a full level change via game.gd.
## Otherwise, teleports the player to destination within the current map.

@export var destination: Vector3 = Vector3.ZERO
@export var target_map: String = ""  ## e.g. "res://scenes/maps/map_cyber.tscn"
## Optional label shown above teleporter. A SETTER, because object.set_prop changes
## it on a live teleporter: reading it only in _ready left the sign showing the old
## text until the next map load.
@export var label_text: String = "":
	set(v):
		label_text = v
		if is_node_ready():
			_refresh_label()

@onready var indicator: OmniLight3D = $Indicator
@onready var label_3d: Label3D = $Label3D


func _ready() -> void:
	body_entered.connect(_on_body_entered)

	# Visual distinction: orange for level switch, cyan for local teleport
	if target_map != "":
		indicator.light_color = Color(1.0, 0.5, 0.0)  # orange
	else:
		indicator.light_color = Color(0.0, 0.8, 1.0)  # cyan
	_refresh_label()
	if snap_to_ground and not Engine.is_editor_hint():
		_snap_origin = global_position


## STAND ON THE GROUND (2026-10-06). Authored pads were 117 m in the air (map_mountain),
## 12.7 m (map_geiss), 11.8 m inside rock (map_geiss_terraces), 10 cm into the floor
## (map_test) — gate_teleporter_ground. The pad resolves its ground the way an arrival
## does (ArrivalGround: physics rays, lift out of rock), re-snapping every SNAP_EVERY s
## to the best ground found until that ground is FINAL (finest LOD, own collision) —
## a pad far from the camera only gets there when someone walks near it. Resolved from
## the AUTHORED point every time, never from where the last snap put it.
@export var snap_to_ground: bool = true
const SNAP_EVERY := 0.5
const _Arrival := preload("res://scenes/player/arrival_ground.gd")
var _snap_origin: Variant = null
var _snap_t := 0.0


func _physics_process(delta: float) -> void:
	if _snap_origin == null:
		return
	_snap_t -= delta
	if _snap_t > 0.0:
		return
	_snap_t = SNAP_EVERY
	var excl: Array[RID] = []
	for c in find_children("*", "CollisionObject3D", true, false):
		excl.append((c as CollisionObject3D).get_rid())
	var r: Dictionary = _Arrival.resolve(get_world_3d().direct_space_state, _snap_origin, true,
			excl, _Arrival.find_terrain(self))
	if r["found"]:
		global_position = r["ground"]
	if r["ready"]:
		_snap_origin = null           # final: stop polling


func _refresh_label() -> void:
	if label_text != "":
		label_3d.text = label_text
	elif target_map != "":
		label_3d.text = target_map.get_file().get_basename().replace("map_", "").capitalize()
	else:
		label_3d.text = "Teleport"


func _on_body_entered(body: Node3D) -> void:
	if not body is CharacterBody3D:
		return
	# Only the local player triggers level changes
	if not body.is_multiplayer_authority():
		return

	if target_map != "":
		var game := get_tree().root.get_node_or_null("Game")
		if game and game.has_method("change_map"):
			game.change_map(target_map, destination)
	else:
		if body.has_method("arrive"):
			body.call("arrive", destination)    # held until the ground there is real
		else:
			body.global_position = destination
			body.velocity = Vector3.ZERO
		if "grapple_hooked" in body:
			body.grapple_hooked = false