176 lines
5.2 KiB
GDScript
176 lines
5.2 KiB
GDScript
extends Node3D
|
|
## First-Person-Spieler. Bewegt sich in Zugkoordinaten (s laengs, x quer),
|
|
## damit der Innenraum auch bei voller Fahrt stabil begehbar bleibt.
|
|
|
|
const Mat = preload("res://scripts/mat.gd")
|
|
const Deco = preload("res://scripts/deco.gd")
|
|
const Car = preload("res://scripts/car.gd")
|
|
|
|
signal interact_pressed(hotspot: Dictionary)
|
|
|
|
enum Mode { WALK, DRIVE }
|
|
|
|
const RADIUS := 0.30
|
|
const HEIGHT := 1.75
|
|
const EYE := 1.62
|
|
const WALK_SPEED := 2.6
|
|
const RUN_SPEED := 4.6
|
|
const ACCEL := 14.0
|
|
const REACH := 2.0
|
|
const MOUSE_SENS := 0.0022
|
|
|
|
var s := 0.0
|
|
var x := 0.0
|
|
var yaw := 0.0
|
|
var pitch := 0.0
|
|
var mode := Mode.WALK
|
|
var carrying := 0 ## getragene Getränke
|
|
var hotspot: Dictionary = {} ## aktuell anvisierter Interaktionspunkt
|
|
var input_locked := false ## wenn ein Menue offen ist
|
|
|
|
var camera: Camera3D
|
|
var train
|
|
|
|
var _vel := Vector2.ZERO ## (quer, laengs)
|
|
|
|
func setup(train_node) -> void:
|
|
train = train_node
|
|
camera = Camera3D.new()
|
|
camera.fov = 75.0
|
|
camera.near = 0.05
|
|
camera.far = 900.0
|
|
add_child(camera)
|
|
snap_to_cab()
|
|
|
|
func snap_to_cab() -> void:
|
|
if train.cars.is_empty():
|
|
return
|
|
var loco = train.cars[0]
|
|
s = loco.center_s + loco.inner_z1 - 2.4
|
|
x = 0.0
|
|
yaw = 0.0
|
|
pitch = 0.0
|
|
# Falls dort etwas steht, nach hinten ausweichen.
|
|
var span: Vector2 = train.walk_range()
|
|
var tries := 0
|
|
while _blocked(x, s) and tries < 40:
|
|
s = maxf(s - 0.25, span.x)
|
|
tries += 1
|
|
|
|
## Aktuelles Lauftempo, für die Schrittgeräusche.
|
|
func move_speed() -> float:
|
|
return _vel.length()
|
|
|
|
func capture_mouse(on: bool) -> void:
|
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if on else Input.MOUSE_MODE_VISIBLE
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
|
yaw -= event.relative.x * MOUSE_SENS
|
|
pitch = clampf(pitch - event.relative.y * MOUSE_SENS, -1.35, 1.35)
|
|
elif event is InputEventKey and event.pressed and not event.echo:
|
|
if event.keycode == KEY_E and not input_locked:
|
|
if not hotspot.is_empty():
|
|
interact_pressed.emit(hotspot)
|
|
|
|
# --- Bewegung -------------------------------------------------------------
|
|
|
|
func forward_dir() -> Vector2:
|
|
## (quer, laengs) im Zugsystem
|
|
return Vector2(sin(yaw), cos(yaw))
|
|
|
|
## Bildschirm-rechts im Zugsystem. Godots Kamerabasis hat +X nach links,
|
|
## deshalb das umgekehrte Vorzeichen zu forward_dir().
|
|
func right_dir() -> Vector2:
|
|
return Vector2(-cos(yaw), sin(yaw))
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if train == null or train.cars.is_empty():
|
|
return
|
|
if mode == Mode.WALK and not input_locked:
|
|
_walk(delta)
|
|
else:
|
|
_vel = Vector2.ZERO
|
|
_update_transform()
|
|
_pick_hotspot()
|
|
|
|
func _walk(delta: float) -> void:
|
|
var move := Vector2.ZERO
|
|
if Input.is_key_pressed(KEY_W):
|
|
move += forward_dir()
|
|
if Input.is_key_pressed(KEY_S):
|
|
move -= forward_dir()
|
|
if Input.is_key_pressed(KEY_D):
|
|
move += right_dir()
|
|
if Input.is_key_pressed(KEY_A):
|
|
move -= right_dir()
|
|
if move.length() > 0.001:
|
|
move = move.normalized()
|
|
var target: float = RUN_SPEED if Input.is_key_pressed(KEY_SHIFT) else WALK_SPEED
|
|
_vel = _vel.move_toward(move * target, ACCEL * delta)
|
|
|
|
var step := _vel * delta
|
|
# Achsenweise schieben, damit man an Hindernissen entlanggleitet.
|
|
if not _blocked(x + step.x, s):
|
|
x += step.x
|
|
if not _blocked(x, s + step.y):
|
|
s += step.y
|
|
|
|
var span: Vector2 = train.walk_range()
|
|
s = clampf(s, span.x, span.y)
|
|
|
|
func _blocked(nx: float, ns: float) -> bool:
|
|
var loc: Dictionary = train.locate(ns)
|
|
var car = train.cars[int(loc["index"])]
|
|
var lz: float = float(loc["local_z"])
|
|
var half: float = Car.GANGWAY_HALF if bool(loc["gangway"]) else Car.INNER_HALF
|
|
if absf(nx) > half - RADIUS:
|
|
return true
|
|
var y0: float = Car.FLOOR_Y + 0.05
|
|
var y1: float = Car.FLOOR_Y + HEIGHT
|
|
for b in car.blockers:
|
|
var box: AABB = b
|
|
if box.position.y + box.size.y <= y0 or box.position.y >= y1:
|
|
continue
|
|
if nx + RADIUS <= box.position.x or nx - RADIUS >= box.position.x + box.size.x:
|
|
continue
|
|
if lz + RADIUS <= box.position.z or lz - RADIUS >= box.position.z + box.size.z:
|
|
continue
|
|
return true
|
|
return false
|
|
|
|
func _update_transform() -> void:
|
|
var loc: Dictionary = train.locate(s)
|
|
var xf: Transform3D = train.car_transform(int(loc["index"]))
|
|
var lz: float = float(loc["local_z"])
|
|
global_position = xf * Vector3(x, Car.FLOOR_Y, lz)
|
|
var look := Vector3(sin(yaw) * cos(pitch), sin(pitch), cos(yaw) * cos(pitch))
|
|
global_basis = xf.basis * Basis.looking_at(look, Vector3.UP)
|
|
camera.position = Vector3(0, EYE, 0)
|
|
|
|
# --- Interaktion ----------------------------------------------------------
|
|
|
|
func _pick_hotspot() -> void:
|
|
hotspot = {}
|
|
if input_locked:
|
|
return
|
|
var best_score := 0.35
|
|
var fwd := forward_dir()
|
|
for ci in train.cars.size():
|
|
var car = train.cars[ci]
|
|
for h in car.hotspots:
|
|
var p: Vector3 = h["pos"]
|
|
var hs: float = car.center_s + p.z
|
|
var d := Vector2(p.x - x, hs - s)
|
|
var dist := d.length()
|
|
if dist > REACH:
|
|
continue
|
|
if absf(p.y - (Car.FLOOR_Y + EYE)) > 1.7:
|
|
continue
|
|
var dir := d.normalized() if dist > 0.01 else fwd
|
|
var score := fwd.dot(dir) * (1.0 - dist / (REACH * 2.0))
|
|
if score > best_score:
|
|
best_score = score
|
|
hotspot = {"car": ci, "kind": String(h["kind"]), "seat": int(h["seat"]),
|
|
"pos": p, "dist": dist}
|