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}