361 lines
9.6 KiB
GDScript
361 lines
9.6 KiB
GDScript
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]
|