update
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
319
scripts/game.gd
319
scripts/game.gd
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
220
scripts/train.gd
220
scripts/train.gd
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
10
tests/run.sh
10
tests/run.sh
@@ -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 ))
|
||||||
|
|||||||
@@ -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
175
tests/switch_test.gd
Normal 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
|
||||||
Reference in New Issue
Block a user