OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / door_mover.gd

120 lines · gd ref:
class_name DoorMover
extends AnimatableBody3D

## A Quake func_door materialised as an animated MODEL (not a per-frame SDF edit — see the
## models-vs-dc_terrain review: models are cheaper, more robust, and Godot's AnimatableBody3D
## carries/pushes the player for free). Slides open along the Quake move direction when the
## local player approaches, waits `wait_time`, then closes (unless wait == -1). Crush damage
## and button/targetname triggering are deferred to a later gameplay session — for now every
## door opens on proximity so the map is traversable.

var closed_pos: Vector3 = Vector3.ZERO
var open_pos: Vector3 = Vector3.ZERO
var move_speed: float = 4.0     # world units/sec
var wait_time: float = 3.0      # seconds open before auto-close; < 0 = stay open
var locked: bool = false        # key-required or targetname/event door → never opens on touch
var _travel: float = 1.0        # world distance closed→open (normalises the interp speed)
var _want_open: bool = false
var _t: float = 0.0             # 0 = closed, 1 = open
var _close_timer: float = 0.0

static func create(entry: Dictionary, tex: Texture2D) -> DoorMover:
	var origin: Vector3 = entry.get("origin", Vector3.ZERO)
	var parts: Array = entry.get("parts", [])
	var mdir: Vector3 = entry.get("move_dir", Vector3.UP)
	var dist: float = float(entry.get("move_distance", 0.0))
	var d := DoorMover.new()
	d.name = "Door_%s" % String(entry.get("targetname", "prox"))
	d.closed_pos = origin
	d.open_pos = origin + mdir * dist
	d.move_speed = maxf(float(entry.get("speed", 4.0)), 0.1)
	d.wait_time = float(entry.get("wait", 3.0))
	d.locked = bool(entry.get("locked", false))
	d._travel = maxf(dist, 0.01)
	d.position = origin
	# sync_to_physics = true RESETS the body transform to (0,0,0) on tree-entry (it expects to be
	# driven by the physics server) — the door then flickers/sticks at the origin. false keeps our
	# set position authoritative; the body still collides with + pushes the player via move.
	d.sync_to_physics = false

	var base_mat: StandardMaterial3D = null
	if tex != null:
		base_mat = StandardMaterial3D.new()
		base_mat.albedo_texture = tex
		base_mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST   # pixelated, like the terrain
		base_mat.uv1_triplanar = true            # box UVs stretch; triplanar keeps the tiling sane

	# One mesh + collision box per brush (offsets relative to the door origin) so a multi-brush
	# door keeps its shape and all parts move together. Track the combined box for the trigger.
	var lmin := Vector3(INF, INF, INF)
	var lmax := Vector3(-INF, -INF, -INF)
	for p in parts:
		var off: Vector3 = p.get("offset", Vector3.ZERO)
		var half: Vector3 = p.get("half", Vector3.ONE)
		lmin = lmin.min(off - half)
		lmax = lmax.max(off + half)
		var box := BoxMesh.new()
		box.size = half * 2.0
		var mi := MeshInstance3D.new()
		mi.mesh = box
		mi.position = off
		if base_mat != null:
			var m := base_mat.duplicate()
			m.uv1_scale = Vector3.ONE / maxf(0.5, half.length())
			mi.material_override = m
		d.add_child(mi)
		var cs := CollisionShape3D.new()
		var shape := BoxShape3D.new()
		shape.size = half * 2.0
		cs.shape = shape
		cs.position = off
		d.add_child(cs)

	# Proximity trigger: an Area a couple metres larger than the whole door opens it.
	var area := Area3D.new()
	area.name = "Proximity"
	area.collision_mask = 0xFFFFF
	var acs := CollisionShape3D.new()
	var ashape := BoxShape3D.new()
	ashape.size = (lmax - lmin) + Vector3(2.0, 2.0, 2.0)
	acs.shape = ashape
	acs.position = (lmin + lmax) * 0.5
	area.add_child(acs)
	d.add_child(area)
	d._area = area
	return d

var _area: Area3D = null

func _ready() -> void:
	position = closed_pos                 # assert the closed position on spawn (frame-0 correctness)
	if _area != null:
		_area.body_entered.connect(_on_proximity_enter)
		_area.body_exited.connect(_on_proximity_exit)

func _on_proximity_enter(body: Node) -> void:
	# Key/targetname (event) doors never open on touch — they wait for a key or a trigger we
	# don't have yet, so they stay in their default CLOSED position (Quake doors.qc model).
	if locked:
		return
	if body is CharacterBody3D:
		_want_open = true
		_close_timer = 0.0            # cancel any pending close

func _on_proximity_exit(body: Node) -> void:
	# wait < 0 → the door stays open forever (Quake wait -1). Otherwise start the countdown.
	if body is CharacterBody3D and wait_time >= 0.0:
		_close_timer = wait_time

func _physics_process(delta: float) -> void:
	if _close_timer > 0.0:
		_close_timer -= delta
		if _close_timer <= 0.0:
			_want_open = false
	var target := 1.0 if _want_open else 0.0
	var step := (move_speed / _travel) * delta
	_t = move_toward(_t, target, step)
	# Assert the position EVERY physics frame: sync_to_physics AnimatableBody3D is driven by
	# the physics server, so an unset frame leaves the door at the physics origin, not closed.
	position = closed_pos.lerp(open_pos, _t)