This commit is contained in:
team3
2026-09-16 17:55:38 +02:00
parent c9416ed3c6
commit f012dd7cd5
17 changed files with 878 additions and 160 deletions

View File

@@ -1,6 +1,6 @@
extends Node3D
## Der Zugverband: Lok + Waggons auf der Strecke, Fahrphysik,
## und die Abbildung "Laengsposition im Zug" -> Weltkoordinate.
## 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")
@@ -8,25 +8,33 @@ const Cargo = preload("res://scripts/cargo.gd")
signal consist_changed()
const GAP := 0.75 ## Abstand zwischen zwei Waggons
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] = [] ## nur die Waggons, ohne Lok
var cars: Array = [] ## Car-Knoten, Index 0 = Lok
var followers: Array[PathFollow3D] = []
var consist: Array[String] = []
var cars: Array = []
var progress := 0.0 ## Position der Zugspitze auf der Kurve
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 = {} ## Cargo.Type -> int
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
@@ -68,14 +76,12 @@ func seat_count() -> int:
n += c.seats.size()
return n
# --- Verband aufbauen -----------------------------------------------------
# --- Verband --------------------------------------------------------------
func rebuild() -> void:
for f in followers:
f.queue_free()
followers.clear()
for c in cars:
c.queue_free()
cars.clear()
var ids: Array[String] = [loco_id]
ids.append_array(consist)
var s := 0.0
@@ -85,16 +91,9 @@ func rebuild() -> void:
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)
add_child(car)
cars.append(car)
_sync_followers()
_place_cars(0.0)
refresh_cargo_visuals()
consist_changed.emit()
@@ -102,9 +101,8 @@ 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)
return absf(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
@@ -112,7 +110,6 @@ func walk_range() -> Vector2:
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
@@ -122,7 +119,7 @@ func locate(s: float) -> Dictionary:
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))
var d: float = minf(absf(s - lo), absf(s - hi))
if d < best_d:
best_d = d
best = i
@@ -130,18 +127,95 @@ func locate(s: float) -> Dictionary:
return {"index": best, "local_z": s - c2.center_s, "gangway": true}
func car_transform(i: int) -> Transform3D:
return followers[i].global_transform
return cars[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"])))
return car_transform(int(loc["index"])).translated_local(Vector3(x, y, float(loc["local_z"])))
func _sync_followers() -> void:
var l: float = _track.length()
# --- 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():
followers[i].progress = fposmod(progress + cars[i].center_s, l)
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 ---------------------------------------------------------------
@@ -188,39 +262,80 @@ func drive(delta: float) -> void:
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()
progress += speed * delta
_advance()
_place_cars(delta)
_update_dock(delta)
## Nur Anzeige aktualisieren (Clients im Netzspiel).
func apply_snapshot(p: float, v: float) -> void:
progress = p
## 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
_sync_followers()
_update_dock(0.0)
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]
var dist: float = near[1]
if st == null:
return
if speed > 0.15 or dist > DOCK_RANGE:
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. 0, solange der Bahnsteig neben dem
## Zug liegt - sonst waere ein langer Zug nie richtig am Bahnsteig.
## Abstand eines Bahnhofs zum Zug entlang der Strecke. 0, solange der
## Bahnsteig neben dem Zug liegt.
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:
var ahead := distance_ahead(st)
if ahead == 0.0:
return 0.0
return minf(behind - len_train, l - behind)
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
@@ -235,10 +350,11 @@ func nearest_station() -> Array:
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:
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]