1241 lines
38 KiB
GDScript
1241 lines
38 KiB
GDScript
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 }
|
|
|
|
## Zwei Weichenpunkte, drei Abschnitte: Talstrecke und Bergstrecke fuehren
|
|
## beide von J1 nach J2, die Heimstrecke zurueck.
|
|
const J1 := Vector2(-139, -77)
|
|
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 := [
|
|
["Waldkirch", 0, 0.10, Cargo.Type.WOOD, [Cargo.Type.GRAIN], 1.0, "c9a06a", "8a4b2f", false],
|
|
["Kohlberg", 0, 0.55, Cargo.Type.COAL, [], -1.0, "6f6a63", "40393a", true],
|
|
["Eisenwerk", 0, 0.88, -1, [Cargo.Type.COAL, Cargo.Type.WOOD], 1.0, "9aa0a6", "55606b", true],
|
|
["Grüntal", 2, 0.33, Cargo.Type.GRAIN, [], -1.0, "e8ddc4", "b4562f", false],
|
|
["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 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 := 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
|
|
|
|
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 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 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.continue_chosen.connect(_start_continue)
|
|
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_continue() -> void:
|
|
_load_on_begin = true
|
|
_start_solo()
|
|
|
|
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:
|
|
my_name = menu.player_name()
|
|
_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] = _new_player_entry(my_name)
|
|
menu.hide_menu()
|
|
player.capture_mouse(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 ---------------------------------------------------------------
|
|
|
|
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 tal: int = track.add_segment("Talstrecke", SEG_TAL, T1, T2)
|
|
var berg: int = track.add_segment("Bergstrecke", SEG_BERG, T1, T2)
|
|
var heim: int = track.add_segment("Heimstrecke", SEG_HEIM, T2, T1)
|
|
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:
|
|
for d in STATION_DEFS:
|
|
var st := StationScript.new()
|
|
add_child(st)
|
|
var acc: Array[int] = []
|
|
for t in d[4]:
|
|
acc.append(int(t))
|
|
var seg_id := int(d[1])
|
|
st.setup(String(d[0]), seg_id, track.seg_length(seg_id) * float(d[2]), int(d[3]), acc)
|
|
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:
|
|
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.speed = 0.0
|
|
train.place_head(st.seg, st.progress + train.total_length() * 0.5)
|
|
train._update_dock(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.cars[0]
|
|
cam_rig.snap_to(train.cars[0].global_position)
|
|
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)
|
|
_track_visits()
|
|
_autosave -= delta
|
|
if _autosave <= 0.0:
|
|
_autosave = AUTOSAVE_SEC
|
|
save_game()
|
|
_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
|
|
var sw: int = train.switch_choice
|
|
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 Input.is_key_pressed(KEY_A):
|
|
sw = 0
|
|
elif Input.is_key_pressed(KEY_D):
|
|
sw = 1
|
|
if is_server():
|
|
if driver_id == my_id:
|
|
train.throttle = th
|
|
train.braking = br
|
|
train.switch_choice = sw
|
|
train.drive(delta)
|
|
else:
|
|
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:
|
|
if not started:
|
|
return
|
|
if event is InputEventKey and event.pressed and not event.echo:
|
|
match event.keycode:
|
|
KEY_F5:
|
|
if is_server():
|
|
save_game()
|
|
hud.toast("Gespeichert", Color("8fd8ff"))
|
|
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())
|
|
rpc("net_train", train.seg, train.progress, train.speed, train.switch_choice, train.history)
|
|
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_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)
|
|
var names := {}
|
|
for id in players.keys():
|
|
names[int(id)] = String(players[id]["name"])
|
|
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(),
|
|
"names": names, "goals": goals_done.keys(), "won": won,
|
|
"tickets": tickets_checked, "delivered": delivered_units,
|
|
}
|
|
|
|
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] = _new_player_entry("Crew %d" % id)
|
|
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] = _new_player_entry("Crew %d" % id)
|
|
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(seg_id: int, p: float, v: float, sw: int, hist: Array) -> void:
|
|
train.apply_snapshot(seg_id, p, v, sw, hist)
|
|
|
|
@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"])
|
|
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:
|
|
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, sw: int) -> void:
|
|
if not is_server():
|
|
return
|
|
if multiplayer.get_remote_sender_id() != driver_id:
|
|
return
|
|
train.throttle = th
|
|
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")
|
|
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] = _new_player_entry("Crew %d" % id)
|
|
rpc_id(id, "net_state", _state_payload())
|
|
rpc_id(id, "net_passengers", _passenger_payload())
|
|
|
|
func _on_peer_disconnected(id: int) -> void:
|
|
var pname: String = String(players.get(id, {}).get("name", "Crew %d" % id))
|
|
_drop_player(id)
|
|
if driver_id == id:
|
|
driver_id = 0
|
|
if hud:
|
|
hud.toast("%s hat den Zug verlassen" % pname, 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), String(e["name"]))
|
|
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"]))
|
|
# Zielpose im Wagenraum; Bewegung wird geglättet, die Wagenlage nicht
|
|
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_label("%s%s" % [String(e["name"]), " (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.seg, 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 d: float = stations[a].global_position.distance_to(stations[b].global_position) * 1.6
|
|
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)
|
|
"sell_loco": _srv_sell_loco(peer)
|
|
"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] = _new_player_entry("Crew %d" % peer)
|
|
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
|
|
tickets_checked += 1
|
|
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
|
|
tickets_checked += 1
|
|
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 is_server():
|
|
_check_goals()
|
|
if mode == Mode.HOST:
|
|
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 ----------------------------------------------------------------
|
|
|
|
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()
|
|
|
|
## 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:
|
|
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
|
|
|
|
# --- 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:
|
|
# 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.cars[0]
|
|
player.snap_to_cab()
|
|
if driver_id == my_id:
|
|
player.mode = PlayerScript.Mode.DRIVE
|