extends Node3D ## Weltaufbau, Spielstand, Crew-Aktionen und Netzwerk. ## Der Host ist massgeblich: er simuliert Zug, Fahrgäste und Kasse. ## Clients schicken Wuensche und bekommen Zustaende zurueck. const Mat = preload("res://scripts/mat.gd") const Deco = preload("res://scripts/deco.gd") const Cargo = preload("res://scripts/cargo.gd") const Defs = preload("res://scripts/car_defs.gd") const TrackScript = preload("res://scripts/track.gd") const WorldScript = preload("res://scripts/world.gd") const StationScript = preload("res://scripts/station.gd") const TrainScript = preload("res://scripts/train.gd") const PlayerScript = preload("res://scripts/player.gd") const PassengerScript = preload("res://scripts/passenger.gd") const AvatarScript = preload("res://scripts/avatar.gd") const CameraRig = preload("res://scripts/camera_rig.gd") const AudioScript = preload("res://scripts/audio.gd") const HudScript = preload("res://scripts/hud.gd") const ShopScript = preload("res://scripts/shop.gd") const MenuScript = preload("res://scripts/menu.gd") enum Mode { MENU, SOLO, HOST, CLIENT } const TRACK_POINTS := [ Vector2(-139, -77), Vector2(-53, -101), Vector2(38, -86), Vector2(110, -48), Vector2(144, 14), Vector2(101, 77), Vector2(14, 96), Vector2(-62, 77), Vector2(-101, 34), Vector2(-154, -14), ] const STATION_DEFS := [ ["Waldkirch", 0.04, Cargo.Type.WOOD, [Cargo.Type.GRAIN], 1.0, "c9a06a", "8a4b2f", false], ["Kohlberg", 0.22, Cargo.Type.COAL, [], -1.0, "6f6a63", "40393a", true], ["Eisenwerk", 0.40, -1, [Cargo.Type.COAL, Cargo.Type.WOOD], 1.0, "9aa0a6", "55606b", true], ["Grüntal", 0.60, Cargo.Type.GRAIN, [], -1.0, "e8ddc4", "b4562f", false], ["Ahornstadt", 0.80, -1, [Cargo.Type.GRAIN, Cargo.Type.WOOD], 1.0, "f0e6d2", "a83b30", true], ] const WORLD_SEED := 20260828 const SPAWN_INTERVAL := 3.5 const BOARD_INTERVAL := 0.45 const LOAD_INTERVAL := 0.35 const DRINK_TIP := 14 const START_MONEY := 1500 const POSE_HZ := 15.0 const STATE_HZ := 4.0 var mode := Mode.MENU var started := false var my_id := 1 var driver_id := 0 var money := START_MONEY var reputation := 0 var upgrades: Dictionary = {} var delivered_units := 0 var track: Node3D var world: Node3D var train: Node3D var player: Node3D var hud: CanvasLayer var shop: CanvasLayer var menu: CanvasLayer var cam_rig: Node3D var audio: Node var stations: Array = [] var passengers: Array = [] var players: Dictionary = {} ## peer_id -> { "s", "x", "yaw", "carrying", "avatar" } var _rng := RandomNumberGenerator.new() var _spawn_timer := 0.0 var _board_timer := 0.0 var _load_timer := 0.0 var _pose_timer := 0.0 var _state_timer := 0.0 var _next_pid := 1 var _by_pid: Dictionary = {} var _exterior := false var _last_dock: Node = null var _last_speed := 0.0 func _ready() -> void: menu = MenuScript.new() add_child(menu) menu.solo_chosen.connect(_start_solo) menu.host_chosen.connect(_start_host) menu.join_chosen.connect(_start_join) multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_disconnected.connect(_on_peer_disconnected) multiplayer.connected_to_server.connect(_on_connected) multiplayer.connection_failed.connect(_on_connect_failed) multiplayer.server_disconnected.connect(_on_server_gone) func is_server() -> bool: return mode == Mode.SOLO or mode == Mode.HOST func networked() -> bool: return mode == Mode.HOST or mode == Mode.CLIENT # --- Start ---------------------------------------------------------------- func _start_solo() -> void: mode = Mode.SOLO my_id = 1 _begin() func _start_host(port: int) -> void: var peer := ENetMultiplayerPeer.new() var err := peer.create_server(port, 8) if err != OK: menu.set_status("Port %d belegt (Fehler %d)." % [port, err]) return multiplayer.multiplayer_peer = peer mode = Mode.HOST my_id = 1 _begin() hud.toast("Sitzung läuft auf Port %d" % port, Color("8fd8ff")) func _start_join(address: String, port: int) -> void: var peer := ENetMultiplayerPeer.new() var err := peer.create_client(address, port) if err != OK: menu.set_status("Verbindung nicht möglich (Fehler %d)." % err) return multiplayer.multiplayer_peer = peer mode = Mode.CLIENT menu.set_status("Verbinde mit %s:%d ..." % [address, port]) func _on_connected() -> void: my_id = multiplayer.get_unique_id() _begin() func _on_connect_failed() -> void: mode = Mode.MENU multiplayer.multiplayer_peer = null menu.set_status("Host nicht erreichbar.") func _on_server_gone() -> void: if hud: hud.toast("Verbindung zum Host verloren.", Color("ff9a8a")) func _begin() -> void: _rng.seed = WORLD_SEED _build_environment() _build_track() _build_stations() world = WorldScript.new() add_child(world) world.build(_rng, track, stations) _build_train() _build_player() _build_ui() audio = AudioScript.new() add_child(audio) players[my_id] = {"s": player.s, "x": player.x, "yaw": 0.0, "carrying": 0, "avatar": null} menu.hide_menu() player.capture_mouse(true) started = true # --- Aufbau --------------------------------------------------------------- func _build_environment() -> void: var sky_mat := ProceduralSkyMaterial.new() sky_mat.sky_top_color = Color("5fa8e0") sky_mat.sky_horizon_color = Color("cfe6f0") sky_mat.ground_bottom_color = Color("bdb6a6") sky_mat.ground_horizon_color = Color("d8e2e6") sky_mat.sun_angle_max = 30.0 var sky := Sky.new() sky.sky_material = sky_mat var env := Environment.new() env.background_mode = Environment.BG_SKY env.sky = sky env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color("d5dde2") env.ambient_light_energy = 0.62 env.tonemap_mode = Environment.TONE_MAPPER_FILMIC env.tonemap_white = 4.0 var we := WorldEnvironment.new() we.environment = env add_child(we) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-48, -125, 0) sun.light_energy = 1.5 sun.light_color = Color("fff2dd") sun.shadow_enabled = true sun.light_angular_distance = 1.4 sun.directional_shadow_max_distance = 260.0 sun.shadow_bias = 0.06 add_child(sun) func _build_track() -> void: track = TrackScript.new() add_child(track) var pts := PackedVector2Array() for p in TRACK_POINTS: pts.append(p) track.build(pts) func _build_stations() -> void: var l: float = track.length() for d in STATION_DEFS: var st := StationScript.new() add_child(st) var acc: Array[int] = [] for t in d[3]: acc.append(int(t)) st.setup(String(d[0]), l * float(d[1]), int(d[2]), acc) st.build_visuals(track.sample(st.progress), float(d[4]), Color(String(d[5])), Color(String(d[6])), bool(d[7])) if st.produces >= 0: st.stock = 8.0 stations.append(st) func _build_train() -> void: train = TrainScript.new() add_child(train) train.setup(track, stations) train.consist = ["coach", "freight"] as Array[String] train.rebuild() _park_at(stations[0]) ## Stellt den Zug mittig an einen Bahnsteig. func _park_at(st) -> void: train.progress = fposmod(st.progress + train.total_length() * 0.5, track.length()) train.apply_snapshot(train.progress, 0.0) func _build_player() -> void: player = PlayerScript.new() add_child(player) player.setup(train) player.interact_pressed.connect(_on_interact) cam_rig = CameraRig.new() add_child(cam_rig) cam_rig.target = train.followers[0] cam_rig.snap_to(track.sample(train.progress).origin) player.camera.current = true func _build_ui() -> void: hud = HudScript.new() add_child(hud) shop = ShopScript.new() add_child(shop) shop.setup(self) shop.closed.connect(_on_shop_closed) # --- Ablauf --------------------------------------------------------------- func _process(delta: float) -> void: if not started: return if is_server(): for st in stations: st.tick(delta) _spawn_passengers(delta) _update_passengers(delta) _handle_station_stop(delta) _handle_hold_actions(delta) _update_audio(delta) _sync_out(delta) _update_avatars() hud.refresh(self) func _physics_process(delta: float) -> void: if not started: return var driving: bool = driver_id == my_id and not player.input_locked var th := 0.0 var br := false if driving: 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) if is_server(): if driver_id == my_id: train.throttle = th train.braking = br train.drive(delta) elif driving: rpc_id(1, "net_controls", th, br) func _unhandled_input(event: InputEvent) -> void: if not started: return if event is InputEventKey and event.pressed and not event.echo: match event.keycode: KEY_H: if driver_id == my_id: request("whistle") KEY_M: hud.toast("Musik %s" % ("an" if audio.toggle_music() else "aus"), Color("8fd8ff")) KEY_V: _toggle_camera() KEY_ESCAPE: if shop.is_open: shop.close() else: player.capture_mouse(Input.mouse_mode != Input.MOUSE_MODE_CAPTURED) func _toggle_camera() -> void: _exterior = not _exterior if _exterior: cam_rig.activate() else: player.camera.current = true # --- Klang ---------------------------------------------------------------- func _update_audio(delta: float) -> void: audio.update_train(train.speed, train.max_speed(), train.braking, delta) audio.update_steps(player.move_speed(), delta) # Ankunft und Auszischen beim Halten if train.docked_at != _last_dock: if train.docked_at != null and train.speed < 0.1: audio.play_at("bell", train.docked_at.global_position, -6.0) _last_dock = train.docked_at if _last_speed > 2.0 and train.speed < 0.2: audio.play_at("hiss", _loco_pos(), -10.0) _last_speed = train.speed func _loco_pos() -> Vector3: return train.cars[0].global_position if not train.cars.is_empty() else global_position func _play_sfx(name: String, at_loco := false, db := 0.0) -> void: if at_loco: audio.play_at(name, _loco_pos(), db) else: audio.play(name, db) ## Klang bei allen Mitspielern auslösen. func _broadcast_sfx(name: String, at_loco := false, db := 0.0) -> void: _play_sfx(name, at_loco, db) if mode == Mode.HOST: rpc("net_sfx", name, at_loco, db) ## Klang nur bei einem bestimmten Spieler. func _sfx_to(peer: int, name: String, db := 0.0) -> void: if peer == my_id: _play_sfx(name, false, db) elif networked(): rpc_id(peer, "net_sfx", name, false, db) # --- Netzwerk: Ausgang ---------------------------------------------------- func _sync_out(delta: float) -> void: if not networked(): players[my_id]["s"] = player.s players[my_id]["x"] = player.x players[my_id]["yaw"] = player.yaw return _pose_timer -= delta if _pose_timer <= 0.0: _pose_timer = 1.0 / POSE_HZ if is_server(): players[my_id]["s"] = player.s players[my_id]["x"] = player.x players[my_id]["yaw"] = player.yaw rpc("net_poses", _pose_payload()) else: rpc_id(1, "net_pose", player.s, player.x, player.yaw) if is_server(): _state_timer -= delta if _state_timer <= 0.0: _state_timer = 1.0 / STATE_HZ rpc("net_train", train.progress, train.speed) rpc("net_state", _state_payload()) rpc("net_passengers", _passenger_payload()) func _pose_payload() -> Array: var out: Array = [] for id in players.keys(): var e: Dictionary = players[id] out.append([int(id), float(e["s"]), float(e["x"]), float(e["yaw"]), int(e["carrying"])]) return out func _state_payload() -> Dictionary: var stock: Array = [] for st in stations: stock.append(st.stock) return { "money": money, "rep": reputation, "upg": upgrades.keys(), "driver": driver_id, "cargo": train.cargo.duplicate(), "stock": stock, "loco": train.loco_id, "consist": train.consist.duplicate(), } func _passenger_payload() -> Array: var out: Array = [] for p in passengers: out.append({ "id": p.pid, "st": p.state, "dest": p.dest_station, "fare": p.fare, "chk": p.ticket_checked, "want": p.wants_drink, "srv": p.served, "mood": p.mood, "car": p.seat_car, "seat": p.seat_index, "pos": p.global_position, "yaw": p.global_rotation.y, }) return out # --- Netzwerk: Eingang ---------------------------------------------------- @rpc("any_peer", "call_remote", "unreliable_ordered") func net_pose(s: float, x: float, yaw: float) -> void: var id := multiplayer.get_remote_sender_id() if not players.has(id): players[id] = {"s": s, "x": x, "yaw": yaw, "carrying": 0, "avatar": null} players[id]["s"] = s players[id]["x"] = x players[id]["yaw"] = yaw @rpc("authority", "call_remote", "unreliable_ordered") func net_poses(list: Array) -> void: var seen := {} for e in list: var id := int(e[0]) seen[id] = true if not players.has(id): players[id] = {"s": 0.0, "x": 0.0, "yaw": 0.0, "carrying": 0, "avatar": null} if id != my_id: players[id]["s"] = float(e[1]) players[id]["x"] = float(e[2]) players[id]["yaw"] = float(e[3]) players[id]["carrying"] = int(e[4]) if id == my_id: player.carrying = int(e[4]) for id in players.keys(): if not seen.has(id): _drop_player(int(id)) @rpc("authority", "call_remote", "unreliable_ordered") func net_train(p: float, v: float) -> void: train.apply_snapshot(p, v) @rpc("authority", "call_remote", "reliable") func net_state(d: Dictionary) -> void: money = int(d["money"]) reputation = int(d["rep"]) upgrades.clear() for k in d["upg"]: upgrades[String(k)] = true driver_id = int(d["driver"]) if driver_id != my_id and player.mode == PlayerScript.Mode.DRIVE: player.mode = PlayerScript.Mode.WALK elif driver_id == my_id: player.mode = PlayerScript.Mode.DRIVE var stock: Array = d["stock"] for i in mini(stock.size(), stations.size()): stations[i].stock = float(stock[i]) var loco := String(d["loco"]) var consist: Array = d["consist"] if loco != train.loco_id or consist != Array(train.consist): train.loco_id = loco var typed: Array[String] = [] for c in consist: typed.append(String(c)) train.consist = typed _rebuild_train() train.cargo = (d["cargo"] as Dictionary).duplicate() train.refresh_cargo_visuals() if shop.is_open: shop.refresh() @rpc("authority", "call_remote", "reliable") func net_passengers(list: Array) -> void: for c in train.cars: for sd in c.seats: sd["occupant"] = null var seen := {} for e in list: var pid := int(e["id"]) seen[pid] = true var p = _by_pid.get(pid) if p == null: p = _make_passenger(pid) p.state = int(e["st"]) p.dest_station = int(e["dest"]) p.fare = int(e["fare"]) p.ticket_checked = bool(e["chk"]) p.wants_drink = bool(e["want"]) p.served = bool(e["srv"]) p.mood = float(e["mood"]) p.seat_car = int(e["car"]) p.seat_index = int(e["seat"]) _place_remote_passenger(p, e) p._refresh_label() for pid in _by_pid.keys(): if not seen.has(pid): var p = _by_pid[pid] passengers.erase(p) p.queue_free() _by_pid.erase(pid) @rpc("authority", "call_remote", "reliable") func net_toast(text: String, color: Color) -> void: hud.toast(text, color) @rpc("any_peer", "call_remote", "reliable") func net_sfx(name: String, at_loco: bool, db: float) -> void: _play_sfx(name, at_loco, db) @rpc("any_peer", "call_remote", "unreliable_ordered") func net_controls(th: float, br: bool) -> void: if not is_server(): return if multiplayer.get_remote_sender_id() != driver_id: return train.throttle = th train.braking = br @rpc("any_peer", "call_remote", "reliable") func net_request(action: String, a: String, b: int, c: int) -> void: if not is_server(): return _do_request(multiplayer.get_remote_sender_id(), action, a, b, c) func _place_remote_passenger(p, e: Dictionary) -> void: if p.state == PassengerScript.State.SEATED or p.state == PassengerScript.State.BOARDING: if p.seat_car < 0 or p.seat_car >= train.cars.size(): return var car = train.cars[p.seat_car] if p.seat_index < 0 or p.seat_index >= car.seats.size(): return if p.get_parent() != car: p.reparent(car, false) var seat: Dictionary = car.seats[p.seat_index] seat["occupant"] = p p.position = Vector3(seat["pos"].x, seat["pos"].y + 0.42 - 0.8, seat["pos"].z) p.rotation = Vector3(0, float(seat["yaw"]), 0) p.set_seated(true) else: if p.get_parent() != self: p.reparent(self, false) p.global_position = e["pos"] p.global_rotation = Vector3(0, float(e["yaw"]), 0) p.set_seated(false) func _make_passenger(pid: int): var p := PassengerScript.new() var r := RandomNumberGenerator.new() r.seed = pid * 7919 + 13 p.build(r) p.pid = pid add_child(p) passengers.append(p) _by_pid[pid] = p return p # --- Peers ---------------------------------------------------------------- func _on_peer_connected(id: int) -> void: if not is_server() or not started: return players[id] = {"s": 0.0, "x": 0.0, "yaw": 0.0, "carrying": 0, "avatar": null} rpc_id(id, "net_state", _state_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: _drop_player(id) if driver_id == id: driver_id = 0 if hud: hud.toast("Spieler %d hat den Zug verlassen" % id, Color("ffd978")) func _drop_player(id: int) -> void: if id == my_id or not players.has(id): return var av = players[id]["avatar"] if av != null: av.queue_free() players.erase(id) func _update_avatars() -> void: for id in players.keys(): if int(id) == my_id: continue var e: Dictionary = players[id] if e["avatar"] == null: var av := AvatarScript.new() av.build(int(id), "Crew %d" % int(id)) add_child(av) e["avatar"] = av var av2 = e["avatar"] var loc: Dictionary = train.locate(float(e["s"])) var xf: Transform3D = train.car_transform(int(loc["index"])) av2.global_position = xf * Vector3(float(e["x"]), 1.10, float(loc["local_z"])) av2.global_rotation = Vector3(0, xf.basis.get_euler().y + float(e["yaw"]), 0) av2.set_carrying(int(e["carrying"])) av2.set_label("Crew %d%s" % [int(id), " (Führerstand)" if int(id) == driver_id else ""]) # --- Fahrgäste ----------------------------------------------------------- func _spawn_passengers(delta: float) -> void: _spawn_timer -= delta if _spawn_timer > 0.0: return _spawn_timer = SPAWN_INTERVAL spawn_passenger(_rng.randi() % stations.size()) func spawn_passenger(idx: int, forced_dest := -1) -> bool: var st = stations[idx] if st.waiting.size() >= st.waiting_spots.size(): return false var dest := forced_dest if dest < 0: dest = idx while dest == idx: dest = _rng.randi() % stations.size() var pid := _next_pid _next_pid += 1 var p = _make_passenger(pid) p.origin_station = idx p.dest_station = dest p.fare = _fare(idx, dest) var spot: Vector3 = st.waiting_spots[st.waiting.size()] p.global_position = spot p.global_rotation = Vector3(0, track.sample(st.progress).basis.get_euler().y + PI * 0.5 * st.side, 0) st.waiting.append(p) return true func _fare(a: int, b: int) -> int: var l: float = track.length() var d: float = fposmod(stations[b].progress - stations[a].progress, l) var bonus := 1.0 for c in train.cars: if c.id == "saloon": bonus = 1.5 break return int(round((14.0 + d * 0.11) * bonus)) func _update_passengers(delta: float) -> void: var has_dining: bool = train.has_dining() for p in passengers.duplicate(): match p.state: PassengerScript.State.SEATED: p.tick(delta, has_dining) PassengerScript.State.BOARDING: if p.advance_lerp(delta): _finish_boarding(p) PassengerScript.State.ALIGHTING: if p.advance_lerp(delta): _finish_alighting(p) func _finish_boarding(p) -> void: var car = train.cars[p.seat_car] p.reparent(car, true) var seat: Dictionary = car.seats[p.seat_index] p.position = Vector3(seat["pos"].x, seat["pos"].y + 0.42 - 0.8, seat["pos"].z) p.rotation = Vector3(0, float(seat["yaw"]), 0) p.set_seated(true) p.state = PassengerScript.State.SEATED func _finish_alighting(p) -> void: passengers.erase(p) _by_pid.erase(p.pid) p.queue_free() func _handle_station_stop(delta: float) -> void: var st = train.docked_at if st == null or train.speed > 0.1: _board_timer = 0.0 return _board_timer -= delta if _board_timer > 0.0: return _board_timer = BOARD_INTERVAL for p in passengers: if p.state == PassengerScript.State.SEATED and stations[p.dest_station] == st: _alight(p, st) return if st.waiting.is_empty(): return var seat := _find_free_seat() if seat.is_empty(): return var p2 = st.waiting.pop_front() _reindex_waiting(st) p2.seat_car = int(seat["car"]) p2.seat_index = int(seat["index"]) var car = train.cars[p2.seat_car] var sd: Dictionary = car.seats[p2.seat_index] sd["occupant"] = p2 var target_xf: Transform3D = car.global_transform.translated_local( Vector3(sd["pos"].x, sd["pos"].y + 0.42 - 0.8, sd["pos"].z)) target_xf.basis = car.global_transform.basis.rotated(Vector3.UP, float(sd["yaw"])) p2.set_seated(true) p2.state = PassengerScript.State.BOARDING p2.start_lerp(p2.global_transform, target_xf, 1.4) func _alight(p, st) -> void: var car = train.cars[p.seat_car] car.seats[p.seat_index]["occupant"] = null p.reparent(self, true) p.set_seated(false) var spot: Vector3 = st.waiting_spots[_rng.randi() % st.waiting_spots.size()] p.state = PassengerScript.State.ALIGHTING p.start_lerp(p.global_transform, Transform3D(p.global_transform.basis, spot), 1.4) _settle_fare(p) func _settle_fare(p) -> void: if not p.ticket_checked: reputation = maxi(0, reputation - 2) _broadcast_toast("Schwarzfahrer entkommen. Ansehen -2", Color("ff9a8a")) _broadcast_sfx("deny", false, -10.0) return if p.mood > 0.55: reputation += 1 elif p.mood < 0.35: reputation = maxi(0, reputation - 1) func _reindex_waiting(st) -> void: for i in st.waiting.size(): st.waiting[i].global_position = st.waiting_spots[i] func _find_free_seat() -> Dictionary: for ci in train.cars.size(): var idx: int = train.cars[ci].free_seat() if idx >= 0: return {"car": ci, "index": idx} return {} # --- Aktionen ------------------------------------------------------------- func _on_interact(h: Dictionary) -> void: var kind := String(h["kind"]) audio.play("click", -16.0) match kind: "shop": _open_shop() "drive": request("drive", "", 0, 0) "tap": request("tap", "", 0, 0) "seat": request("seat", "", int(h["car"]), int(h["seat"])) ## Fuehrt eine Aktion aus - lokal beim Host, sonst als Anfrage. func request(action: String, a := "", b := 0, c := 0) -> void: if is_server(): _do_request(my_id, action, a, b, c) else: rpc_id(1, "net_request", action, a, b, c) func _do_request(peer: int, action: String, a: String, b: int, c: int) -> void: match action: "drive": _srv_toggle_drive(peer) "tap": _srv_take_drinks(peer) "seat": _srv_use_seat(peer, b, c) "load": _srv_transfer_one(peer) "buy_loco": _srv_buy_loco(peer, a) "buy_car": _srv_buy_car(peer, a) "sell_car": _srv_sell_car(peer, b) "buy_upgrade": _srv_buy_upgrade(peer, a) "whistle": _srv_whistle(peer) func _toast_to(peer: int, text: String, color: Color) -> void: if peer == my_id: hud.toast(text, color) elif networked(): rpc_id(peer, "net_toast", text, color) func _broadcast_toast(text: String, color: Color) -> void: hud.toast(text, color) if networked(): rpc("net_toast", text, color) func _srv_toggle_drive(peer: int) -> void: if driver_id == peer: driver_id = 0 train.throttle = 0.0 _toast_to(peer, "Steuerung abgegeben", Color("cfd8e0")) elif driver_id == 0: driver_id = peer _toast_to(peer, "Du fährst jetzt. W = Gas, S = Bremse", Color("aef2a0")) else: _toast_to(peer, "Der Führerstand ist besetzt", Color("ffd978")) _sfx_to(peer, "deny", -12.0) if peer == my_id: player.mode = PlayerScript.Mode.DRIVE if driver_id == my_id else PlayerScript.Mode.WALK _push_state() func _srv_whistle(peer: int) -> void: if peer != driver_id: return _broadcast_sfx("whistle", true, -6.0) func _srv_take_drinks(peer: int) -> void: if not train.has_dining(): return var n := 3 if upgrades.has("trolley") else 1 _set_carrying(peer, n) _toast_to(peer, "%d Getränk(e) aufgenommen" % n, Color("8fd8ff")) _sfx_to(peer, "pour", -9.0) func _carrying(peer: int) -> int: return int(players.get(peer, {}).get("carrying", 0)) func _set_carrying(peer: int, n: int) -> void: if not players.has(peer): players[peer] = {"s": 0.0, "x": 0.0, "yaw": 0.0, "carrying": 0, "avatar": null} players[peer]["carrying"] = n if peer == my_id: player.carrying = n func _srv_use_seat(peer: int, car_index: int, seat_index: int) -> void: if car_index < 0 or car_index >= train.cars.size(): return var car = train.cars[car_index] if seat_index < 0 or seat_index >= car.seats.size(): return var occ = car.seats[seat_index]["occupant"] if occ == null: return if not occ.ticket_checked: if upgrades.has("scanner"): _check_all_tickets(peer) else: _check_ticket(occ, peer) return if occ.wants_drink and not occ.served: if _carrying(peer) <= 0: _toast_to(peer, "Kein Getränk dabei. Ab in den Speisewagen!", Color("ffd978")) _sfx_to(peer, "deny", -12.0) return _set_carrying(peer, _carrying(peer) - 1) occ.served = true occ.wants_drink = false occ.mood = minf(occ.mood + 0.4, 1.0) money += DRINK_TIP _toast_to(peer, "Serviert. Trinkgeld +%d €" % DRINK_TIP, Color("aef2a0")) _sfx_to(peer, "clink", -7.0) occ._refresh_label() _push_state() return _toast_to(peer, "%s - %s" % [stations[occ.dest_station].station_name, occ.mood_word()], Color("cfd8e0")) func _check_ticket(p, peer: int) -> void: p.ticket_checked = true money += p.fare _toast_to(peer, "Ticket nach %s +%d €" % [stations[p.dest_station].station_name, p.fare], Color("aef2a0")) _sfx_to(peer, "coin", -6.0) p._refresh_label() _push_state() func _check_all_tickets(peer: int) -> void: var sum := 0 var n := 0 for p in passengers: if p.state == PassengerScript.State.SEATED and not p.ticket_checked: p.ticket_checked = true sum += p.fare n += 1 p._refresh_label() money += sum _toast_to(peer, "%d Tickets gescannt +%d €" % [n, sum], Color("aef2a0")) _sfx_to(peer, "coin", -4.0) _push_state() func _handle_hold_actions(delta: float) -> void: if player.input_locked or player.hotspot.is_empty() \ or String(player.hotspot.get("kind", "")) != "load" \ or not Input.is_key_pressed(KEY_E): _load_timer = 0.0 return if train.docked_at == null or train.speed > 0.1: return _load_timer -= delta if _load_timer > 0.0: return _load_timer = LOAD_INTERVAL * (0.5 if upgrades.has("loader") else 1.0) request("load") func _srv_transfer_one(peer: int) -> void: var st = train.docked_at if st == null or train.speed > 0.1: return for t in st.accepts: if int(train.cargo.get(t, 0)) > 0: train.remove_cargo(int(t), 1) money += Cargo.PRICES[int(t)] delivered_units += 1 if delivered_units % 10 == 0: reputation += 1 _toast_to(peer, "%s geliefert +%d €" % [Cargo.NAMES[int(t)], Cargo.PRICES[int(t)]], Color("aef2a0")) _broadcast_sfx("thud", false, -11.0) _sfx_to(peer, "coin", -12.0) _push_state() return if st.produces >= 0 and st.stock >= 1.0: if train.total_cargo() >= train.cargo_capacity(): _toast_to(peer, "Kein Platz mehr im Güterwagen", Color("ffd978")) _sfx_to(peer, "deny", -12.0) return if train.add_cargo(st.produces, 1) > 0: st.stock -= 1.0 _broadcast_sfx("thud", false, -11.0) _push_state() func _push_state() -> void: if mode == Mode.HOST: rpc("net_state", _state_payload()) # --- Laden ---------------------------------------------------------------- func _open_shop() -> void: player.input_locked = true player.capture_mouse(false) shop.open() func _on_shop_closed() -> void: player.input_locked = false player.capture_mouse(true) func can_buy(price: int, rep: int) -> bool: return money >= price and reputation >= rep func _srv_buy_loco(peer: int, id: String) -> void: if not Defs.LOCOS.has(id): return var d: Dictionary = Defs.LOCOS[id] if id == train.loco_id or not can_buy(int(d["price"]), int(d["rep"])): return money -= int(d["price"]) money += int(Defs.LOCOS[train.loco_id]["sell"]) train.loco_id = id _rebuild_train() _broadcast_toast("Neue Lok: %s" % String(d["name"]), Color("aef2a0")) _broadcast_sfx("coin", false, -5.0) _push_state() func _srv_buy_car(peer: int, id: String) -> void: if not Defs.CARS.has(id): return var d: Dictionary = Defs.CARS[id] if not can_buy(int(d["price"]), int(d["rep"])) or train.consist.size() >= 6: return money -= int(d["price"]) train.consist.append(id) _rebuild_train() _broadcast_toast("Angehaengt: %s" % String(d["name"]), Color("aef2a0")) _push_state() func _srv_sell_car(peer: int, index: int) -> void: if index < 0 or index >= train.consist.size(): return var id: String = train.consist[index] money += int(Defs.CARS[id]["sell"]) train.consist.remove_at(index) _rebuild_train() _broadcast_toast("Verkauft: %s" % Defs.display_name(id), Color("ffd978")) _broadcast_sfx("coin", false, -5.0) _push_state() func _srv_buy_upgrade(peer: int, id: String) -> void: if not Defs.UPGRADES.has(id): return var d: Dictionary = Defs.UPGRADES[id] if upgrades.has(id) or not can_buy(int(d["price"]), int(d["rep"])): return money -= int(d["price"]) upgrades[id] = true _broadcast_toast("Ausbau: %s" % String(d["name"]), Color("aef2a0")) _broadcast_sfx("coin", false, -5.0) _push_state() # Bequeme Namen für Tests und die Ladenoberflaeche. func buy_loco(id: String) -> bool: var before: String = train.loco_id request("buy_loco", id) return train.loco_id != before func buy_car(id: String) -> bool: var before: int = train.consist.size() request("buy_car", id) return train.consist.size() != before func sell_car(index: int) -> bool: var before: int = train.consist.size() request("sell_car", "", index) return train.consist.size() != before func buy_upgrade(id: String) -> bool: var before := upgrades.size() request("buy_upgrade", id) return upgrades.size() != before func _rebuild_train() -> void: # Sitzende Gaeste zwischenparken, sonst verschwinden sie mit dem alten Wagen. var riders: Array = [] for p in passengers.duplicate(): if p.state == PassengerScript.State.SEATED or p.state == PassengerScript.State.BOARDING: p.reparent(self, true) riders.append(p) train.rebuild() for p in riders: var seat := _find_free_seat() if seat.is_empty(): passengers.erase(p) _by_pid.erase(p.pid) p.queue_free() continue p.seat_car = int(seat["car"]) p.seat_index = int(seat["index"]) train.cars[p.seat_car].seats[p.seat_index]["occupant"] = p _finish_boarding(p) cam_rig.target = train.followers[0] player.snap_to_cab() if driver_id == my_id: player.mode = PlayerScript.Mode.DRIVE