extends Node3D ## Der Zugverband: Lok + Waggons auf der Strecke, Fahrphysik, ## 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 ## Abstand zwischen zwei Waggons const DRAG := 0.6 const DOCK_RANGE := 3.0 var loco_id := "steam" var consist: Array[String] = [] ## nur die Waggons, ohne Lok var cars: Array = [] ## Car-Knoten, Index 0 = Lok var followers: Array[PathFollow3D] = [] var progress := 0.0 ## Position der Zugspitze auf der Kurve var speed := 0.0 var throttle := 0.0 var braking := false var cargo: Dictionary = {} ## Cargo.Type -> int var stations: Array = [] var docked_at: Node = null var _track var _stand_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 aufbauen ----------------------------------------------------- func rebuild() -> void: for f in followers: f.queue_free() followers.clear() 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 var f := PathFollow3D.new() f.loop = true f.rotation_mode = PathFollow3D.ROTATION_Y f.use_model_front = true f.cubic_interp = true f.add_child(car) _track.path.add_child(f) followers.append(f) cars.append(car) _sync_followers() refresh_cargo_visuals() consist_changed.emit() func total_length() -> float: if cars.is_empty(): return 0.0 var last = cars[cars.size() - 1] return abs(last.center_s - last.length * 0.5) ## Begehbarer Bereich als [hinten, vorne] in Zug-Laengskoordinaten. 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) ## Findet den Waggon zu einer Laengsposition. 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 = min(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 followers[i].global_transform ## Weltposition eines Punktes im Zug (x quer, y hoch, s laengs). func world_point(s: float, x: float, y: float) -> Transform3D: var loc := locate(s) var xf: Transform3D = car_transform(int(loc["index"])) return xf.translated_local(Vector3(x, y, float(loc["local_z"]))) func _sync_followers() -> void: var l: float = _track.length() for i in cars.size(): followers[i].progress = fposmod(progress + cars[i].center_s, l) # --- 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 = fposmod(progress + speed * delta, _track.length()) _sync_followers() _update_dock(delta) ## Nur Anzeige aktualisieren (Clients im Netzspiel). func apply_snapshot(p: float, v: float) -> void: progress = p speed = v _sync_followers() _update_dock(0.0) func _update_dock(delta: float) -> void: var near := nearest_station() var st: Node = near[0] var dist: float = near[1] if st == null: return if speed > 0.15 or dist > DOCK_RANGE: _stand_time = 0.0 docked_at = null return _stand_time += delta docked_at = st ## Abstand eines Bahnhofs zum Zug. 0, solange der Bahnsteig neben dem ## Zug liegt - sonst waere ein langer Zug nie richtig am Bahnsteig. func station_gap(st) -> float: var l: float = _track.length() var behind: float = fposmod(progress - st.progress, l) var len_train: float = total_length() if behind <= len_train: return 0.0 return minf(behind - len_train, l - behind) 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 var l: float = _track.length() for s in stations: var d: float = fposmod(s.progress - progress, l) if d < best_d: best_d = d best = s return [best, best_d]