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

@@ -17,6 +17,7 @@ signal ready_for_play()
var sounds: Dictionary = {} var sounds: Dictionary = {}
var music_on := true var music_on := true
var loaded := false var loaded := false
var music_loaded := false
var clacks := 0 ## Zähler, damit sich das testen lässt var clacks := 0 ## Zähler, damit sich das testen lässt
var last_played := "" ## zuletzt gestarteter Klang var last_played := "" ## zuletzt gestarteter Klang
@@ -63,28 +64,32 @@ func _exit_tree() -> void:
## Läuft im Thread: nur Rechnen, keine Godot-Knoten anfassen. ## Läuft im Thread: nur Rechnen, keine Godot-Knoten anfassen.
func _build_async() -> void: func _build_async() -> void:
var pcm: Dictionary = Sfx.build_all() call_deferred("_publish_sfx", Sfx.build_all())
pcm["music"] = Sfx.build_music() call_deferred("_publish_music", Sfx.build_music())
call_deferred("_finish_build", pcm)
func _finish_build(pcm: Dictionary) -> void: ## Die kurzen Klänge sind schnell fertig - sie gehen zuerst live.
if _thread != null: func _publish_sfx(pcm: Dictionary) -> void:
_thread.wait_to_finish()
_thread = null
for name in pcm.keys(): for name in pcm.keys():
var loop: bool = name == "music" or LOOPS.has(name) sounds[name] = Synth.to_stream(pcm[name], LOOPS.has(name))
sounds[name] = Synth.to_stream(pcm[name], loop)
_music.stream = sounds["music"]
_music.volume_db = MUSIC_DB if music_on else -80.0
_music.play()
_rumble.stream = sounds["rumble"] _rumble.stream = sounds["rumble"]
_rumble.play() _rumble.play()
_squeal.stream = sounds["squeal"] _squeal.stream = sounds["squeal"]
_squeal.play() _squeal.play()
loaded = true loaded = true
print("[audio] %d Klänge in %d ms erzeugt" % [sounds.size(), Time.get_ticks_msec() - _t0]) print("[audio] %d Klänge in %d ms" % [sounds.size(), Time.get_ticks_msec() - _t0])
ready_for_play.emit() ready_for_play.emit()
func _publish_music(pcm: PackedByteArray) -> void:
if _thread != null:
_thread.wait_to_finish()
_thread = null
sounds["music"] = Synth.to_stream(pcm, true)
_music.stream = sounds["music"]
_music.volume_db = MUSIC_DB if music_on else -80.0
_music.play()
music_loaded = true
print("[audio] Musik in %d ms" % (Time.get_ticks_msec() - _t0))
func toggle_music() -> bool: func toggle_music() -> bool:
music_on = not music_on music_on = not music_on
_music.volume_db = MUSIC_DB if music_on else -80.0 _music.volume_db = MUSIC_DB if music_on else -80.0

View File

@@ -11,6 +11,7 @@ var distance := 46.0
var _cam: Camera3D var _cam: Camera3D
var _focus := Vector3.ZERO var _focus := Vector3.ZERO
var blocked := false ## zuletzt an einem Hindernis abgefangen
func _ready() -> void: func _ready() -> void:
_cam = Camera3D.new() _cam = Camera3D.new()
@@ -51,11 +52,26 @@ func _process(delta: float) -> void:
if target: if target:
_focus = _focus.lerp(target.global_position, clampf(delta * 5.0, 0.0, 1.0)) _focus = _focus.lerp(target.global_position, clampf(delta * 5.0, 0.0, 1.0))
blocked = false
position = _focus position = _focus
var dir := Vector3(0, 0, 1).rotated(Vector3.RIGHT, pitch).rotated(Vector3.UP, yaw) var dir := Vector3(0, 0, 1).rotated(Vector3.RIGHT, pitch).rotated(Vector3.UP, yaw)
_cam.position = dir * distance var wanted: Vector3 = dir * distance
_cam.position = _clip_to_view(wanted)
_cam.look_at(global_position + Vector3(0, 2.5, 0), Vector3.UP) _cam.look_at(global_position + Vector3(0, 2.5, 0), Vector3.UP)
## Zieht die Kamera vor ein Hindernis, statt hindurchzuschauen.
func _clip_to_view(local_target: Vector3) -> Vector3:
var from: Vector3 = global_position + Vector3(0, 2.5, 0)
var to: Vector3 = global_position + local_target
var space := get_world_3d().direct_space_state
var q := PhysicsRayQueryParameters3D.create(from, to, 2)
var hit := space.intersect_ray(q)
if hit.is_empty():
return local_target
var p: Vector3 = hit["position"] + (from - hit["position"]).normalized() * 0.7
blocked = true
return p - global_position
func snap_to(p: Vector3) -> void: func snap_to(p: Vector3) -> void:
_focus = p _focus = p
position = p position = p

View File

@@ -92,4 +92,15 @@ static func house(wall: Color, roof: Color, w: float, d: float, h: float, chimne
if chimney: if chimney:
n.add_child(_mi(box(Vector3(w * 0.16, h * 0.7, w * 0.16)), roof.darkened(0.3), n.add_child(_mi(box(Vector3(w * 0.16, h * 0.7, w * 0.16)), roof.darkened(0.3),
Vector3(w * 0.25, h + h * 0.45, 0))) Vector3(w * 0.25, h + h * 0.45, 0)))
# Kollision nur fuer die Aussenkamera, damit sie nicht durch Waende schaut
var body := StaticBody3D.new()
body.collision_layer = 2
body.collision_mask = 0
var shape := CollisionShape3D.new()
var bs := BoxShape3D.new()
bs.size = Vector3(w * 1.14, h * 1.6, d * 1.06)
shape.shape = bs
shape.position = Vector3(0, h * 0.8, 0)
body.add_child(shape)
n.add_child(body)
return n return n

View File

@@ -22,18 +22,24 @@ const MenuScript = preload("res://scripts/menu.gd")
enum Mode { MENU, SOLO, HOST, CLIENT } enum Mode { MENU, SOLO, HOST, CLIENT }
const TRACK_POINTS := [ ## Zwei Weichenpunkte, drei Abschnitte: Talstrecke und Bergstrecke fuehren
Vector2(-139, -77), Vector2(-53, -101), Vector2(38, -86), Vector2(110, -48), ## beide von J1 nach J2, die Heimstrecke zurueck.
Vector2(144, 14), Vector2(101, 77), Vector2(14, 96), Vector2(-62, 77), const J1 := Vector2(-139, -77)
Vector2(-101, 34), Vector2(-154, -14), const J2 := Vector2(144, 14)
] const T1 := Vector2(101, -87) ## Fahrtrichtung an J1
const T2 := Vector2(-9, 125) ## Fahrtrichtung an J2
const SEG_TAL := [J1, Vector2(-53, -101), Vector2(38, -86), Vector2(110, -48), J2]
const SEG_BERG := [J1, Vector2(-85, -38), Vector2(-20, -14), Vector2(58, -12), Vector2(112, -24), J2]
const SEG_HEIM := [J2, Vector2(101, 77), Vector2(14, 96), Vector2(-62, 77), Vector2(-101, 34), Vector2(-154, -14), J1]
## name, abschnitt, anteil, produziert, akzeptiert, seite, wand, dach, schornstein
const STATION_DEFS := [ const STATION_DEFS := [
["Waldkirch", 0.04, Cargo.Type.WOOD, [Cargo.Type.GRAIN], 1.0, "c9a06a", "8a4b2f", false], ["Waldkirch", 0, 0.10, Cargo.Type.WOOD, [Cargo.Type.GRAIN], 1.0, "c9a06a", "8a4b2f", false],
["Kohlberg", 0.22, Cargo.Type.COAL, [], -1.0, "6f6a63", "40393a", true], ["Kohlberg", 0, 0.55, Cargo.Type.COAL, [], -1.0, "6f6a63", "40393a", true],
["Eisenwerk", 0.40, -1, [Cargo.Type.COAL, Cargo.Type.WOOD], 1.0, "9aa0a6", "55606b", true], ["Eisenwerk", 0, 0.88, -1, [Cargo.Type.COAL, Cargo.Type.WOOD], 1.0, "9aa0a6", "55606b", true],
["Grüntal", 0.60, Cargo.Type.GRAIN, [], -1.0, "e8ddc4", "b4562f", false], ["Grüntal", 2, 0.33, Cargo.Type.GRAIN, [], -1.0, "e8ddc4", "b4562f", false],
["Ahornstadt", 0.80, -1, [Cargo.Type.GRAIN, Cargo.Type.WOOD], 1.0, "f0e6d2", "a83b30", true], ["Ahornstadt", 2, 0.67, -1, [Cargo.Type.GRAIN, Cargo.Type.WOOD], 1.0, "f0e6d2", "a83b30", true],
["Bergblick", 1, 0.50, -1, [], -1.0, "e6dcc2", "5c7a4e", false],
] ]
const WORLD_SEED := 20260828 const WORLD_SEED := 20260828
@@ -42,7 +48,21 @@ const BOARD_INTERVAL := 0.45
const LOAD_INTERVAL := 0.35 const LOAD_INTERVAL := 0.35
const DRINK_TIP := 14 const DRINK_TIP := 14
const START_MONEY := 1500 const START_MONEY := 1500
const POSE_HZ := 15.0 const POSE_HZ := 20.0
const SAVE_PATH := "user://save.json"
const AUTOSAVE_SEC := 20.0
## Meilensteine. "kind" sagt, welcher Zähler zählt.
const GOALS := [
{"id": "tickets10", "text": "10 Tickets kontrollieren", "kind": "tickets", "value": 10, "reward": 250},
{"id": "cargo30", "text": "30 Einheiten Fracht liefern", "kind": "cargo", "value": 30, "reward": 400},
{"id": "dining", "text": "Einen Speisewagen anhängen", "kind": "dining", "value": 1, "reward": 300},
{"id": "berg", "text": "Bergblick anfahren", "kind": "visit", "value": 1, "reward": 500},
{"id": "rep15", "text": "Ansehen 15 erreichen", "kind": "rep", "value": 15, "reward": 800},
{"id": "tickets60", "text": "60 Tickets kontrollieren", "kind": "tickets", "value": 60, "reward": 900},
{"id": "rep40", "text": "Ansehen 40 erreichen", "kind": "rep", "value": 40, "reward": 1500},
{"id": "final", "text": "Elektro-Express mit 5 Wagen und Ansehen 60", "kind": "final", "value": 60, "reward": 5000},
]
const STATE_HZ := 4.0 const STATE_HZ := 4.0
var mode := Mode.MENU var mode := Mode.MENU
@@ -53,6 +73,13 @@ var money := START_MONEY
var reputation := 0 var reputation := 0
var upgrades: Dictionary = {} var upgrades: Dictionary = {}
var delivered_units := 0 var delivered_units := 0
var tickets_checked := 0
var visited: Dictionary = {} ## Bahnhofsname -> true
var goals_done: Dictionary = {} ## Ziel-ID -> true
var won := false
var my_name := "Crew"
var _autosave := AUTOSAVE_SEC
var _load_on_begin := false
var track: Node3D var track: Node3D
var world: Node3D var world: Node3D
@@ -83,6 +110,7 @@ func _ready() -> void:
menu = MenuScript.new() menu = MenuScript.new()
add_child(menu) add_child(menu)
menu.solo_chosen.connect(_start_solo) menu.solo_chosen.connect(_start_solo)
menu.continue_chosen.connect(_start_continue)
menu.host_chosen.connect(_start_host) menu.host_chosen.connect(_start_host)
menu.join_chosen.connect(_start_join) menu.join_chosen.connect(_start_join)
multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_connected.connect(_on_peer_connected)
@@ -104,6 +132,10 @@ func _start_solo() -> void:
my_id = 1 my_id = 1
_begin() _begin()
func _start_continue() -> void:
_load_on_begin = true
_start_solo()
func _start_host(port: int) -> void: func _start_host(port: int) -> void:
var peer := ENetMultiplayerPeer.new() var peer := ENetMultiplayerPeer.new()
var err := peer.create_server(port, 8) var err := peer.create_server(port, 8)
@@ -140,6 +172,7 @@ func _on_server_gone() -> void:
hud.toast("Verbindung zum Host verloren.", Color("ff9a8a")) hud.toast("Verbindung zum Host verloren.", Color("ff9a8a"))
func _begin() -> void: func _begin() -> void:
my_name = menu.player_name()
_rng.seed = WORLD_SEED _rng.seed = WORLD_SEED
_build_environment() _build_environment()
_build_track() _build_track()
@@ -152,10 +185,18 @@ func _begin() -> void:
_build_ui() _build_ui()
audio = AudioScript.new() audio = AudioScript.new()
add_child(audio) add_child(audio)
players[my_id] = {"s": player.s, "x": player.x, "yaw": 0.0, "carrying": 0, "avatar": null} players[my_id] = _new_player_entry(my_name)
menu.hide_menu() menu.hide_menu()
player.capture_mouse(true) player.capture_mouse(true)
started = true started = true
if _load_on_begin:
load_game()
if mode == Mode.CLIENT:
rpc_id(1, "net_hello", my_name)
func _new_player_entry(pname: String) -> Dictionary:
return {"s": 0.0, "x": 0.0, "yaw": 0.0, "carrying": 0, "avatar": null,
"name": pname, "tpos": Vector3.ZERO, "tyaw": 0.0}
# --- Aufbau --------------------------------------------------------------- # --- Aufbau ---------------------------------------------------------------
@@ -193,22 +234,26 @@ func _build_environment() -> void:
func _build_track() -> void: func _build_track() -> void:
track = TrackScript.new() track = TrackScript.new()
add_child(track) add_child(track)
var pts := PackedVector2Array() var tal: int = track.add_segment("Talstrecke", SEG_TAL, T1, T2)
for p in TRACK_POINTS: var berg: int = track.add_segment("Bergstrecke", SEG_BERG, T1, T2)
pts.append(p) var heim: int = track.add_segment("Heimstrecke", SEG_HEIM, T2, T1)
track.build(pts) track.connect_segments(heim, tal) # Nachfolger 0 = links
track.connect_segments(heim, berg) # Nachfolger 1 = rechts
track.connect_segments(tal, heim)
track.connect_segments(berg, heim)
track.finish()
func _build_stations() -> void: func _build_stations() -> void:
var l: float = track.length()
for d in STATION_DEFS: for d in STATION_DEFS:
var st := StationScript.new() var st := StationScript.new()
add_child(st) add_child(st)
var acc: Array[int] = [] var acc: Array[int] = []
for t in d[3]: for t in d[4]:
acc.append(int(t)) acc.append(int(t))
st.setup(String(d[0]), l * float(d[1]), int(d[2]), acc) var seg_id := int(d[1])
st.build_visuals(track.sample(st.progress), float(d[4]), st.setup(String(d[0]), seg_id, track.seg_length(seg_id) * float(d[2]), int(d[3]), acc)
Color(String(d[5])), Color(String(d[6])), bool(d[7])) st.build_visuals(track.sample(seg_id, st.progress), float(d[5]),
Color(String(d[6])), Color(String(d[7])), bool(d[8]))
if st.produces >= 0: if st.produces >= 0:
st.stock = 8.0 st.stock = 8.0
stations.append(st) stations.append(st)
@@ -223,8 +268,9 @@ func _build_train() -> void:
## Stellt den Zug mittig an einen Bahnsteig. ## Stellt den Zug mittig an einen Bahnsteig.
func _park_at(st) -> void: func _park_at(st) -> void:
train.progress = fposmod(st.progress + train.total_length() * 0.5, track.length()) train.speed = 0.0
train.apply_snapshot(train.progress, 0.0) train.place_head(st.seg, st.progress + train.total_length() * 0.5)
train._update_dock(0.0)
func _build_player() -> void: func _build_player() -> void:
player = PlayerScript.new() player = PlayerScript.new()
@@ -234,8 +280,8 @@ func _build_player() -> void:
cam_rig = CameraRig.new() cam_rig = CameraRig.new()
add_child(cam_rig) add_child(cam_rig)
cam_rig.target = train.followers[0] cam_rig.target = train.cars[0]
cam_rig.snap_to(track.sample(train.progress).origin) cam_rig.snap_to(train.cars[0].global_position)
player.camera.current = true player.camera.current = true
func _build_ui() -> void: func _build_ui() -> void:
@@ -257,6 +303,11 @@ func _process(delta: float) -> void:
_spawn_passengers(delta) _spawn_passengers(delta)
_update_passengers(delta) _update_passengers(delta)
_handle_station_stop(delta) _handle_station_stop(delta)
_track_visits()
_autosave -= delta
if _autosave <= 0.0:
_autosave = AUTOSAVE_SEC
save_game()
_handle_hold_actions(delta) _handle_hold_actions(delta)
_update_audio(delta) _update_audio(delta)
_sync_out(delta) _sync_out(delta)
@@ -269,22 +320,38 @@ func _physics_process(delta: float) -> void:
var driving: bool = driver_id == my_id and not player.input_locked var driving: bool = driver_id == my_id and not player.input_locked
var th := 0.0 var th := 0.0
var br := false var br := false
var sw: int = train.switch_choice
if driving: if driving:
th = 1.0 if Input.is_key_pressed(KEY_W) else 0.0 th = 1.0 if Input.is_key_pressed(KEY_W) else 0.0
br = Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_SPACE) br = Input.is_key_pressed(KEY_S) or Input.is_key_pressed(KEY_SPACE)
if Input.is_key_pressed(KEY_A):
sw = 0
elif Input.is_key_pressed(KEY_D):
sw = 1
if is_server(): if is_server():
if driver_id == my_id: if driver_id == my_id:
train.throttle = th train.throttle = th
train.braking = br train.braking = br
train.switch_choice = sw
train.drive(delta) train.drive(delta)
elif driving: else:
rpc_id(1, "net_controls", th, br) if driving:
rpc_id(1, "net_controls", th, br, sw)
train.client_tick(delta)
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST and started and is_server():
save_game()
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if not started: if not started:
return return
if event is InputEventKey and event.pressed and not event.echo: if event is InputEventKey and event.pressed and not event.echo:
match event.keycode: match event.keycode:
KEY_F5:
if is_server():
save_game()
hud.toast("Gespeichert", Color("8fd8ff"))
KEY_H: KEY_H:
if driver_id == my_id: if driver_id == my_id:
request("whistle") request("whistle")
@@ -358,13 +425,13 @@ func _sync_out(delta: float) -> void:
players[my_id]["x"] = player.x players[my_id]["x"] = player.x
players[my_id]["yaw"] = player.yaw players[my_id]["yaw"] = player.yaw
rpc("net_poses", _pose_payload()) rpc("net_poses", _pose_payload())
rpc("net_train", train.seg, train.progress, train.speed, train.switch_choice, train.history)
else: else:
rpc_id(1, "net_pose", player.s, player.x, player.yaw) rpc_id(1, "net_pose", player.s, player.x, player.yaw)
if is_server(): if is_server():
_state_timer -= delta _state_timer -= delta
if _state_timer <= 0.0: if _state_timer <= 0.0:
_state_timer = 1.0 / STATE_HZ _state_timer = 1.0 / STATE_HZ
rpc("net_train", train.progress, train.speed)
rpc("net_state", _state_payload()) rpc("net_state", _state_payload())
rpc("net_passengers", _passenger_payload()) rpc("net_passengers", _passenger_payload())
@@ -379,10 +446,15 @@ func _state_payload() -> Dictionary:
var stock: Array = [] var stock: Array = []
for st in stations: for st in stations:
stock.append(st.stock) stock.append(st.stock)
var names := {}
for id in players.keys():
names[int(id)] = String(players[id]["name"])
return { return {
"money": money, "rep": reputation, "upg": upgrades.keys(), "money": money, "rep": reputation, "upg": upgrades.keys(),
"driver": driver_id, "cargo": train.cargo.duplicate(), "stock": stock, "driver": driver_id, "cargo": train.cargo.duplicate(), "stock": stock,
"loco": train.loco_id, "consist": train.consist.duplicate(), "loco": train.loco_id, "consist": train.consist.duplicate(),
"names": names, "goals": goals_done.keys(), "won": won,
"tickets": tickets_checked, "delivered": delivered_units,
} }
func _passenger_payload() -> Array: func _passenger_payload() -> Array:
@@ -402,7 +474,7 @@ func _passenger_payload() -> Array:
func net_pose(s: float, x: float, yaw: float) -> void: func net_pose(s: float, x: float, yaw: float) -> void:
var id := multiplayer.get_remote_sender_id() var id := multiplayer.get_remote_sender_id()
if not players.has(id): if not players.has(id):
players[id] = {"s": s, "x": x, "yaw": yaw, "carrying": 0, "avatar": null} players[id] = _new_player_entry("Crew %d" % id)
players[id]["s"] = s players[id]["s"] = s
players[id]["x"] = x players[id]["x"] = x
players[id]["yaw"] = yaw players[id]["yaw"] = yaw
@@ -414,7 +486,7 @@ func net_poses(list: Array) -> void:
var id := int(e[0]) var id := int(e[0])
seen[id] = true seen[id] = true
if not players.has(id): if not players.has(id):
players[id] = {"s": 0.0, "x": 0.0, "yaw": 0.0, "carrying": 0, "avatar": null} players[id] = _new_player_entry("Crew %d" % id)
if id != my_id: if id != my_id:
players[id]["s"] = float(e[1]) players[id]["s"] = float(e[1])
players[id]["x"] = float(e[2]) players[id]["x"] = float(e[2])
@@ -427,8 +499,8 @@ func net_poses(list: Array) -> void:
_drop_player(int(id)) _drop_player(int(id))
@rpc("authority", "call_remote", "unreliable_ordered") @rpc("authority", "call_remote", "unreliable_ordered")
func net_train(p: float, v: float) -> void: func net_train(seg_id: int, p: float, v: float, sw: int, hist: Array) -> void:
train.apply_snapshot(p, v) train.apply_snapshot(seg_id, p, v, sw, hist)
@rpc("authority", "call_remote", "reliable") @rpc("authority", "call_remote", "reliable")
func net_state(d: Dictionary) -> void: func net_state(d: Dictionary) -> void:
@@ -438,6 +510,16 @@ func net_state(d: Dictionary) -> void:
for k in d["upg"]: for k in d["upg"]:
upgrades[String(k)] = true upgrades[String(k)] = true
driver_id = int(d["driver"]) driver_id = int(d["driver"])
for id in d["names"].keys():
if not players.has(int(id)):
players[int(id)] = _new_player_entry(String(d["names"][id]))
players[int(id)]["name"] = String(d["names"][id])
goals_done.clear()
for g in d["goals"]:
goals_done[String(g)] = true
won = bool(d["won"])
tickets_checked = int(d["tickets"])
delivered_units = int(d["delivered"])
if driver_id != my_id and player.mode == PlayerScript.Mode.DRIVE: if driver_id != my_id and player.mode == PlayerScript.Mode.DRIVE:
player.mode = PlayerScript.Mode.WALK player.mode = PlayerScript.Mode.WALK
elif driver_id == my_id: elif driver_id == my_id:
@@ -498,13 +580,25 @@ func net_sfx(name: String, at_loco: bool, db: float) -> void:
_play_sfx(name, at_loco, db) _play_sfx(name, at_loco, db)
@rpc("any_peer", "call_remote", "unreliable_ordered") @rpc("any_peer", "call_remote", "unreliable_ordered")
func net_controls(th: float, br: bool) -> void: func net_controls(th: float, br: bool, sw: int) -> void:
if not is_server(): if not is_server():
return return
if multiplayer.get_remote_sender_id() != driver_id: if multiplayer.get_remote_sender_id() != driver_id:
return return
train.throttle = th train.throttle = th
train.braking = br train.braking = br
train.switch_choice = sw
@rpc("any_peer", "call_remote", "reliable")
func net_hello(pname: String) -> void:
if not is_server():
return
var id := multiplayer.get_remote_sender_id()
if not players.has(id):
players[id] = _new_player_entry(pname)
players[id]["name"] = pname
_broadcast_toast("%s ist an Bord" % pname, Color("8fd8ff"))
_push_state()
@rpc("any_peer", "call_remote", "reliable") @rpc("any_peer", "call_remote", "reliable")
func net_request(action: String, a: String, b: int, c: int) -> void: func net_request(action: String, a: String, b: int, c: int) -> void:
@@ -549,17 +643,17 @@ func _make_passenger(pid: int):
func _on_peer_connected(id: int) -> void: func _on_peer_connected(id: int) -> void:
if not is_server() or not started: if not is_server() or not started:
return return
players[id] = {"s": 0.0, "x": 0.0, "yaw": 0.0, "carrying": 0, "avatar": null} players[id] = _new_player_entry("Crew %d" % id)
rpc_id(id, "net_state", _state_payload()) rpc_id(id, "net_state", _state_payload())
rpc_id(id, "net_passengers", _passenger_payload()) rpc_id(id, "net_passengers", _passenger_payload())
hud.toast("Spieler %d ist an Bord" % id, Color("8fd8ff"))
func _on_peer_disconnected(id: int) -> void: func _on_peer_disconnected(id: int) -> void:
var pname: String = String(players.get(id, {}).get("name", "Crew %d" % id))
_drop_player(id) _drop_player(id)
if driver_id == id: if driver_id == id:
driver_id = 0 driver_id = 0
if hud: if hud:
hud.toast("Spieler %d hat den Zug verlassen" % id, Color("ffd978")) hud.toast("%s hat den Zug verlassen" % pname, Color("ffd978"))
func _drop_player(id: int) -> void: func _drop_player(id: int) -> void:
if id == my_id or not players.has(id): if id == my_id or not players.has(id):
@@ -576,16 +670,22 @@ func _update_avatars() -> void:
var e: Dictionary = players[id] var e: Dictionary = players[id]
if e["avatar"] == null: if e["avatar"] == null:
var av := AvatarScript.new() var av := AvatarScript.new()
av.build(int(id), "Crew %d" % int(id)) av.build(int(id), String(e["name"]))
add_child(av) add_child(av)
e["avatar"] = av e["avatar"] = av
var av2 = e["avatar"] var av2 = e["avatar"]
var loc: Dictionary = train.locate(float(e["s"])) var loc: Dictionary = train.locate(float(e["s"]))
var xf: Transform3D = train.car_transform(int(loc["index"])) var xf: Transform3D = train.car_transform(int(loc["index"]))
av2.global_position = xf * Vector3(float(e["x"]), 1.10, float(loc["local_z"])) # Zielpose im Wagenraum; Bewegung wird geglättet, die Wagenlage nicht
av2.global_rotation = Vector3(0, xf.basis.get_euler().y + float(e["yaw"]), 0) var tp: Vector3 = Vector3(float(e["x"]), 1.10, float(loc["local_z"]))
var ty: float = float(e["yaw"])
var k: float = clampf(get_process_delta_time() * 14.0, 0.0, 1.0)
e["tpos"] = (e["tpos"] as Vector3).lerp(tp, k) if e["tpos"] != Vector3.ZERO else tp
e["tyaw"] = lerp_angle(float(e["tyaw"]), ty, k)
av2.global_position = xf * (e["tpos"] as Vector3)
av2.global_rotation = Vector3(0, xf.basis.get_euler().y + float(e["tyaw"]), 0)
av2.set_carrying(int(e["carrying"])) av2.set_carrying(int(e["carrying"]))
av2.set_label("Crew %d%s" % [int(id), " (Führerstand)" if int(id) == driver_id else ""]) av2.set_label("%s%s" % [String(e["name"]), " (Führerstand)" if int(id) == driver_id else ""])
# --- Fahrgäste ----------------------------------------------------------- # --- Fahrgäste -----------------------------------------------------------
@@ -613,13 +713,12 @@ func spawn_passenger(idx: int, forced_dest := -1) -> bool:
p.fare = _fare(idx, dest) p.fare = _fare(idx, dest)
var spot: Vector3 = st.waiting_spots[st.waiting.size()] var spot: Vector3 = st.waiting_spots[st.waiting.size()]
p.global_position = spot p.global_position = spot
p.global_rotation = Vector3(0, track.sample(st.progress).basis.get_euler().y + PI * 0.5 * st.side, 0) p.global_rotation = Vector3(0, track.sample(st.seg, st.progress).basis.get_euler().y + PI * 0.5 * st.side, 0)
st.waiting.append(p) st.waiting.append(p)
return true return true
func _fare(a: int, b: int) -> int: func _fare(a: int, b: int) -> int:
var l: float = track.length() var d: float = stations[a].global_position.distance_to(stations[b].global_position) * 1.6
var d: float = fposmod(stations[b].progress - stations[a].progress, l)
var bonus := 1.0 var bonus := 1.0
for c in train.cars: for c in train.cars:
if c.id == "saloon": if c.id == "saloon":
@@ -749,6 +848,7 @@ func _do_request(peer: int, action: String, a: String, b: int, c: int) -> void:
"buy_loco": _srv_buy_loco(peer, a) "buy_loco": _srv_buy_loco(peer, a)
"buy_car": _srv_buy_car(peer, a) "buy_car": _srv_buy_car(peer, a)
"sell_car": _srv_sell_car(peer, b) "sell_car": _srv_sell_car(peer, b)
"sell_loco": _srv_sell_loco(peer)
"buy_upgrade": _srv_buy_upgrade(peer, a) "buy_upgrade": _srv_buy_upgrade(peer, a)
"whistle": _srv_whistle(peer) "whistle": _srv_whistle(peer)
@@ -796,7 +896,7 @@ func _carrying(peer: int) -> int:
func _set_carrying(peer: int, n: int) -> void: func _set_carrying(peer: int, n: int) -> void:
if not players.has(peer): if not players.has(peer):
players[peer] = {"s": 0.0, "x": 0.0, "yaw": 0.0, "carrying": 0, "avatar": null} players[peer] = _new_player_entry("Crew %d" % peer)
players[peer]["carrying"] = n players[peer]["carrying"] = n
if peer == my_id: if peer == my_id:
player.carrying = n player.carrying = n
@@ -836,6 +936,7 @@ func _srv_use_seat(peer: int, car_index: int, seat_index: int) -> void:
func _check_ticket(p, peer: int) -> void: func _check_ticket(p, peer: int) -> void:
p.ticket_checked = true p.ticket_checked = true
tickets_checked += 1
money += p.fare money += p.fare
_toast_to(peer, "Ticket nach %s +%d €" % [stations[p.dest_station].station_name, p.fare], _toast_to(peer, "Ticket nach %s +%d €" % [stations[p.dest_station].station_name, p.fare],
Color("aef2a0")) Color("aef2a0"))
@@ -849,6 +950,7 @@ func _check_all_tickets(peer: int) -> void:
for p in passengers: for p in passengers:
if p.state == PassengerScript.State.SEATED and not p.ticket_checked: if p.state == PassengerScript.State.SEATED and not p.ticket_checked:
p.ticket_checked = true p.ticket_checked = true
tickets_checked += 1
sum += p.fare sum += p.fare
n += 1 n += 1
p._refresh_label() p._refresh_label()
@@ -899,9 +1001,54 @@ func _srv_transfer_one(peer: int) -> void:
_push_state() _push_state()
func _push_state() -> void: func _push_state() -> void:
if is_server():
_check_goals()
if mode == Mode.HOST: if mode == Mode.HOST:
rpc("net_state", _state_payload()) rpc("net_state", _state_payload())
func _track_visits() -> void:
if train.docked_at != null and train.speed < 0.1:
var nm: String = train.docked_at.station_name
if not visited.has(nm):
visited[nm] = true
_check_goals()
## Aktuelles Ziel und Fortschritt, für das HUD.
func current_goal() -> Dictionary:
for g in GOALS:
if not goals_done.has(String(g["id"])):
return {"text": String(g["text"]), "progress": _goal_progress(g), "target": int(g["value"])}
return {}
func _goal_progress(g: Dictionary) -> int:
match String(g["kind"]):
"tickets": return tickets_checked
"cargo": return delivered_units
"rep": return reputation
"dining": return 1 if train.has_dining() else 0
"visit": return 1 if visited.has("Bergblick") else 0
"final": return reputation if train.loco_id == "express" and train.consist.size() >= 5 else 0
return 0
func _check_goals() -> void:
for g in GOALS:
var id := String(g["id"])
if goals_done.has(id):
continue
if _goal_progress(g) < int(g["value"]):
break # Ziele kommen der Reihe nach
goals_done[id] = true
money += int(g["reward"])
if id == "final":
won = true
_broadcast_toast("Linie ausgebaut! Ihr habt es geschafft. +%d €" % int(g["reward"]),
Color("ffd978"))
else:
_broadcast_toast("Ziel erreicht: %s +%d €" % [String(g["text"]), int(g["reward"])],
Color("ffd978"))
_broadcast_sfx("bell", false, -8.0)
save_game()
# --- Laden ---------------------------------------------------------------- # --- Laden ----------------------------------------------------------------
func _open_shop() -> void: func _open_shop() -> void:
@@ -953,6 +1100,18 @@ func _srv_sell_car(peer: int, index: int) -> void:
_broadcast_sfx("coin", false, -5.0) _broadcast_sfx("coin", false, -5.0)
_push_state() _push_state()
## Die Lok geht zurück ans Depot, der Dampf-Veteran springt ein.
func _srv_sell_loco(peer: int) -> void:
if train.loco_id == "steam":
return
var d: Dictionary = Defs.LOCOS[train.loco_id]
money += int(d["sell"])
train.loco_id = "steam"
_rebuild_train()
_broadcast_toast("Verkauft: %s +%d €" % [String(d["name"]), int(d["sell"])], Color("ffd978"))
_broadcast_sfx("coin", false, -5.0)
_push_state()
func _srv_buy_upgrade(peer: int, id: String) -> void: func _srv_buy_upgrade(peer: int, id: String) -> void:
if not Defs.UPGRADES.has(id): if not Defs.UPGRADES.has(id):
return return
@@ -986,6 +1145,76 @@ func buy_upgrade(id: String) -> bool:
request("buy_upgrade", id) request("buy_upgrade", id)
return upgrades.size() != before return upgrades.size() != before
# --- Spielstand -------------------------------------------------------------
func save_game() -> void:
if not is_server():
return
var stock: Array = []
for st in stations:
stock.append(st.stock)
var data := {
"money": money, "rep": reputation, "upg": upgrades.keys(),
"delivered": delivered_units, "tickets": tickets_checked,
"visited": visited.keys(), "goals": goals_done.keys(), "won": won,
"loco": train.loco_id, "consist": train.consist,
"seg": train.seg, "progress": train.progress, "cargo": train.cargo,
"stock": stock,
}
var f := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify(data))
f.close()
func load_game() -> bool:
if not FileAccess.file_exists(SAVE_PATH):
return false
var f := FileAccess.open(SAVE_PATH, FileAccess.READ)
if f == null:
return false
var parsed = JSON.parse_string(f.get_as_text())
f.close()
if not (parsed is Dictionary):
return false
var d: Dictionary = parsed
money = int(d.get("money", START_MONEY))
reputation = int(d.get("rep", 0))
upgrades.clear()
for k in d.get("upg", []):
upgrades[String(k)] = true
delivered_units = int(d.get("delivered", 0))
tickets_checked = int(d.get("tickets", 0))
visited.clear()
for k in d.get("visited", []):
visited[String(k)] = true
goals_done.clear()
for k in d.get("goals", []):
goals_done[String(k)] = true
won = bool(d.get("won", false))
var loco := String(d.get("loco", "steam"))
if Defs.LOCOS.has(loco):
train.loco_id = loco
var typed: Array[String] = []
for c in d.get("consist", []):
if Defs.CARS.has(String(c)):
typed.append(String(c))
train.consist = typed
train.cargo.clear()
var cargo: Dictionary = d.get("cargo", {})
for k in cargo.keys():
train.cargo[int(k)] = int(cargo[k])
var stock: Array = d.get("stock", [])
for i in mini(stock.size(), stations.size()):
stations[i].stock = float(stock[i])
_rebuild_train()
var seg_id := int(d.get("seg", 0))
if seg_id >= 0 and seg_id < track.segments.size():
train.place_head(seg_id, float(d.get("progress", 0.0)))
train._update_dock(0.0)
hud.toast("Spielstand geladen", Color("8fd8ff"))
return true
func _rebuild_train() -> void: func _rebuild_train() -> void:
# Sitzende Gaeste zwischenparken, sonst verschwinden sie mit dem alten Wagen. # Sitzende Gaeste zwischenparken, sonst verschwinden sie mit dem alten Wagen.
var riders: Array = [] var riders: Array = []
@@ -1005,7 +1234,7 @@ func _rebuild_train() -> void:
p.seat_index = int(seat["index"]) p.seat_index = int(seat["index"])
train.cars[p.seat_car].seats[p.seat_index]["occupant"] = p train.cars[p.seat_car].seats[p.seat_index]["occupant"] = p
_finish_boarding(p) _finish_boarding(p)
cam_rig.target = train.followers[0] cam_rig.target = train.cars[0]
player.snap_to_cab() player.snap_to_cab()
if driver_id == my_id: if driver_id == my_id:
player.mode = PlayerScript.Mode.DRIVE player.mode = PlayerScript.Mode.DRIVE

View File

@@ -129,7 +129,7 @@ func refresh(game) -> void:
_prompt.text = String(PROMPTS.get(String(h["kind"]), "")) _prompt.text = String(PROMPTS.get(String(h["kind"]), ""))
if game.player.mode == PlayerScript.Mode.DRIVE: if game.player.mode == PlayerScript.Mode.DRIVE:
_drive.text = "W Gas S Bremse %3.0f km/h" % t.speed_kmh() _drive.text = "W Gas S Bremse A/D Weiche %3.0f km/h" % t.speed_kmh()
_drive.add_theme_color_override("font_color", _drive.add_theme_color_override("font_color",
Color("aef2a0") if t.speed > 0.2 else Color("ffd978")) Color("aef2a0") if t.speed > 0.2 else Color("ffd978"))
else: else:

View File

@@ -2,6 +2,7 @@ extends CanvasLayer
## Startmenue: Einzelspieler, Hosten oder einer Sitzung beitreten. ## Startmenue: Einzelspieler, Hosten oder einer Sitzung beitreten.
signal solo_chosen() signal solo_chosen()
signal continue_chosen()
signal host_chosen(port: int) signal host_chosen(port: int)
signal join_chosen(address: String, port: int) signal join_chosen(address: String, port: int)
@@ -11,6 +12,8 @@ var _root: Control
var _status := Label.new() var _status := Label.new()
var _addr := LineEdit.new() var _addr := LineEdit.new()
var _port := LineEdit.new() var _port := LineEdit.new()
var _name := LineEdit.new()
var _continue: Button
func _ready() -> void: func _ready() -> void:
layer = 3 layer = 3
@@ -47,6 +50,14 @@ func _ready() -> void:
vb.add_child(sub) vb.add_child(sub)
vb.add_child(_spacer(18)) vb.add_child(_spacer(18))
_name.placeholder_text = "Dein Name"
_name.text = "Crew"
_name.max_length = 16
_name.alignment = HORIZONTAL_ALIGNMENT_CENTER
vb.add_child(_name)
_continue = _button("Weiterspielen", func(): continue_chosen.emit())
_continue.visible = FileAccess.file_exists("user://save.json")
vb.add_child(_continue)
vb.add_child(_button("Einzelspieler", func(): solo_chosen.emit())) vb.add_child(_button("Einzelspieler", func(): solo_chosen.emit()))
vb.add_child(_button("Sitzung hosten", func(): host_chosen.emit(_port_value()))) vb.add_child(_button("Sitzung hosten", func(): host_chosen.emit(_port_value())))
@@ -91,6 +102,10 @@ func _port_value() -> int:
var v := int(_port.text) var v := int(_port.text)
return v if v > 0 else DEFAULT_PORT return v if v > 0 else DEFAULT_PORT
func player_name() -> String:
var n := _name.text.strip_edges()
return n if n != "" else "Crew"
func set_status(text: String) -> void: func set_status(text: String) -> void:
_status.text = text _status.text = text

View File

@@ -32,6 +32,30 @@ const PROGRESSION := [
"melody": [440.00, 493.88, 587.33, 659.26, 783.99]}, "melody": [440.00, 493.88, 587.33, 659.26, 783.99]},
] ]
## Zweiter Teil: geht über Dm7 und Em7, kommt über G6 zurück.
const PROGRESSION_B := [
{"name": "Dm7", "pad": [220.00, 261.63, 293.66, 349.23], "bass": 146.83,
"melody": [440.00, 523.25, 587.33, 659.26, 698.46, 783.99]},
{"name": "Em7", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 164.81,
"melody": [493.88, 587.33, 659.26, 783.99, 880.00]},
{"name": "Fmaj7", "pad": [174.61, 220.00, 261.63, 329.63], "bass": 87.31,
"melody": [440.00, 523.25, 587.33, 659.26, 783.99, 880.00]},
{"name": "G6", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 98.00,
"melody": [440.00, 493.88, 587.33, 659.26, 783.99]},
{"name": "Am7", "pad": [220.00, 261.63, 329.63, 392.00], "bass": 110.00,
"melody": [440.00, 493.88, 523.25, 587.33, 659.26, 783.99]},
{"name": "Em7", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 164.81,
"melody": [493.88, 587.33, 659.26, 783.99, 880.00]},
{"name": "Fmaj7", "pad": [174.61, 220.00, 261.63, 329.63], "bass": 87.31,
"melody": [440.00, 523.25, 587.33, 659.26, 783.99, 880.00]},
{"name": "G6", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 98.00,
"melody": [440.00, 493.88, 587.33, 659.26, 783.99]},
]
## Reihenfolge der Teile: A, B, A mit anderer Melodie.
const FORM := [0, 1, 0]
const SECTION_SEEDS := [7411, 9187, 3323]
static func build_all() -> Dictionary: static func build_all() -> Dictionary:
var rng := RandomNumberGenerator.new() var rng := RandomNumberGenerator.new()
rng.seed = 20260828 rng.seed = 20260828
@@ -53,26 +77,29 @@ static func build_all() -> Dictionary:
} }
static func build_music() -> PackedByteArray: static func build_music() -> PackedByteArray:
var rng := RandomNumberGenerator.new() var section_len := BAR * BARS
rng.seed = 7411 var loop_len := section_len * FORM.size()
var loop_len := BAR * BARS
var buf := Synth.buffer(loop_len + 3.0) var buf := Synth.buffer(loop_len + 3.0)
for sec in FORM.size():
var prog: Array = PROGRESSION if FORM[sec] == 0 else PROGRESSION_B
var rng := RandomNumberGenerator.new()
rng.seed = int(SECTION_SEEDS[sec])
_render_section(buf, sec * section_len, prog, rng)
return Synth.to_pcm(Synth.seamless(buf, loop_len))
static func _render_section(buf: PackedFloat32Array, offset: float, prog: Array,
rng: RandomNumberGenerator) -> void:
for b in BARS: for b in BARS:
var t0 := b * BAR var t0 := offset + b * BAR
var stage: Dictionary = PROGRESSION[b] var stage: Dictionary = prog[b]
var pad: Array = stage["pad"] var pad: Array = stage["pad"]
var bass: float = float(stage["bass"]) var bass: float = float(stage["bass"])
var melody: Array = stage["melody"] var melody: Array = stage["melody"]
# Flächen, leicht überlappend für weiche Übergänge
for i in pad.size(): for i in pad.size():
var amp: float = 0.19 / (1.0 + i * 0.4) var amp: float = 0.19 / (1.0 + i * 0.4)
Synth.add_pad(buf, t0 - 0.45, float(pad[i]), BAR + 1.0, amp, 1.2) Synth.add_pad(buf, t0 - 0.45, float(pad[i]), BAR + 1.0, amp, 1.2)
# Bass: Grundton auf 1, Quinte auf 3
Synth.add_tone(buf, t0, bass, 2.3, 0.36, 1.4, [1.0, 0.38, 0.12], 0.01) Synth.add_tone(buf, t0, bass, 2.3, 0.36, 1.4, [1.0, 0.38, 0.12], 0.01)
Synth.add_tone(buf, t0 + BEAT * 2.0, bass * 1.5, 1.7, 0.20, 1.8, [1.0, 0.28], 0.01) Synth.add_tone(buf, t0 + BEAT * 2.0, bass * 1.5, 1.7, 0.20, 1.8, [1.0, 0.28], 0.01)
# Melodie: sparsame Glockentöne, immer aus dem Akkord
var beats := [0.0, 1.5, 2.5] if b % 2 == 0 else [0.5, 2.0, 3.0] var beats := [0.0, 1.5, 2.5] if b % 2 == 0 else [0.5, 2.0, 3.0]
for k in beats.size(): for k in beats.size():
if rng.randf() < 0.3: if rng.randf() < 0.3:
@@ -80,13 +107,10 @@ static func build_music() -> PackedByteArray:
var f: float = float(melody[rng.randi() % melody.size()]) var f: float = float(melody[rng.randi() % melody.size()])
Synth.add_tone(buf, t0 + float(beats[k]) * BEAT, f, 2.4, Synth.add_tone(buf, t0 + float(beats[k]) * BEAT, f, 2.4,
0.11, 2.2, [1.0, 0.45, 0.18, 0.07], 0.006) 0.11, 2.2, [1.0, 0.45, 0.18, 0.07], 0.006)
# Sehr leiser Shaker auf jedem Schlag
for beat in 4: for beat in 4:
Synth.add_noise(buf, t0 + beat * BEAT, 0.12, Synth.add_noise(buf, t0 + beat * BEAT, 0.12,
0.032 if beat % 2 == 1 else 0.020, 34.0, 0.62, rng) 0.032 if beat % 2 == 1 else 0.020, 34.0, 0.62, rng)
return Synth.to_pcm(Synth.seamless(buf, loop_len))
# --- Einzelklänge --------------------------------------------------------- # --- Einzelklänge ---------------------------------------------------------
## Ein Radstoß: tiefer Schlag, mittleres Klopfen, kurzer Metallanteil. ## Ein Radstoß: tiefer Schlag, mittleres Klopfen, kurzer Metallanteil.

View File

@@ -181,8 +181,12 @@ func _car_rows() -> Array:
func _consist_rows() -> Array: func _consist_rows() -> Array:
var rows: Array = [] var rows: Array = []
var d: Dictionary = Defs.LOCOS[_game.train.loco_id] var d: Dictionary = Defs.LOCOS[_game.train.loco_id]
rows.append(_row(String(d["name"]), "Lok - lässt sich nur ersetzen, nicht verkaufen", var is_steam: bool = _game.train.loco_id == "steam"
"", "", false, func(): pass)) rows.append(_row(String(d["name"]),
"Lok - beim Verkauf springt der Dampf-Veteran ein" if not is_steam else "Lok - die Grundausstattung bleibt",
"" if is_steam else "Rücknahme %d €" % int(d["sell"]),
"Verkaufen", not is_steam,
func(): _game.request("sell_loco"); _rebuild()))
var list: Array = _game.train.consist var list: Array = _game.train.consist
for i in list.size(): for i in list.size():
var cid: String = list[i] var cid: String = list[i]

View File

@@ -9,6 +9,7 @@ const PLATFORM_LEN := 44.0
const PLATFORM_H := 0.95 const PLATFORM_H := 0.95
var station_name := "" var station_name := ""
var seg := 0
var progress := 0.0 var progress := 0.0
var side := 1.0 var side := 1.0
var produces := -1 var produces := -1
@@ -19,8 +20,9 @@ var rate := 0.55
var waiting_spots: Array[Vector3] = [] ## Weltpositionen auf dem Bahnsteig var waiting_spots: Array[Vector3] = [] ## Weltpositionen auf dem Bahnsteig
var waiting: Array = [] ## wartende Passagiere var waiting: Array = [] ## wartende Passagiere
func setup(nm: String, prog: float, prod: int, acc: Array[int]) -> void: func setup(nm: String, seg_id: int, prog: float, prod: int, acc: Array[int]) -> void:
station_name = nm station_name = nm
seg = seg_id
progress = prog progress = prog
produces = prod produces = prod
accepts = acc accepts = acc

View File

@@ -1,5 +1,6 @@
extends Node3D extends Node3D
## Baut aus 2D-Stuetzpunkten eine geschlossene Gleisschleife inkl. Optik. ## Streckennetz aus Abschnitten. Ein Abschnitt hat einen oder zwei
## Nachfolger - bei zweien steht dort eine Weiche.
const Mat = preload("res://scripts/mat.gd") const Mat = preload("res://scripts/mat.gd")
const Deco = preload("res://scripts/deco.gd") const Deco = preload("res://scripts/deco.gd")
@@ -9,37 +10,69 @@ const SLEEPER := Color("5e4130")
const RAIL := Color("9a9da2") const RAIL := Color("9a9da2")
const STEP := 1.2 const STEP := 1.2
var path: Path3D ## Jeder Eintrag: { "name", "curve": Curve3D, "length", "next": [ids], "prev": [ids] }
var segments: Array = []
func build(points: PackedVector2Array) -> void: ## Abschnitt anlegen. Anfangs- und Endrichtung werden vorgegeben, damit
path = Path3D.new() ## sich mehrere Abschnitte an einer Weiche ohne Knick treffen.
path.name = "TrackPath" func add_segment(seg_name: String, points: Array, t_start: Vector2, t_end: Vector2) -> int:
var c := Curve3D.new() var c := Curve3D.new()
var n := points.size() var n := points.size()
# Catmull-Rom Tangenten -> weiche, geschlossene Schleife. for i in n:
for i in n + 1: var p: Vector2 = points[i]
var idx := i % n var t: Vector2
var prev := points[(idx - 1 + n) % n] if i == 0:
var nxt := points[(idx + 1) % n] t = t_start.normalized() * p.distance_to(points[1]) / 3.0
var t := (nxt - prev) / 6.0 elif i == n - 1:
var p := points[idx] t = t_end.normalized() * p.distance_to(points[n - 2]) / 3.0
else:
t = (points[i + 1] - points[i - 1]) / 6.0
c.add_point(Vector3(p.x, 0, p.y), Vector3(-t.x, 0, -t.y), Vector3(t.x, 0, t.y)) c.add_point(Vector3(p.x, 0, p.y), Vector3(-t.x, 0, -t.y), Vector3(t.x, 0, t.y))
path.curve = c segments.append({"name": seg_name, "curve": c, "length": c.get_baked_length(),
add_child(path) "next": [], "prev": []})
_build_visuals() return segments.size() - 1
func length() -> float: func connect_segments(from: int, to: int) -> void:
return path.curve.get_baked_length() segments[from]["next"].append(to)
segments[to]["prev"].append(from)
func sample(offset: float) -> Transform3D: func finish() -> void:
return path.curve.sample_baked_with_rotation(fposmod(offset, length()), true, true) for i in segments.size():
_build_visuals(i)
func _build_visuals() -> void: func seg_length(i: int) -> float:
var count := int(length() / STEP) return float(segments[i]["length"])
func seg_name(i: int) -> String:
return String(segments[i]["name"])
func next_of(i: int) -> Array:
return segments[i]["next"]
func prev_of(i: int) -> Array:
return segments[i]["prev"]
func is_switch(i: int) -> bool:
return segments[i]["next"].size() > 1
func sample(i: int, offset: float) -> Transform3D:
var c: Curve3D = segments[i]["curve"]
return c.sample_baked_with_rotation(clampf(offset, 0.0, seg_length(i)), true, false)
## Nächster Punkt auf irgendeinem Abschnitt - für die Deko-Verteilung.
func distance_to_track(p: Vector2) -> float:
var best := INF
for s in segments:
var c: Curve3D = s["curve"]
var q: Vector3 = c.get_closest_point(Vector3(p.x, 0, p.y))
best = minf(best, Vector2(q.x, q.z).distance_to(p))
return best
func _build_visuals(i: int) -> void:
var count := int(seg_length(i) / STEP)
var xf: Array[Transform3D] = [] var xf: Array[Transform3D] = []
for i in count: for k in count:
xf.append(sample(i * STEP)) xf.append(sample(i, k * STEP + STEP * 0.5))
_multi(Deco.box(Vector3(2.7, 0.16, STEP * 1.12)), BALLAST, xf, [Vector3(0, 0.08, 0)]) _multi(Deco.box(Vector3(2.7, 0.16, STEP * 1.12)), BALLAST, xf, [Vector3(0, 0.08, 0)])
_multi(Deco.box(Vector3(2.1, 0.09, 0.34)), SLEEPER, xf, [Vector3(0, 0.20, 0)]) _multi(Deco.box(Vector3(2.1, 0.09, 0.34)), SLEEPER, xf, [Vector3(0, 0.20, 0)])
_multi(Deco.box(Vector3(0.09, 0.11, STEP * 1.08)), RAIL, xf, _multi(Deco.box(Vector3(0.09, 0.11, STEP * 1.08)), RAIL, xf,

View File

@@ -1,6 +1,6 @@
extends Node3D extends Node3D
## Der Zugverband: Lok + Waggons auf der Strecke, Fahrphysik, ## Der Zugverband auf dem Streckennetz: Fahrphysik, Weichenwahl und
## und die Abbildung "Laengsposition im Zug" -> Weltkoordinate. ## die Abbildung "Laengsposition im Zug" -> Weltkoordinate.
const CarScene = preload("res://scripts/car.gd") const CarScene = preload("res://scripts/car.gd")
const Defs = preload("res://scripts/car_defs.gd") const Defs = preload("res://scripts/car_defs.gd")
@@ -8,25 +8,33 @@ const Cargo = preload("res://scripts/cargo.gd")
signal consist_changed() signal consist_changed()
const GAP := 0.75 ## Abstand zwischen zwei Waggons const GAP := 0.75
const DRAG := 0.6 const DRAG := 0.6
const DOCK_RANGE := 3.0 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 loco_id := "steam"
var consist: Array[String] = [] ## nur die Waggons, ohne Lok var consist: Array[String] = []
var cars: Array = [] ## Car-Knoten, Index 0 = Lok var cars: Array = []
var followers: Array[PathFollow3D] = []
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 speed := 0.0
var throttle := 0.0 var throttle := 0.0
var braking := false var braking := false
var cargo: Dictionary = {} ## Cargo.Type -> int var cargo: Dictionary = {}
var stations: Array = [] var stations: Array = []
var docked_at: Node = null var docked_at: Node = null
var sway_enabled := true
var _track var _track
var _stand_time := 0.0 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: func setup(track, station_list: Array) -> void:
_track = track _track = track
@@ -68,14 +76,12 @@ func seat_count() -> int:
n += c.seats.size() n += c.seats.size()
return n return n
# --- Verband aufbauen ----------------------------------------------------- # --- Verband --------------------------------------------------------------
func rebuild() -> void: func rebuild() -> void:
for f in followers: for c in cars:
f.queue_free() c.queue_free()
followers.clear()
cars.clear() cars.clear()
var ids: Array[String] = [loco_id] var ids: Array[String] = [loco_id]
ids.append_array(consist) ids.append_array(consist)
var s := 0.0 var s := 0.0
@@ -85,16 +91,9 @@ func rebuild() -> void:
car.index = i car.index = i
car.center_s = s - car.length * 0.5 car.center_s = s - car.length * 0.5
s = car.center_s - car.length * 0.5 - GAP s = car.center_s - car.length * 0.5 - GAP
var f := PathFollow3D.new() add_child(car)
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) cars.append(car)
_sync_followers() _place_cars(0.0)
refresh_cargo_visuals() refresh_cargo_visuals()
consist_changed.emit() consist_changed.emit()
@@ -102,9 +101,8 @@ func total_length() -> float:
if cars.is_empty(): if cars.is_empty():
return 0.0 return 0.0
var last = cars[cars.size() - 1] 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: func walk_range() -> Vector2:
if cars.is_empty(): if cars.is_empty():
return Vector2.ZERO return Vector2.ZERO
@@ -112,7 +110,6 @@ func walk_range() -> Vector2:
var back = cars[cars.size() - 1] var back = cars[cars.size() - 1]
return Vector2(back.center_s + back.inner_z0 + 0.35, front.center_s + front.inner_z1 - 0.35) 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: func locate(s: float) -> Dictionary:
var best := 0 var best := 0
var best_d := INF var best_d := INF
@@ -122,7 +119,7 @@ func locate(s: float) -> Dictionary:
var hi: float = c.center_s + c.inner_z1 var hi: float = c.center_s + c.inner_z1
if s >= lo and s <= hi: if s >= lo and s <= hi:
return {"index": i, "local_z": s - c.center_s, "gangway": false} 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: if d < best_d:
best_d = d best_d = d
best = i best = i
@@ -130,18 +127,95 @@ func locate(s: float) -> Dictionary:
return {"index": best, "local_z": s - c2.center_s, "gangway": true} return {"index": best, "local_z": s - c2.center_s, "gangway": true}
func car_transform(i: int) -> Transform3D: 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: func world_point(s: float, x: float, y: float) -> Transform3D:
var loc := locate(s) var loc := locate(s)
var xf: Transform3D = car_transform(int(loc["index"])) return car_transform(int(loc["index"])).translated_local(Vector3(x, y, float(loc["local_z"])))
return xf.translated_local(Vector3(x, y, float(loc["local_z"])))
func _sync_followers() -> void: # --- Position auf dem Netz -----------------------------------------------
var l: float = _track.length()
## 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(): 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 --------------------------------------------------------------- # --- Ladung ---------------------------------------------------------------
@@ -188,39 +262,80 @@ func drive(delta: float) -> void:
speed = minf(speed + accel() * throttle * delta, max_speed()) speed = minf(speed + accel() * throttle * delta, max_speed())
else: else:
speed = maxf(speed - DRAG * delta, 0.0) speed = maxf(speed - DRAG * delta, 0.0)
progress = fposmod(progress + speed * delta, _track.length()) progress += speed * delta
_sync_followers() _advance()
_place_cars(delta)
_update_dock(delta) _update_dock(delta)
## Nur Anzeige aktualisieren (Clients im Netzspiel). ## Client: Server-Stand übernehmen, dazwischen selbst weiterrechnen.
func apply_snapshot(p: float, v: float) -> void: func apply_snapshot(seg_id: int, prog: float, v: float, sw: int, hist: Array) -> void:
progress = p switch_choice = sw
if seg_id != seg:
seg = seg_id
progress = prog
history = hist.duplicate()
else:
progress = lerpf(progress, prog, 0.4)
speed = v 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: func _update_dock(delta: float) -> void:
var near := nearest_station() var near := nearest_station()
var st: Node = near[0] var st: Node = near[0]
var dist: float = near[1]
if st == null: if st == null:
return return
if speed > 0.15 or dist > DOCK_RANGE: if speed > 0.15 or float(near[1]) > DOCK_RANGE:
_stand_time = 0.0 _stand_time = 0.0
docked_at = null docked_at = null
return return
_stand_time += delta _stand_time += delta
docked_at = st docked_at = st
## Abstand eines Bahnhofs zum Zug. 0, solange der Bahnsteig neben dem ## Abstand eines Bahnhofs zum Zug entlang der Strecke. 0, solange der
## Zug liegt - sonst waere ein langer Zug nie richtig am Bahnsteig. ## Bahnsteig neben dem Zug liegt.
func station_gap(st) -> float: func station_gap(st) -> float:
var l: float = _track.length() var ahead := distance_ahead(st)
var behind: float = fposmod(progress - st.progress, l) if ahead == 0.0:
var len_train: float = total_length()
if behind <= len_train:
return 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: func nearest_station() -> Array:
var best: Node = null var best: Node = null
@@ -235,10 +350,11 @@ func nearest_station() -> Array:
func next_station() -> Array: func next_station() -> Array:
var best: Node = null var best: Node = null
var best_d := INF var best_d := INF
var l: float = _track.length()
for s in stations: for s in stations:
var d: float = fposmod(s.progress - progress, l) var d: float = distance_ahead(s)
if d < best_d: if d > 0.0 and d < best_d:
best_d = d best_d = d
best = s best = s
if best == null:
return nearest_station()
return [best, best_d] return [best, best_d]

View File

@@ -68,8 +68,7 @@ func _scatter(track, stations: Array) -> void:
while placed < 620 and tries < 12000: while placed < 620 and tries < 12000:
tries += 1 tries += 1
var p := Vector2(_rng.randf_range(-EXTENT, EXTENT), _rng.randf_range(-EXTENT, EXTENT)) var p := Vector2(_rng.randf_range(-EXTENT, EXTENT), _rng.randf_range(-EXTENT, EXTENT))
var closest: Vector3 = track.path.curve.get_closest_point(Vector3(p.x, 0, p.y)) if track.distance_to_track(p) < 11.0:
if Vector2(closest.x, closest.z).distance_to(p) < 11.0:
continue continue
if p.distance_to(Vector2(-230, 150)) < 78.0: if p.distance_to(Vector2(-230, 150)) < 78.0:
continue continue

View File

@@ -7,6 +7,7 @@ var ok := 0
var fail := 0 var fail := 0
var worst := 0.0 var worst := 0.0
var elapsed := 0.0 var elapsed := 0.0
var sfx_first := false
func chk(n: String, c: bool, i := "") -> void: func chk(n: String, c: bool, i := "") -> void:
if c: ok += 1; print(" OK %s %s" % [n, i]) if c: ok += 1; print(" OK %s %s" % [n, i])
@@ -23,11 +24,14 @@ func _process(d):
if not main.started: if not main.started:
main._start_solo() main._start_solo()
return return
if not main.audio.loaded: if not main.audio.music_loaded:
elapsed += d elapsed += d
worst = maxf(worst, d) worst = maxf(worst, d)
if main.audio.loaded:
sfx_first = true
return return
if step == 0: if step == 0:
chk("Klänge vor der Musik verfügbar", sfx_first)
chk("Kein Ruckler beim Erzeugen", worst < 0.2, chk("Kein Ruckler beim Erzeugen", worst < 0.2,
"längster Frame %.0f ms in %.1f s Ladezeit" % [worst * 1000.0, elapsed]) "längster Frame %.0f ms in %.1f s Ladezeit" % [worst * 1000.0, elapsed])
if wait > 0.0: if wait > 0.0:
@@ -56,7 +60,7 @@ func _process(d):
chk("Thread beendet", a._thread == null) chk("Thread beendet", a._thread == null)
var m: AudioStreamWAV = a._music.stream var m: AudioStreamWAV = a._music.stream
chk("Musik ist eine Schleife", m.loop_mode == AudioStreamWAV.LOOP_FORWARD) chk("Musik ist eine Schleife", m.loop_mode == AudioStreamWAV.LOOP_FORWARD)
chk("Musik lang genug", m.data.size() / 2 > 20 * 22050, chk("Musik hat drei Teile", m.data.size() / 2 > 75 * 22050,
"%.1f s" % (m.data.size() / 2.0 / 22050.0)) "%.1f s" % (m.data.size() / 2.0 / 22050.0))
chk("Musik nicht stumm", _peak(m) > 3000, "peak=%d" % _peak(m)) chk("Musik nicht stumm", _peak(m) > 3000, "peak=%d" % _peak(m))
chk("Rumble läuft leise", a._rumble.playing and a._rumble.volume_db < -40.0, chk("Rumble läuft leise", a._rumble.playing and a._rumble.volume_db < -40.0,
@@ -134,7 +138,7 @@ func _peak(w: AudioStreamWAV) -> int:
func _check_harmony() -> void: func _check_harmony() -> void:
var Sfx = load("res://scripts/sfx.gd") var Sfx = load("res://scripts/sfx.gd")
var bad: Array[String] = [] var bad: Array[String] = []
for stage in Sfx.PROGRESSION: for stage in Sfx.PROGRESSION + Sfx.PROGRESSION_B:
var voices: Array[float] = [] var voices: Array[float] = []
for f in stage["pad"]: for f in stage["pad"]:
voices.append(float(f)) voices.append(float(f))

View File

@@ -31,6 +31,7 @@ func _process(d):
if role == "host": if role == "host":
main._start_host(7788) main._start_host(7788)
elif role == "client" and step == 0: elif role == "client" and step == 0:
main.menu._name.text = "Tester"
main._start_join("127.0.0.1", 7788) main._start_join("127.0.0.1", 7788)
step = 1 step = 1
return return
@@ -42,14 +43,24 @@ func _process(d):
else: else:
_client_step() _client_step()
func _key(c: int, d: bool) -> void:
var e := InputEventKey.new(); e.physical_keycode = c; e.keycode = c; e.pressed = d
Input.parse_input_event(e)
func _host_step() -> void: func _host_step() -> void:
match step: match step:
0: 0:
print("[host] Sitzung gestartet") print("[host] Sitzung gestartet")
for i in 4: for i in 4:
main.spawn_passenger(0, 2) main.spawn_passenger(0, 2)
wait = 30.0 wait = 16.0
1: 1:
# Host faehrt los, der Client soll das weich sehen
main._srv_toggle_drive(1)
_key(KEY_W, true)
wait = 20.0
2:
_key(KEY_W, false)
print("[host] Ende. money=%d driver=%d" % [main.money, main.driver_id]) print("[host] Ende. money=%d driver=%d" % [main.money, main.driver_id])
get_tree().quit() get_tree().quit()
step += 1 step += 1
@@ -59,7 +70,7 @@ func _client_step() -> void:
1: 1:
wait = 6.0 wait = 6.0
2: 2:
chk("Welt gebaut", main.stations.size() == 5) chk("Welt gebaut", main.stations.size() == 6)
chk("Zug übernommen", main.train.cars.size() == 3, "cars=%d" % main.train.cars.size()) chk("Zug übernommen", main.train.cars.size() == 3, "cars=%d" % main.train.cars.size())
chk("Eigene Peer-ID", main.my_id > 1) chk("Eigene Peer-ID", main.my_id > 1)
chk("Fahrgäste sichtbar", main.passengers.size() >= 4, "n=%d" % main.passengers.size()) chk("Fahrgäste sichtbar", main.passengers.size() >= 4, "n=%d" % main.passengers.size())
@@ -69,6 +80,11 @@ func _client_step() -> void:
seated += 1 seated += 1
chk("Gäste sitzen", seated >= 4, "seated=%d" % seated) chk("Gäste sitzen", seated >= 4, "seated=%d" % seated)
chk("Geld synchron", main.money == 1500) chk("Geld synchron", main.money == 1500)
chk("Eigener Name beim Host angekommen",
String(main.players[main.my_id]["name"]) == "Tester",
"%s" % main.players[main.my_id]["name"])
var host_av = main.players.get(1, {}).get("avatar")
chk("Avatar des Hosts sichtbar", host_av != null)
main.request("drive") main.request("drive")
wait = 1.5 wait = 1.5
3: 3:
@@ -96,6 +112,36 @@ func _client_step() -> void:
wait = 2.0 wait = 2.0
6: 6:
chk("Kein Selbstlauf ohne Gas", main.train.speed < 0.01) chk("Kein Selbstlauf ohne Gas", main.train.speed < 0.01)
main.request("drive") # Steuerung abgeben, der Host faehrt gleich
_measuring = true
wait = 12.0
7:
_measuring = false
chk("Zug beim Client in Fahrt", _top_speed > 3.0, "v=%.1f" % _top_speed)
chk("Zug beim Client ruckelfrei", _frames > 100 and _jumps * 20 < _frames,
"%d Sprünge in %d Frames, größter %.2f m" % [_jumps, _frames, _worst])
print("[client] == %d ok, %d fail ==" % [ok, fail]) print("[client] == %d ok, %d fail ==" % [ok, fail])
get_tree().quit(1 if fail > 0 else 0) get_tree().quit(1 if fail > 0 else 0)
step += 1 step += 1
var _measuring := false
var _last := Vector3.ZERO
var _frames := 0
var _jumps := 0
var _worst := 0.0
var _top_speed := 0.0
func _physics_process(d: float) -> void:
if not _measuring or main == null or not main.started:
return
var t = main.train
var p: Vector3 = t.cars[0].global_position
if _last != Vector3.ZERO and t.speed > 1.0:
_frames += 1
var moved := p.distance_to(_last)
var expected: float = t.speed * d
_worst = maxf(_worst, absf(moved - expected))
if absf(moved - expected) > 0.6:
_jumps += 1
_top_speed = maxf(_top_speed, t.speed)
_last = p

View File

@@ -27,6 +27,12 @@ install_test tests/input_test.gd
"$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|==" "$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|=="
INPUT=${PIPESTATUS[0]} INPUT=${PIPESTATUS[0]}
echo
echo "=== Weichen-Tests ==="
install_test tests/switch_test.gd
"$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|=="
SWITCH=${PIPESTATUS[0]}
echo echo
echo "=== Klang-Tests ===" echo "=== Klang-Tests ==="
install_test tests/audio_test.gd install_test tests/audio_test.gd
@@ -50,5 +56,5 @@ NET=${PIPESTATUS[0]}
wait $HOST 2>/dev/null wait $HOST 2>/dev/null
echo echo
[ "$SOLO" -eq 0 ] && [ "$INPUT" -eq 0 ] && [ "$DIR" -eq 0 ] && [ "$AUDIO" -eq 0 ] && [ "$NET" -eq 0 ] && echo "Alle Tests bestanden." || echo "Es sind Tests fehlgeschlagen." [ "$SOLO" -eq 0 ] && [ "$SWITCH" -eq 0 ] && [ "$INPUT" -eq 0 ] && [ "$DIR" -eq 0 ] && [ "$AUDIO" -eq 0 ] && [ "$NET" -eq 0 ] && echo "Alle Tests bestanden." || echo "Es sind Tests fehlgeschlagen."
exit $(( SOLO || INPUT || DIR || AUDIO || NET )) exit $(( SOLO || SWITCH || INPUT || DIR || AUDIO || NET ))

View File

@@ -112,6 +112,36 @@ func _process(d):
var before = main.train.consist.size() var before = main.train.consist.size()
main.sell_car(0) main.sell_car(0)
chk("Wagen verkauft", main.train.consist.size() == before - 1) chk("Wagen verkauft", main.train.consist.size() == before - 1)
# Lok verkaufen: Dampf-Veteran springt ein, Geld kommt zurueck
var m_before = main.money
main.request("sell_loco")
chk("Lok verkauft", main.train.loco_id == "steam", "loco=%s" % main.train.loco_id)
chk("Rücknahme gutgeschrieben", main.money - m_before == 6600, "+%d €" % (main.money - m_before))
main.buy_loco("express")
# Speichern und Laden
main.save_game()
var saved_money = main.money
var saved_consist = main.train.consist.duplicate()
var saved_seg = main.train.seg
var saved_prog = main.train.progress
main.money = 1
main.train.consist = ["coach"] as Array[String]
main.train.loco_id = "steam"
main._rebuild_train()
chk("Spielstand geladen", main.load_game())
chk("Geld wiederhergestellt", main.money == saved_money, "%d vs %d" % [main.money, saved_money])
chk("Zug wiederhergestellt", main.train.consist == saved_consist and main.train.loco_id == "express",
"%s %s" % [main.train.loco_id, main.train.consist])
chk("Position wiederhergestellt", main.train.seg == saved_seg and absf(main.train.progress - saved_prog) < 0.5)
# Ziele
main.tickets_checked = 10
var m_goal = main.money
main._check_goals()
chk("Ziel erreicht", main.goals_done.has("tickets10"))
chk("Belohnung gezahlt", main.money - m_goal == 250, "+%d €" % (main.money - m_goal))
chk("Ziele der Reihe nach", not main.goals_done.has("cargo30") and not main.won)
var g = main.current_goal()
chk("Nächstes Ziel im HUD", String(g.get("text", "")).begins_with("30 Einheiten"), "%s" % g)
wait = 0.3 wait = 0.3
7: 7:
chk("Spieler im Zug", main.train.locate(main.player.s)["index"] == 0) chk("Spieler im Zug", main.train.locate(main.player.s)["index"] == 0)
@@ -121,6 +151,9 @@ func _process(d):
wait = 3.0 wait = 3.0
8: 8:
chk("Zug fährt", main.train.speed > 3.0, "v=%.1f m/s" % main.train.speed) chk("Zug fährt", main.train.speed > 3.0, "v=%.1f m/s" % main.train.speed)
var e: Vector3 = main.train.cars[0].global_transform.basis.get_euler()
chk("Wagen wackelt bei Fahrt", absf(e.z) > 0.0005 or absf(e.x) > 0.0005,
"roll=%.4f pitch=%.4f" % [e.z, e.x])
_key(KEY_W, false) _key(KEY_W, false)
_key(KEY_S, true) _key(KEY_S, true)
wait = 4.0 wait = 4.0

175
tests/switch_test.gd Normal file
View File

@@ -0,0 +1,175 @@
extends Node
## Weichen: Route wählen, Übergang ohne Sprung, Bergstrecke erreichbar.
var main
var step := 0
var wait := 0.0
var ok := 0
var fail := 0
var last_pos := Vector3.ZERO
var worst_jump := 0.0
var jump_frames := 0
var _dbg := 0.0
var _cam_hits := 0
var _cam_pulled := 0
var _cam_yaw := 0
func chk(n: String, c: bool, i := "") -> void:
if c: ok += 1; print(" OK %s %s" % [n, i])
else: fail += 1; print(" FAIL %s %s" % [n, i])
func _key(c: int, d: bool) -> void:
var e := InputEventKey.new(); e.physical_keycode = c; e.keycode = c; e.pressed = d
Input.parse_input_event(e)
func _watch(d: float) -> void:
var t = main.train
var p: Vector3 = t.cars[t.cars.size() - 1].global_position
if last_pos != Vector3.ZERO and d > 0.0:
var jump := p.distance_to(last_pos)
var allowed: float = t.speed * d * 1.5 + 0.05
if jump > allowed:
jump_frames += 1
worst_jump = maxf(worst_jump, jump - t.speed * d)
last_pos = p
## Im Physik-Takt messen: dort bewegt sich der Zug genau einmal pro Aufruf.
func _physics_process(d: float) -> void:
if main != null and main.started and step >= 1 and step <= 8:
_watch(d)
func _process(d):
if main == null:
main = get_tree().current_scene
return
if not main.started:
main._start_solo()
return
var t = main.train
if wait > 0.0:
wait -= d
return
match step:
0:
chk("Drei Abschnitte", main.track.segments.size() == 3)
chk("Heimstrecke hat Weiche", main.track.is_switch(2))
chk("Sechs Bahnhöfe", main.stations.size() == 6)
main._park_at(main.stations[4]) # Ahornstadt, Heimstrecke
chk("Start auf Heimstrecke", t.seg == 2, "seg=%d" % t.seg)
chk("Weiche voraus erkannt", t.upcoming_switch() == 2)
var nxt: Array = t.next_station()
chk("Nächster Halt Waldkirch (links)", nxt[0].station_name == "Waldkirch",
"%s" % nxt[0].station_name)
t.switch_choice = 1
var nxt2: Array = t.next_station()
chk("Nächster Halt Bergblick (rechts)", nxt2[0].station_name == "Bergblick",
"%s" % nxt2[0].station_name)
t.switch_choice = 0
main.driver_id = 1
_key(KEY_W, true)
last_pos = Vector3.ZERO
wait = 1.0
1:
wait = 0.5
2:
if t.seg != 0:
step -= 1
wait = 0.3
_dbg += 0.3
if _dbg >= 2.0:
_dbg = 0.0
print("[dbg] seg=%d prog=%.1f/%.1f v=%.1f thr=%.1f driver=%d mode=%d" % [t.seg, t.progress,
main.track.seg_length(t.seg), t.speed, t.throttle, main.driver_id, main.player.mode])
else:
_key(KEY_W, false)
_key(KEY_S, true)
chk("Links -> Talstrecke", t.seg == 0, "seg=%d" % t.seg)
chk("Historie geführt", t.history.size() > 0 and t.history[t.history.size() - 1] == 2,
"%s" % [t.history])
chk("Letzter Wagen ohne Sprung", jump_frames == 0,
"Sprungframes=%d, größter Sprung %.2f m" % [jump_frames, worst_jump])
wait = 4.0
3:
_key(KEY_S, false)
main._park_at(main.stations[4])
t.switch_choice = 1
last_pos = Vector3.ZERO
jump_frames = 0
worst_jump = 0.0
_key(KEY_D, true)
_key(KEY_W, true)
wait = 1.0
4:
wait = 0.5
5:
if t.seg != 1:
step -= 1
wait = 0.3
else:
chk("Rechts -> Bergstrecke", t.seg == 1, "seg=%d" % t.seg)
chk("D hält die Weiche rechts", t.switch_choice == 1)
chk("Übergang rechts ohne Sprung", jump_frames == 0,
"Sprungframes=%d" % jump_frames)
_key(KEY_D, false)
wait = 0.5
6:
var nxt: Array = t.next_station()
chk("Bergblick voraus", nxt[0].station_name == "Bergblick", "%s" % nxt[0].station_name)
wait = 0.5
7:
# bis kurz vor Bergblick fahren, dann bremsen
var berg = main.stations[5]
var ahead: float = t.distance_ahead(berg)
if ahead > t.speed * t.speed / 16.0 + 8.0:
step -= 1
wait = 0.2
else:
_key(KEY_W, false)
_key(KEY_S, true)
wait = 6.0
8:
_key(KEY_S, false)
chk("Zug steht", t.speed < 0.05, "v=%.2f" % t.speed)
chk("Am Bergblick angekommen", t.docked_at != null and t.docked_at.station_name == "Bergblick",
"dock=%s" % ("-" if t.docked_at == null else t.docked_at.station_name))
# Sichtkontrolle: Kreuzung von oben
main._park_at(main.stations[4])
t.place_head(2, main.track.seg_length(2) - 6.0)
main._toggle_camera()
main.cam_rig.distance = 95.0
main.cam_rig.pitch = deg_to_rad(-70.0)
main.cam_rig.yaw = 0.4
wait = 1.0
9:
get_viewport().get_texture().get_image().save_png("user://junction.png")
t.place_head(0, main.track.seg_length(0) - 20.0)
main.cam_rig.snap_to(t.cars[0].global_position)
wait = 1.0
10:
get_viewport().get_texture().get_image().save_png("user://junction2.png")
# Kamera-Kollision: Fokus auf den Bahnsteig, rundherum drehen
var st = main.stations[0]
main.cam_rig.target = null
main.cam_rig.snap_to(st.global_position + Vector3(0, 0, 0))
main.cam_rig.distance = 30.0
main.cam_rig.pitch = deg_to_rad(-10.0)
_cam_hits = 0
_cam_yaw = 0
wait = 0.2
11:
if _cam_yaw < 12:
main.cam_rig.yaw = _cam_yaw * TAU / 12.0
_cam_yaw += 1
step -= 1
wait = 0.15
if main.cam_rig.blocked:
_cam_hits += 1
var dist: float = main.cam_rig._cam.global_position.distance_to(main.cam_rig.global_position)
if dist < 29.0:
_cam_pulled += 1
else:
chk("Kamera stößt am Gebäude an", _cam_hits > 0, "%d von 12 Richtungen" % _cam_hits)
chk("Kamera rückt davor", _cam_pulled == _cam_hits, "%d/%d" % [_cam_pulled, _cam_hits])
12:
print("== %d ok, %d fail ==" % [ok, fail])
get_tree().quit(1 if fail > 0 else 0)
step += 1