extends Node3D ## Der Zugverband auf dem Streckennetz: Fahrphysik, Weichenwahl und ## die Abbildung "Laengsposition im Zug" -> Weltkoordinate. const CarScene = preload("res://scripts/car.gd") const Defs = preload("res://scripts/car_defs.gd") const Cargo = preload("res://scripts/cargo.gd") signal consist_changed() const GAP := 0.75 const DRAG := 0.6 const DOCK_RANGE := 3.0 const PLATFORM_HALF := 18.0 ## Bahnsteig reicht so weit um seine Mitte const HISTORY := 6 ## so viele Abschnitte merken wir uns rückwärts var loco_id := "steam" var consist: Array[String] = [] var cars: Array = [] var seg := 0 ## Abschnitt der Zugspitze var progress := 0.0 ## Position der Spitze auf dem Abschnitt var history: Array = [] ## zuletzt befahrene Abschnitte, ältester zuerst var switch_choice := 0 ## 0 = links/erster Nachfolger, 1 = rechts var speed := 0.0 var throttle := 0.0 var braking := false var cargo: Dictionary = {} var stations: Array = [] var docked_at: Node = null var sway_enabled := true var _track var _stand_time := 0.0 var _last_yaw := 0.0 var _roll := 0.0 var _time := 0.0 func setup(track, station_list: Array) -> void: _track = track stations = station_list func max_speed() -> float: return float(Defs.LOCOS[loco_id]["max_speed"]) func accel() -> float: return float(Defs.LOCOS[loco_id]["accel"]) func brake_power() -> float: return float(Defs.LOCOS[loco_id]["brake"]) func speed_kmh() -> float: return speed * 3.6 func cargo_capacity() -> int: var n := 0 for c in cars: n += c.capacity return n func total_cargo() -> int: var n := 0 for v in cargo.values(): n += int(v) return n func has_dining() -> bool: for c in cars: if c.kind == Defs.Kind.DINING: return true return false func seat_count() -> int: var n := 0 for c in cars: n += c.seats.size() return n # --- Verband -------------------------------------------------------------- func rebuild() -> void: for c in cars: c.queue_free() cars.clear() var ids: Array[String] = [loco_id] ids.append_array(consist) var s := 0.0 for i in ids.size(): var car = CarScene.new() car.build(ids[i]) car.index = i car.center_s = s - car.length * 0.5 s = car.center_s - car.length * 0.5 - GAP add_child(car) cars.append(car) _place_cars(0.0) refresh_cargo_visuals() consist_changed.emit() func total_length() -> float: if cars.is_empty(): return 0.0 var last = cars[cars.size() - 1] return absf(last.center_s - last.length * 0.5) func walk_range() -> Vector2: if cars.is_empty(): return Vector2.ZERO var front = cars[0] var back = cars[cars.size() - 1] return Vector2(back.center_s + back.inner_z0 + 0.35, front.center_s + front.inner_z1 - 0.35) func locate(s: float) -> Dictionary: var best := 0 var best_d := INF for i in cars.size(): var c = cars[i] var lo: float = c.center_s + c.inner_z0 var hi: float = c.center_s + c.inner_z1 if s >= lo and s <= hi: return {"index": i, "local_z": s - c.center_s, "gangway": false} var d: float = minf(absf(s - lo), absf(s - hi)) if d < best_d: best_d = d best = i var c2 = cars[best] return {"index": best, "local_z": s - c2.center_s, "gangway": true} func car_transform(i: int) -> Transform3D: return cars[i].global_transform func world_point(s: float, x: float, y: float) -> Transform3D: var loc := locate(s) return car_transform(int(loc["index"])).translated_local(Vector3(x, y, float(loc["local_z"]))) # --- Position auf dem Netz ----------------------------------------------- ## Spitze setzen und die Historie rückwärts auffüllen, damit auch der ## letzte Wagen einen Abschnitt unter sich hat. func place_head(seg_id: int, prog: float) -> void: seg = seg_id progress = prog while progress >= _track.seg_length(seg): progress -= _track.seg_length(seg) var nxt: Array = _track.next_of(seg) history.append(seg) seg = nxt[mini(switch_choice, nxt.size() - 1)] history.clear() var s := seg for i in HISTORY: var prev: Array = _track.prev_of(s) if prev.is_empty(): break s = prev[0] history.push_front(s) _place_cars(0.0) ## Abschnitt und Position eines Punktes, der d Meter hinter der Spitze liegt. func point_behind(d: float) -> Array: var s := seg var p := progress - d var k := history.size() while p < 0.0 and k > 0: k -= 1 s = history[k] p += _track.seg_length(s) return [s, maxf(p, 0.0)] ## Weltlage eines Punktes d Meter hinter der Spitze. func transform_behind(d: float) -> Transform3D: var sp := point_behind(d) return _track.sample(int(sp[0]), float(sp[1])) func _advance() -> void: while progress >= _track.seg_length(seg): progress -= _track.seg_length(seg) var nxt: Array = _track.next_of(seg) if nxt.is_empty(): progress = _track.seg_length(seg) speed = 0.0 return history.append(seg) if history.size() > HISTORY: history.pop_front() seg = nxt[mini(switch_choice, nxt.size() - 1)] ## Der nächste Abschnitt mit Weiche, den die Spitze erreicht - oder -1. func upcoming_switch() -> int: var s := seg for i in 3: if _track.is_switch(s): return s var nxt: Array = _track.next_of(s) if nxt.is_empty(): return -1 s = nxt[0] return -1 func _place_cars(delta: float) -> void: _time += delta var f: float = clampf(speed / maxf(max_speed(), 1.0), 0.0, 1.0) var head_xf: Transform3D = _track.sample(seg, progress) var yaw: float = head_xf.basis.get_euler().y if delta > 0.0: var rate: float = wrapf(yaw - _last_yaw, -PI, PI) / delta var target: float = clampf(-speed * rate * 0.018, -0.05, 0.05) _roll = lerpf(_roll, target, clampf(delta * 3.0, 0.0, 1.0)) _last_yaw = yaw for i in cars.size(): var car = cars[i] var xf: Transform3D = transform_behind(-car.center_s) if sway_enabled and speed > 0.2: var ph: float = float(i) * 1.9 var roll: float = _roll + sin(_time * 1.7 + ph) * 0.006 * f var pitch: float = sin(_time * 2.9 + ph) * 0.003 * f var bounce: float = sin(_time * 3.3 + ph) * 0.014 * f xf = xf * Transform3D(Basis.from_euler(Vector3(pitch, 0.0, roll)), Vector3(0, bounce, 0)) car.global_transform = xf # --- Ladung --------------------------------------------------------------- func refresh_cargo_visuals() -> void: var units: Array[int] = [] for t in cargo.keys(): for i in int(cargo[t]): units.append(int(t)) var idx := 0 for c in cars: if c.capacity <= 0: continue var take: int = mini(c.capacity, units.size() - idx) if take <= 0: c.show_cargo(0, Color.WHITE) continue c.show_cargo(take, Cargo.COLORS[units[idx]]) idx += take func add_cargo(t: int, amount: int) -> int: var free: int = cargo_capacity() - total_cargo() var take: int = mini(free, amount) if take > 0: cargo[t] = int(cargo.get(t, 0)) + take refresh_cargo_visuals() return take func remove_cargo(t: int, amount: int) -> int: var have: int = int(cargo.get(t, 0)) var take: int = mini(have, amount) if take > 0: cargo[t] = have - take if cargo[t] <= 0: cargo.erase(t) refresh_cargo_visuals() return take # --- Fahrt ---------------------------------------------------------------- func drive(delta: float) -> void: if braking: speed = maxf(speed - brake_power() * delta, 0.0) elif throttle > 0.0: speed = minf(speed + accel() * throttle * delta, max_speed()) else: speed = maxf(speed - DRAG * delta, 0.0) progress += speed * delta _advance() _place_cars(delta) _update_dock(delta) ## Client: Server-Stand übernehmen, dazwischen selbst weiterrechnen. func apply_snapshot(seg_id: int, prog: float, v: float, sw: int, hist: Array) -> void: switch_choice = sw if seg_id != seg: seg = seg_id progress = prog history = hist.duplicate() else: progress = lerpf(progress, prog, 0.4) speed = v func client_tick(delta: float) -> void: progress += speed * delta _advance() _place_cars(delta) _update_dock(delta) func _update_dock(delta: float) -> void: var near := nearest_station() var st: Node = near[0] if st == null: return if speed > 0.15 or float(near[1]) > DOCK_RANGE: _stand_time = 0.0 docked_at = null return _stand_time += delta docked_at = st ## Abstand eines Bahnhofs zum Zug entlang der Strecke. 0, solange der ## Bahnsteig neben dem Zug liegt. func station_gap(st) -> float: var ahead := distance_ahead(st) if ahead == 0.0: return 0.0 var behind := _distance_behind(st) if behind >= 0.0: return maxf(behind - total_length() - PLATFORM_HALF, 0.0) return maxf(ahead - PLATFORM_HALF, 0.0) ## Wie weit vor der Spitze liegt der Bahnhof, entlang der gewählten Route? ## 0 bedeutet: neben dem Zug oder direkt an der Spitze. INF: nicht auf dem Weg. func distance_ahead(st) -> float: var behind := _distance_behind(st) if behind >= 0.0 and behind <= total_length(): return 0.0 var s := seg var d := -progress for i in 4: if st.seg == s and (i > 0 or st.progress >= progress): return maxf(d + st.progress, 0.0) d += _track.seg_length(s) var nxt: Array = _track.next_of(s) if nxt.is_empty(): break s = nxt[mini(switch_choice, nxt.size() - 1)] return INF ## Abstand von der Spitze rückwärts bis zum Bahnhof, oder -1. func _distance_behind(st) -> float: if st.seg == seg and st.progress <= progress: return progress - st.progress var d := progress for k in range(history.size() - 1, -1, -1): var s: int = history[k] if st.seg == s: return d + (_track.seg_length(s) - st.progress) d += _track.seg_length(s) return -1.0 func nearest_station() -> Array: var best: Node = null var best_d := INF for s in stations: var d: float = station_gap(s) if d < best_d: best_d = d best = s return [best, best_d] func next_station() -> Array: var best: Node = null var best_d := INF for s in stations: var d: float = distance_ahead(s) if d > 0.0 and d < best_d: best_d = d best = s if best == null: return nearest_station() return [best, best_d]