Files
train-game/scripts/player.gd
2026-08-29 11:44:31 +02:00

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}