first commit
This commit is contained in:
245
tests/audio_test.gd
Normal file
245
tests/audio_test.gd
Normal file
@@ -0,0 +1,245 @@
|
||||
extends Node
|
||||
## Prüft Klangerzeugung und Wiedergabe.
|
||||
var main
|
||||
var step := 0
|
||||
var wait := 0.0
|
||||
var ok := 0
|
||||
var fail := 0
|
||||
var worst := 0.0
|
||||
var elapsed := 0.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 _process(d):
|
||||
if main == null:
|
||||
main = get_tree().current_scene
|
||||
return
|
||||
if not main.started:
|
||||
main._start_solo()
|
||||
return
|
||||
if not main.audio.loaded:
|
||||
elapsed += d
|
||||
worst = maxf(worst, d)
|
||||
return
|
||||
if step == 0:
|
||||
chk("Kein Ruckler beim Erzeugen", worst < 0.2,
|
||||
"längster Frame %.0f ms in %.1f s Ladezeit" % [worst * 1000.0, elapsed])
|
||||
if wait > 0.0:
|
||||
wait -= d
|
||||
return
|
||||
var a = main.audio
|
||||
match step:
|
||||
0:
|
||||
_check_harmony()
|
||||
_check_shapes()
|
||||
_check_loops()
|
||||
var names := ["clack", "clack2", "rumble", "squeal", "whistle", "hiss",
|
||||
"bell", "coin", "pour", "clink", "thud", "step", "click", "deny"]
|
||||
var missing: Array[String] = []
|
||||
for n in names:
|
||||
if not a.sounds.has(n):
|
||||
missing.append(n)
|
||||
chk("Alle Klänge da", missing.is_empty(), "fehlt: %s" % [missing])
|
||||
var silent: Array[String] = []
|
||||
for n in a.sounds.keys():
|
||||
var w: AudioStreamWAV = a.sounds[n]
|
||||
if w.data.size() < 200 or _peak(w) < 1000:
|
||||
silent.append(String(n))
|
||||
chk("Kein Klang ist stumm", silent.is_empty(), "still: %s" % [silent])
|
||||
chk("Musik erzeugt", a._music.stream != null and a._music.playing)
|
||||
chk("Thread beendet", a._thread == null)
|
||||
var m: AudioStreamWAV = a._music.stream
|
||||
chk("Musik ist eine Schleife", m.loop_mode == AudioStreamWAV.LOOP_FORWARD)
|
||||
chk("Musik lang genug", m.data.size() / 2 > 20 * 22050,
|
||||
"%.1f s" % (m.data.size() / 2.0 / 22050.0))
|
||||
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,
|
||||
"db=%.0f" % a._rumble.volume_db)
|
||||
wait = 0.3
|
||||
1:
|
||||
# Losfahren: Rumble muss lauter werden, Radschläge kommen
|
||||
main._srv_toggle_drive(1)
|
||||
a.clacks = 0
|
||||
_key(KEY_W, true)
|
||||
wait = 9.0
|
||||
2:
|
||||
chk("Rumble folgt dem Tempo", a._rumble.volume_db > -30.0,
|
||||
"v=%.1f db=%.0f" % [main.train.speed, a._rumble.volume_db])
|
||||
chk("Radschläge kommen", a.clacks >= 3, "clacks=%d" % a.clacks)
|
||||
_key(KEY_W, false)
|
||||
_key(KEY_S, true)
|
||||
wait = 0.5
|
||||
3:
|
||||
chk("Bremsen quietscht", a._squeal.volume_db > -60.0,
|
||||
"db=%.0f" % a._squeal.volume_db)
|
||||
# Pfeife
|
||||
_key(KEY_H, true); _key(KEY_H, false)
|
||||
wait = 0.2
|
||||
4:
|
||||
var whistling := false
|
||||
for p in a._pool3d:
|
||||
if p.playing and p.stream == a.sounds["whistle"]:
|
||||
whistling = true
|
||||
chk("H pfeift", whistling)
|
||||
wait = 3.0
|
||||
5:
|
||||
_key(KEY_S, false)
|
||||
chk("Zug steht", main.train.speed < 0.2, "v=%.2f" % main.train.speed)
|
||||
chk("Rumble wieder still", a._rumble.volume_db < -40.0,
|
||||
"db=%.0f" % a._rumble.volume_db)
|
||||
_key(KEY_M, true); _key(KEY_M, false)
|
||||
wait = 0.2
|
||||
6:
|
||||
chk("M schaltet Musik aus", not a.music_on and a._music.volume_db < -60.0)
|
||||
_key(KEY_M, true); _key(KEY_M, false)
|
||||
wait = 0.2
|
||||
7:
|
||||
chk("M schaltet Musik wieder an", a.music_on and a._music.volume_db > -30.0)
|
||||
# Ankunftsglocke und Auszischen gezielt auslösen
|
||||
main._last_dock = null
|
||||
main._park_at(main.stations[1])
|
||||
a.last_played = ""
|
||||
main._update_audio(0.016)
|
||||
chk("Glocke bei Ankunft", a.last_played == "bell", "zuletzt=%s" % a.last_played)
|
||||
main._last_speed = 6.0
|
||||
main.train.speed = 0.0
|
||||
a.last_played = ""
|
||||
main._update_audio(0.016)
|
||||
chk("Zischen beim Halten", a.last_played == "hiss", "zuletzt=%s" % a.last_played)
|
||||
wait = 0.2
|
||||
8:
|
||||
print("== %d ok, %d fail ==" % [ok, fail])
|
||||
get_tree().quit(1 if fail > 0 else 0)
|
||||
step += 1
|
||||
|
||||
func _peak(w: AudioStreamWAV) -> int:
|
||||
var d := w.data
|
||||
var n: int = mini(d.size() / 2, 300000)
|
||||
var peak := 0
|
||||
for i in range(0, n, 7):
|
||||
var v: int = absi(d.decode_s16(i * 2))
|
||||
if v > peak:
|
||||
peak = v
|
||||
return peak
|
||||
|
||||
## Kein Ton darf eine kleine Sekunde oder kleine None gegen eine andere
|
||||
## Stimme bilden. Große Septimen sind erlaubt - die gehören zum Klang.
|
||||
## An genau dieser Regel ist das C über G6 aufgefallen.
|
||||
func _check_harmony() -> void:
|
||||
var Sfx = load("res://scripts/sfx.gd")
|
||||
var bad: Array[String] = []
|
||||
for stage in Sfx.PROGRESSION:
|
||||
var voices: Array[float] = []
|
||||
for f in stage["pad"]:
|
||||
voices.append(float(f))
|
||||
voices.append(float(stage["bass"]))
|
||||
# Flächen und Bass untereinander
|
||||
for i in voices.size():
|
||||
for j in range(i + 1, voices.size()):
|
||||
_flag(bad, String(stage["name"]), voices[i], voices[j])
|
||||
# Melodie gegen jede Stimme
|
||||
for f in stage["melody"]:
|
||||
for v in voices:
|
||||
_flag(bad, String(stage["name"]), float(f), v)
|
||||
chk("Keine Halbton- oder None-Reibung", bad.is_empty(), "%s" % [bad])
|
||||
|
||||
func _flag(bad: Array[String], chord: String, a: float, b: float) -> void:
|
||||
var d: int = absi(int(round(12.0 * (log(a / b) / log(2.0)))))
|
||||
if d == 1 or d == 13:
|
||||
bad.append("%s: %s gegen %s (%d HT)" % [chord, _name(_pc(a)), _name(_pc(b)), d])
|
||||
|
||||
func _pc(f: float) -> int:
|
||||
return int(round(69.0 + 12.0 * (log(f / 440.0) / log(2.0)))) % 12
|
||||
|
||||
func _name(pc: int) -> String:
|
||||
return ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][pc]
|
||||
|
||||
## Form der Schlaggeräusche: lang genug und mit Tiefe, nicht nur ein Tick.
|
||||
func _check_shapes() -> void:
|
||||
for n in ["clack", "clack2", "step", "thud"]:
|
||||
var x := _samples(n)
|
||||
chk("%s klingt aus" % n, _tail(x) > 0.18, "%.2f s hörbar" % _tail(x))
|
||||
chk("%s hat Tiefe" % n, _zcr(x, 0, x.size()) < 0.22,
|
||||
"Nulldurchgänge %.3f je Sample" % _zcr(x, 0, x.size()))
|
||||
for n in ["clack", "clack2"]:
|
||||
var r := _double_hit(_samples(n))
|
||||
chk("%s ist ein Doppelschlag" % n, bool(r["ok"]),
|
||||
"zweiter Schlag bei %.3f s, Einbruch davor auf %.0f %%"
|
||||
% [r["at"], float(r["dip"]) * 100.0])
|
||||
|
||||
## Schleifen dürfen an der Naht nicht knicken, und das Quietschen darf
|
||||
## seine Tonhöhe nicht weglaufen lassen.
|
||||
func _check_loops() -> void:
|
||||
for n in ["squeal", "rumble"]:
|
||||
var x := _samples(n)
|
||||
var kink: float = absf(x[0] - (2.0 * x[x.size() - 1] - x[x.size() - 2]))
|
||||
var worst := 0.0
|
||||
for i in range(1, x.size()):
|
||||
worst = maxf(worst, absf(x[i] - x[i - 1]))
|
||||
chk("%s läuft nahtlos" % n, kink < worst * 1.5,
|
||||
"Knick %.4f, größter Schritt %.4f" % [kink, worst])
|
||||
var sq := _samples("squeal")
|
||||
var h: int = sq.size() / 2
|
||||
var a := _zcr(sq, 0, h)
|
||||
var b := _zcr(sq, h, sq.size())
|
||||
chk("Quietschen bleibt auf Tonhöhe", absf(a - b) / maxf(a, 0.0001) < 0.15,
|
||||
"Nulldurchgänge %.3f vorne, %.3f hinten" % [a, b])
|
||||
|
||||
func _samples(name: String) -> PackedFloat32Array:
|
||||
var d: PackedByteArray = (main.audio.sounds[name] as AudioStreamWAV).data
|
||||
var out := PackedFloat32Array()
|
||||
out.resize(d.size() / 2)
|
||||
for i in out.size():
|
||||
out[i] = d.decode_s16(i * 2) / 32768.0
|
||||
return out
|
||||
|
||||
## Sekunde, bis der Klang unter 4 % seines Höchstwerts fällt.
|
||||
func _tail(x: PackedFloat32Array) -> float:
|
||||
var peak := 0.0
|
||||
for v in x:
|
||||
peak = maxf(peak, absf(v))
|
||||
var last := 0
|
||||
for i in x.size():
|
||||
if absf(x[i]) > peak * 0.04:
|
||||
last = i
|
||||
return float(last) / 22050.0
|
||||
|
||||
func _zcr(x: PackedFloat32Array, a: int, b: int) -> float:
|
||||
var c := 0
|
||||
for i in range(a + 1, b):
|
||||
if (x[i] >= 0.0) != (x[i - 1] >= 0.0):
|
||||
c += 1
|
||||
return float(c) / float(maxi(b - a, 1))
|
||||
|
||||
## Sucht einen zweiten, deutlich getrennten Schlag: eine Spitze im
|
||||
## Bereich 0,10-0,25 s, davor ein echter Einbruch der Hüllkurve.
|
||||
func _double_hit(x: PackedFloat32Array) -> Dictionary:
|
||||
var win := 66
|
||||
var env := PackedFloat32Array()
|
||||
env.resize(x.size() / win)
|
||||
var peak := 0.0
|
||||
for i in env.size():
|
||||
var m := 0.0
|
||||
for k in win:
|
||||
m = maxf(m, absf(x[i * win + k]))
|
||||
env[i] = m
|
||||
peak = maxf(peak, m)
|
||||
var a: int = int(0.10 * 22050.0 / win)
|
||||
var b: int = mini(int(0.25 * 22050.0 / win), env.size() - 1)
|
||||
var best := 0.0
|
||||
var best_i := a
|
||||
for i in range(a, b + 1):
|
||||
if env[i] > best:
|
||||
best = env[i]
|
||||
best_i = i
|
||||
var dip := peak
|
||||
for i in range(int(0.03 * 22050.0 / win), best_i):
|
||||
dip = minf(dip, env[i])
|
||||
var ok: bool = best > peak * 0.22 and dip < best * 0.6
|
||||
return {"ok": ok, "at": float(best_i * win) / 22050.0, "dip": dip / maxf(best, 0.0001)}
|
||||
97
tests/direction_test.gd
Normal file
97
tests/direction_test.gd
Normal file
@@ -0,0 +1,97 @@
|
||||
extends Node
|
||||
## Prüft Bewegungs- und Blickrichtung gegen die Kamera-Achsen.
|
||||
var main
|
||||
var step := 0
|
||||
var wait := 0.0
|
||||
var ok := 0
|
||||
var fail := 0
|
||||
var pos0 := Vector3.ZERO
|
||||
var fwd0 := Vector3.ZERO
|
||||
var right0 := Vector3.ZERO
|
||||
|
||||
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 _mouse(dx: float, dy: float) -> void:
|
||||
var e := InputEventMouseMotion.new()
|
||||
e.relative = Vector2(dx, dy)
|
||||
e.position = get_viewport().get_visible_rect().size * 0.5
|
||||
e.global_position = e.position
|
||||
Input.parse_input_event(e)
|
||||
|
||||
func snap() -> void:
|
||||
var c = main.player.camera
|
||||
pos0 = main.player.global_position
|
||||
fwd0 = -c.global_basis.z # Blickrichtung
|
||||
right0 = c.global_basis.x # Bildschirm rechts
|
||||
|
||||
func moved() -> Vector3:
|
||||
return main.player.global_position - pos0
|
||||
|
||||
func _process(d):
|
||||
if main == null:
|
||||
main = get_tree().current_scene
|
||||
return
|
||||
if not main.started:
|
||||
main._start_solo()
|
||||
return
|
||||
if wait > 0.0:
|
||||
wait -= d
|
||||
return
|
||||
var p = main.player
|
||||
match step:
|
||||
0:
|
||||
# Freie Fläche im Personenwagen, quer zur Fahrtrichtung schauen
|
||||
var car = main.train.cars[1]
|
||||
p.s = car.center_s
|
||||
p.x = 0.0
|
||||
p.yaw = 0.0
|
||||
p.pitch = 0.0
|
||||
wait = 0.3
|
||||
1:
|
||||
snap(); _key(KEY_W, true); wait = 0.5
|
||||
2:
|
||||
_key(KEY_W, false)
|
||||
chk("W läuft nach vorn", moved().dot(fwd0) > 0.2,
|
||||
"%.2f" % moved().dot(fwd0))
|
||||
wait = 0.3
|
||||
3:
|
||||
snap(); _key(KEY_S, true); wait = 0.5
|
||||
4:
|
||||
_key(KEY_S, false)
|
||||
chk("S läuft rückwärts", moved().dot(fwd0) < -0.2,
|
||||
"%.2f" % moved().dot(fwd0))
|
||||
wait = 0.3
|
||||
5:
|
||||
snap(); _key(KEY_D, true); wait = 0.4
|
||||
6:
|
||||
_key(KEY_D, false)
|
||||
chk("D läuft nach rechts", moved().dot(right0) > 0.15,
|
||||
"rechts=%.2f (negativ = falsche Seite)" % moved().dot(right0))
|
||||
wait = 0.3
|
||||
7:
|
||||
snap(); _key(KEY_A, true); wait = 0.4
|
||||
8:
|
||||
_key(KEY_A, false)
|
||||
chk("A läuft nach links", moved().dot(right0) < -0.15,
|
||||
"rechts=%.2f" % moved().dot(right0))
|
||||
wait = 0.3
|
||||
9:
|
||||
snap(); _mouse(150.0, 0.0); wait = 0.3
|
||||
10:
|
||||
var f = -p.camera.global_basis.z
|
||||
chk("Maus rechts dreht nach rechts", f.dot(right0) > 0.1,
|
||||
"%.2f" % f.dot(right0))
|
||||
snap(); _mouse(0.0, 150.0); wait = 0.3
|
||||
11:
|
||||
var f2 = -p.camera.global_basis.z
|
||||
chk("Maus runter schaut runter", f2.y < fwd0.y - 0.05,
|
||||
"y %.2f -> %.2f" % [fwd0.y, f2.y])
|
||||
print("== %d ok, %d fail ==" % [ok, fail])
|
||||
get_tree().quit(1 if fail > 0 else 0)
|
||||
step += 1
|
||||
1
tests/direction_test.gd.uid
Normal file
1
tests/direction_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bnbo2gtpe5mtw
|
||||
289
tests/input_test.gd
Normal file
289
tests/input_test.gd
Normal file
@@ -0,0 +1,289 @@
|
||||
extends Node
|
||||
## Prüft die tatsächliche Eingabekette: Events -> GUI -> _unhandled_input.
|
||||
var main
|
||||
var step := 0
|
||||
var wait := 0.0
|
||||
var ok := 0
|
||||
var fail := 0
|
||||
var y0 := 0.0
|
||||
var p0 := 0.0
|
||||
var s0 := 0.0
|
||||
var x0 := 0.0
|
||||
var m0 := 0
|
||||
var n0 := 0
|
||||
|
||||
func chk(name: String, cond: bool, info := "") -> void:
|
||||
if cond:
|
||||
ok += 1
|
||||
print(" OK %s %s" % [name, info])
|
||||
else:
|
||||
fail += 1
|
||||
print(" FAIL %s %s" % [name, info])
|
||||
|
||||
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 _mouse(dx: float, dy: float) -> void:
|
||||
var e := InputEventMouseMotion.new()
|
||||
e.relative = Vector2(dx, dy)
|
||||
e.position = get_viewport().get_visible_rect().size * 0.5
|
||||
e.global_position = e.position
|
||||
Input.parse_input_event(e)
|
||||
|
||||
## Stellt den Spieler vor einen Hotspot und laesst ihn hinsehen.
|
||||
func goto(car_index: int, kind: String) -> bool:
|
||||
var car = main.train.cars[car_index]
|
||||
for h in car.hotspots:
|
||||
if String(h["kind"]) != kind:
|
||||
continue
|
||||
var pos: Vector3 = h["pos"]
|
||||
var hs: float = car.center_s + pos.z
|
||||
var p = main.player
|
||||
for dz in [-1.0, 1.0, -1.4, 1.4, -0.7, 0.7]:
|
||||
for px in [pos.x * 0.35, 0.0, pos.x * 0.6, -pos.x * 0.35]:
|
||||
if p._blocked(px, hs + dz):
|
||||
continue
|
||||
p.x = px
|
||||
p.s = hs + dz
|
||||
p.yaw = atan2(pos.x - px, hs - p.s)
|
||||
p.pitch = clampf(atan2(pos.y - (2.72), Vector2(pos.x - px, hs - p.s).length()), -1.2, 1.2)
|
||||
return true
|
||||
return false
|
||||
|
||||
func _process(d):
|
||||
if main == null:
|
||||
main = get_tree().current_scene
|
||||
return
|
||||
if not main.started:
|
||||
main._start_solo()
|
||||
return
|
||||
if wait > 0.0:
|
||||
wait -= d
|
||||
return
|
||||
var p = main.player
|
||||
match step:
|
||||
0:
|
||||
chk("Maus gefangen", Input.mouse_mode == Input.MOUSE_MODE_CAPTURED,
|
||||
"mode=%d" % Input.mouse_mode)
|
||||
y0 = p.yaw
|
||||
p0 = p.pitch
|
||||
_mouse(120.0, 60.0)
|
||||
wait = 0.3
|
||||
1:
|
||||
chk("Mausblick horizontal", absf(p.yaw - y0) > 0.05,
|
||||
"yaw %.3f -> %.3f" % [y0, p.yaw])
|
||||
chk("Mausblick vertikal", absf(p.pitch - p0) > 0.05,
|
||||
"pitch %.3f -> %.3f" % [p0, p.pitch])
|
||||
p.yaw = 0.0
|
||||
p.pitch = 0.0
|
||||
s0 = p.s
|
||||
_key(KEY_W, true)
|
||||
wait = 1.0
|
||||
2:
|
||||
_key(KEY_W, false)
|
||||
chk("Vorwärts laufen", p.s - s0 > 0.5, "ds=%.2f" % (p.s - s0))
|
||||
x0 = p.x
|
||||
_key(KEY_D, true)
|
||||
wait = 0.6
|
||||
3:
|
||||
_key(KEY_D, false)
|
||||
chk("Seitwärts laufen", absf(p.x - x0) > 0.2, "dx=%.2f" % (p.x - x0))
|
||||
# Ans Fahrpult stellen und E drücken
|
||||
var loco = main.train.cars[0]
|
||||
p.s = loco.center_s + loco.inner_z1 - 1.7
|
||||
p.x = 0.0
|
||||
p.yaw = 0.0
|
||||
p.pitch = 0.0
|
||||
wait = 0.3
|
||||
4:
|
||||
chk("Hotspot erkannt", String(p.hotspot.get("kind", "")) == "drive",
|
||||
"kind=%s" % String(p.hotspot.get("kind", "-")))
|
||||
_key(KEY_E, true)
|
||||
wait = 0.3
|
||||
5:
|
||||
_key(KEY_E, false)
|
||||
chk("E übernimmt Steuerung", main.driver_id == 1, "driver=%d" % main.driver_id)
|
||||
chk("Spieler im Fahrmodus", p.mode == 1, "mode=%d" % p.mode)
|
||||
_key(KEY_W, true)
|
||||
wait = 1.5
|
||||
6:
|
||||
_key(KEY_W, false)
|
||||
chk("W gibt Gas", main.train.speed > 1.0, "v=%.1f" % main.train.speed)
|
||||
_key(KEY_S, true)
|
||||
wait = 3.0
|
||||
7:
|
||||
_key(KEY_S, false)
|
||||
chk("S bremst", main.train.speed < 0.05, "v=%.2f" % main.train.speed)
|
||||
_key(KEY_E, true)
|
||||
wait = 0.3
|
||||
8:
|
||||
_key(KEY_E, false)
|
||||
chk("E gibt Steuerung frei", main.driver_id == 0)
|
||||
_key(KEY_V, true)
|
||||
_key(KEY_V, false)
|
||||
wait = 0.3
|
||||
9:
|
||||
chk("V schaltet auf Außenkamera", main.cam_rig.is_active())
|
||||
_key(KEY_V, true)
|
||||
_key(KEY_V, false)
|
||||
wait = 0.3
|
||||
10:
|
||||
chk("V schaltet zurück", p.camera.current)
|
||||
_key(KEY_ESCAPE, true)
|
||||
_key(KEY_ESCAPE, false)
|
||||
wait = 0.3
|
||||
11:
|
||||
chk("Esc gibt Maus frei", Input.mouse_mode != Input.MOUSE_MODE_CAPTURED)
|
||||
p.capture_mouse(true)
|
||||
# Zweiter Spieler darf nicht dazwischenfunken
|
||||
main._srv_toggle_drive(1)
|
||||
main._srv_toggle_drive(2)
|
||||
chk("Führerstand exklusiv", main.driver_id == 1, "driver=%d" % main.driver_id)
|
||||
main._srv_toggle_drive(1)
|
||||
# Zug mit Speisewagen und Gästen ausstatten
|
||||
main.train.consist = ["coach", "dining", "freight"] as Array[String]
|
||||
main._rebuild_train()
|
||||
main._park_at(main.stations[0])
|
||||
for i in 4:
|
||||
main.spawn_passenger(0, 2)
|
||||
wait = 7.0
|
||||
12:
|
||||
chk("Gäste eingestiegen", _seated() >= 4, "seated=%d" % _seated())
|
||||
chk("Zum Depot-Terminal gelaufen", goto(0, "shop"))
|
||||
wait = 0.3
|
||||
13:
|
||||
chk("Terminal im Blick", String(p.hotspot.get("kind", "")) == "shop",
|
||||
"kind=%s" % String(p.hotspot.get("kind", "-")))
|
||||
_key(KEY_E, true); _key(KEY_E, false)
|
||||
wait = 0.4
|
||||
14:
|
||||
chk("Terminal offen", main.shop.is_open)
|
||||
main.shop._tabs.current_tab = 1
|
||||
wait = 0.3
|
||||
15:
|
||||
var btn := _find_button(main.shop, "Kaufen")
|
||||
chk("Kaufen-Knopf sichtbar", btn != null)
|
||||
m0 = main.money
|
||||
n0 = main.train.consist.size()
|
||||
if btn != null:
|
||||
_click(btn.get_global_rect().get_center())
|
||||
wait = 0.5
|
||||
16:
|
||||
chk("Klick im Laden kauft", main.train.consist.size() == n0 + 1 and main.money < m0,
|
||||
"consist %d->%d, geld %d->%d" % [n0, main.train.consist.size(), m0, main.money])
|
||||
_key(KEY_ESCAPE, true); _key(KEY_ESCAPE, false)
|
||||
wait = 0.4
|
||||
17:
|
||||
chk("Terminal geschlossen", not main.shop.is_open)
|
||||
chk("Gäste überleben den Umbau", _seated() >= 4, "seated=%d" % _seated())
|
||||
chk("Maus wieder gefangen", Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
|
||||
chk("Zur Theke gelaufen", goto(_car_of("tap"), "tap"))
|
||||
wait = 0.3
|
||||
18:
|
||||
chk("Theke im Blick", String(p.hotspot.get("kind", "")) == "tap",
|
||||
"kind=%s" % String(p.hotspot.get("kind", "-")))
|
||||
_key(KEY_E, true); _key(KEY_E, false)
|
||||
wait = 0.3
|
||||
19:
|
||||
chk("Getränk aufgenommen", p.carrying > 0, "carrying=%d" % p.carrying)
|
||||
chk("Zum Gast gelaufen", _goto_occupied_seat())
|
||||
wait = 0.3
|
||||
20:
|
||||
chk("Gast im Blick", String(p.hotspot.get("kind", "")) == "seat",
|
||||
"kind=%s" % String(p.hotspot.get("kind", "-")))
|
||||
m0 = main.money
|
||||
_key(KEY_E, true); _key(KEY_E, false)
|
||||
wait = 0.3
|
||||
21:
|
||||
chk("Ticket bringt Geld", main.money > m0, "+%d €" % (main.money - m0))
|
||||
var occ = _seat_occupant()
|
||||
chk("Gast erreichbar", occ != null)
|
||||
if occ != null:
|
||||
occ.wants_drink = true
|
||||
occ.served = false
|
||||
m0 = main.money
|
||||
_key(KEY_E, true); _key(KEY_E, false)
|
||||
wait = 0.3
|
||||
22:
|
||||
var occ2 = _seat_occupant()
|
||||
chk("Serviert", occ2 != null and occ2.served)
|
||||
chk("Trinkgeld", main.money > m0, "+%d €" % (main.money - m0))
|
||||
chk("Getränk verbraucht", p.carrying == 0, "carrying=%d" % p.carrying)
|
||||
chk("Zur Ladekonsole gelaufen", goto(_car_of("load"), "load"))
|
||||
wait = 0.3
|
||||
23:
|
||||
chk("Konsole im Blick", String(p.hotspot.get("kind", "")) == "load",
|
||||
"kind=%s" % String(p.hotspot.get("kind", "-")))
|
||||
main.stations[0].stock = 10.0
|
||||
_key(KEY_E, true)
|
||||
wait = 2.5
|
||||
24:
|
||||
_key(KEY_E, false)
|
||||
chk("E halten lädt Ware", main.train.total_cargo() > 0,
|
||||
"cargo=%d" % main.train.total_cargo())
|
||||
print("== %d ok, %d fail ==" % [ok, fail])
|
||||
get_tree().quit(1 if fail > 0 else 0)
|
||||
step += 1
|
||||
|
||||
func _seated() -> int:
|
||||
var n := 0
|
||||
for pa in main.passengers:
|
||||
if pa.state == 2:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
func _seat_occupant():
|
||||
var h = main.player.hotspot
|
||||
if String(h.get("kind", "")) != "seat":
|
||||
return null
|
||||
return main.train.cars[int(h["car"])].seats[int(h["seat"])]["occupant"]
|
||||
|
||||
func _click(pos: Vector2) -> void:
|
||||
for pressed in [true, false]:
|
||||
var e := InputEventMouseButton.new()
|
||||
e.button_index = MOUSE_BUTTON_LEFT
|
||||
e.pressed = pressed
|
||||
e.position = pos
|
||||
e.global_position = pos
|
||||
Input.parse_input_event(e)
|
||||
|
||||
func _find_button(n: Node, text: String) -> Button:
|
||||
if n is Button and (n as Button).text == text and not (n as Button).disabled:
|
||||
return n
|
||||
for c in n.get_children():
|
||||
var r := _find_button(c, text)
|
||||
if r != null:
|
||||
return r
|
||||
return null
|
||||
|
||||
func _car_of(kind: String) -> int:
|
||||
for ci in main.train.cars.size():
|
||||
for h in main.train.cars[ci].hotspots:
|
||||
if String(h["kind"]) == kind:
|
||||
return ci
|
||||
return -1
|
||||
|
||||
func _goto_occupied_seat() -> bool:
|
||||
var pl = main.player
|
||||
for ci in main.train.cars.size():
|
||||
var car = main.train.cars[ci]
|
||||
for h in car.hotspots:
|
||||
if String(h["kind"]) != "seat":
|
||||
continue
|
||||
if car.seats[int(h["seat"])]["occupant"] == null:
|
||||
continue
|
||||
var pos: Vector3 = h["pos"]
|
||||
var hs: float = car.center_s + pos.z
|
||||
for dz in [0.0, -0.8, 0.8, -1.2, 1.2]:
|
||||
if pl._blocked(0.0, hs + dz):
|
||||
continue
|
||||
pl.x = 0.0
|
||||
pl.s = hs + dz
|
||||
pl.yaw = atan2(pos.x, hs - pl.s)
|
||||
pl.pitch = 0.0
|
||||
return true
|
||||
return false
|
||||
1
tests/input_test.gd.uid
Normal file
1
tests/input_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bip2svfktigjy
|
||||
101
tests/net_test.gd
Normal file
101
tests/net_test.gd
Normal file
@@ -0,0 +1,101 @@
|
||||
extends Node
|
||||
## Netzwerktest. Rolle über: -- --host bzw. -- --client
|
||||
var main
|
||||
var role := ""
|
||||
var step := 0
|
||||
var wait := 0.0
|
||||
var ok := 0
|
||||
var fail := 0
|
||||
var m0 := 0
|
||||
|
||||
func _ready() -> void:
|
||||
for a in OS.get_cmdline_user_args():
|
||||
if a == "--host":
|
||||
role = "host"
|
||||
elif a == "--client":
|
||||
role = "client"
|
||||
|
||||
func chk(name: String, cond: bool, info := "") -> void:
|
||||
if cond:
|
||||
ok += 1
|
||||
print("[%s] OK %s %s" % [role, name, info])
|
||||
else:
|
||||
fail += 1
|
||||
print("[%s] FAIL %s %s" % [role, name, info])
|
||||
|
||||
func _process(d):
|
||||
if main == null:
|
||||
main = get_tree().current_scene
|
||||
return
|
||||
if not main.started:
|
||||
if role == "host":
|
||||
main._start_host(7788)
|
||||
elif role == "client" and step == 0:
|
||||
main._start_join("127.0.0.1", 7788)
|
||||
step = 1
|
||||
return
|
||||
if wait > 0.0:
|
||||
wait -= d
|
||||
return
|
||||
if role == "host":
|
||||
_host_step()
|
||||
else:
|
||||
_client_step()
|
||||
|
||||
func _host_step() -> void:
|
||||
match step:
|
||||
0:
|
||||
print("[host] Sitzung gestartet")
|
||||
for i in 4:
|
||||
main.spawn_passenger(0, 2)
|
||||
wait = 30.0
|
||||
1:
|
||||
print("[host] Ende. money=%d driver=%d" % [main.money, main.driver_id])
|
||||
get_tree().quit()
|
||||
step += 1
|
||||
|
||||
func _client_step() -> void:
|
||||
match step:
|
||||
1:
|
||||
wait = 6.0
|
||||
2:
|
||||
chk("Welt gebaut", main.stations.size() == 5)
|
||||
chk("Zug übernommen", main.train.cars.size() == 3, "cars=%d" % main.train.cars.size())
|
||||
chk("Eigene Peer-ID", main.my_id > 1)
|
||||
chk("Fahrgäste sichtbar", main.passengers.size() >= 4, "n=%d" % main.passengers.size())
|
||||
var seated := 0
|
||||
for p in main.passengers:
|
||||
if p.state == 2:
|
||||
seated += 1
|
||||
chk("Gäste sitzen", seated >= 4, "seated=%d" % seated)
|
||||
chk("Geld synchron", main.money == 1500)
|
||||
main.request("drive")
|
||||
wait = 1.5
|
||||
3:
|
||||
chk("Steuerung erhalten", main.driver_id == main.my_id)
|
||||
m0 = main.money
|
||||
var done := false
|
||||
for ci in main.train.cars.size():
|
||||
for si in main.train.cars[ci].seats.size():
|
||||
var occ = main.train.cars[ci].seats[si]["occupant"]
|
||||
if occ != null and not occ.ticket_checked:
|
||||
main.request("seat", "", ci, si)
|
||||
done = true
|
||||
break
|
||||
if done:
|
||||
break
|
||||
chk("Ticket-Anfrage gesendet", done)
|
||||
wait = 1.5
|
||||
4:
|
||||
chk("Geld gestiegen", main.money > m0, "%d -> %d" % [m0, main.money])
|
||||
main.request("buy_car", "coach")
|
||||
wait = 1.5
|
||||
5:
|
||||
chk("Kauf synchron", main.train.consist.size() == 3)
|
||||
chk("Zug neu gebaut", main.train.cars.size() == 4)
|
||||
wait = 2.0
|
||||
6:
|
||||
chk("Kein Selbstlauf ohne Gas", main.train.speed < 0.01)
|
||||
print("[client] == %d ok, %d fail ==" % [ok, fail])
|
||||
get_tree().quit(1 if fail > 0 else 0)
|
||||
step += 1
|
||||
1
tests/net_test.gd.uid
Normal file
1
tests/net_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dodlor1js5f2c
|
||||
54
tests/run.sh
Executable file
54
tests/run.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Führt die Testskripte aus. Sie werden als Autoload eingehängt und danach
|
||||
# wird das Projekt wieder in den Ausgangszustand gebracht.
|
||||
set -u
|
||||
GODOT="${GODOT:-$HOME/bin/godot}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
restore() { git checkout project.godot 2>/dev/null || cp /tmp/_pg.bak project.godot; rm -f scripts/_test.gd /tmp/_pg.bak; }
|
||||
cp project.godot /tmp/_pg.bak
|
||||
trap restore EXIT
|
||||
|
||||
install_test() {
|
||||
cp "$1" scripts/_test.gd
|
||||
cp /tmp/_pg.bak project.godot
|
||||
printf '\n[autoload]\n\nTest="*res://scripts/_test.gd"\n' >> project.godot
|
||||
}
|
||||
|
||||
echo "=== Einzelspieler-Tests ==="
|
||||
install_test tests/solo_test.gd
|
||||
"$GODOT" --headless --path . --fixed-fps 60 2>&1 | grep -E "OK|FAIL|=="
|
||||
SOLO=${PIPESTATUS[0]}
|
||||
|
||||
echo
|
||||
echo "=== Eingabe-Tests (braucht ein Fenster) ==="
|
||||
install_test tests/input_test.gd
|
||||
"$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|=="
|
||||
INPUT=${PIPESTATUS[0]}
|
||||
|
||||
echo
|
||||
echo "=== Klang-Tests ==="
|
||||
install_test tests/audio_test.gd
|
||||
"$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|=="
|
||||
AUDIO=${PIPESTATUS[0]}
|
||||
|
||||
echo
|
||||
echo "=== Richtungs-Tests ==="
|
||||
install_test tests/direction_test.gd
|
||||
"$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|=="
|
||||
DIR=${PIPESTATUS[0]}
|
||||
|
||||
echo
|
||||
echo "=== Netzwerk-Tests ==="
|
||||
install_test tests/net_test.gd
|
||||
"$GODOT" --headless --path . -- --host > /tmp/_host.log 2>&1 &
|
||||
HOST=$!
|
||||
sleep 6
|
||||
"$GODOT" --headless --path . -- --client 2>&1 | grep -E "OK|FAIL|=="
|
||||
NET=${PIPESTATUS[0]}
|
||||
wait $HOST 2>/dev/null
|
||||
|
||||
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."
|
||||
exit $(( SOLO || INPUT || DIR || AUDIO || NET ))
|
||||
158
tests/solo_test.gd
Normal file
158
tests/solo_test.gd
Normal file
@@ -0,0 +1,158 @@
|
||||
extends Node
|
||||
var main
|
||||
var step := 0
|
||||
var wait := 0.0
|
||||
var ok := 0
|
||||
var fail := 0
|
||||
var m0 := 0
|
||||
|
||||
func chk(name: String, cond: bool, info := "") -> void:
|
||||
if cond:
|
||||
ok += 1
|
||||
print(" OK %s %s" % [name, info])
|
||||
else:
|
||||
fail += 1
|
||||
print(" FAIL %s %s" % [name, info])
|
||||
|
||||
func seated() -> int:
|
||||
var n := 0
|
||||
for p in main.passengers:
|
||||
if p.state == 2:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
func _unchecked() -> int:
|
||||
var n := 0
|
||||
for p in main.passengers:
|
||||
if p.state == 2 and not p.ticket_checked:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
func _key(code: int, down: bool) -> void:
|
||||
var e := InputEventKey.new()
|
||||
e.physical_keycode = code
|
||||
e.keycode = code
|
||||
e.pressed = down
|
||||
Input.parse_input_event(e)
|
||||
|
||||
func _process(d):
|
||||
if main == null:
|
||||
main = get_tree().current_scene
|
||||
return
|
||||
if not main.started:
|
||||
main._start_solo()
|
||||
return
|
||||
if wait > 0.0:
|
||||
wait -= d
|
||||
return
|
||||
match step:
|
||||
0:
|
||||
print("== Test start ==")
|
||||
chk("Zug gebaut", main.train.cars.size() == 3, "cars=%d" % main.train.cars.size())
|
||||
chk("Sitze", main.train.seat_count() == 8)
|
||||
chk("Ladekapazität", main.train.cargo_capacity() == 10)
|
||||
chk("Am Bahnhof", main.train.docked_at == main.stations[0])
|
||||
var wr = main.train.walk_range()
|
||||
chk("Begehbarer Bereich", wr.y - wr.x > 25.0, "%.1f m" % (wr.y - wr.x))
|
||||
chk("Spieler startet im Führerstand", main.train.locate(main.player.s)["index"] == 0)
|
||||
for i in 6:
|
||||
main.spawn_passenger(0, 2)
|
||||
chk("Wartende", main.stations[0].waiting.size() == 6)
|
||||
wait = 8.0
|
||||
1:
|
||||
chk("Eingestiegen", seated() == 6, "seated=%d" % seated())
|
||||
m0 = main.money
|
||||
for p in main.passengers:
|
||||
if p.state == 2:
|
||||
main._check_ticket(p, 1)
|
||||
chk("Ticketgeld", main.money > m0, "+%d €" % (main.money - m0))
|
||||
chk("Alle kontrolliert", _unchecked() == 0)
|
||||
2:
|
||||
m0 = main.money
|
||||
var st = main.stations[0]
|
||||
st.stock = 12.0
|
||||
for i in 10:
|
||||
main._srv_transfer_one(1)
|
||||
chk("Ladung aufgenommen", main.train.total_cargo() == 10)
|
||||
chk("Lager geleert", st.stock < 3.0, "stock=%.1f" % st.stock)
|
||||
3:
|
||||
main._park_at(main.stations[2])
|
||||
wait = 0.3
|
||||
4:
|
||||
chk("Am Eisenwerk", main.train.docked_at == main.stations[2])
|
||||
m0 = main.money
|
||||
for i in 12:
|
||||
main._srv_transfer_one(1)
|
||||
chk("Ware verkauft", main.train.total_cargo() == 0)
|
||||
chk("Erlös", main.money - m0 >= 100, "+%d €" % (main.money - m0))
|
||||
wait = 8.0
|
||||
5:
|
||||
var still := 0
|
||||
for p in main.passengers:
|
||||
if p.state == 2 and p.dest_station == 2:
|
||||
still += 1
|
||||
chk("Ausgestiegen", still == 0)
|
||||
chk("Ansehen gestiegen", main.reputation > 0, "rep=%d" % main.reputation)
|
||||
main.money = 20000
|
||||
main.reputation = 50
|
||||
var n0 = main.train.consist.size()
|
||||
main.buy_car("dining")
|
||||
chk("Speisewagen gekauft", main.train.consist.size() == n0 + 1)
|
||||
chk("Speisewagen erkannt", main.train.has_dining())
|
||||
main.buy_car("saloon")
|
||||
chk("Salonwagen gekauft", main.train.seat_count() == 22)
|
||||
wait = 0.2
|
||||
6:
|
||||
var v0 = main.train.max_speed()
|
||||
main.buy_loco("express")
|
||||
chk("Lok getauscht", main.train.loco_id == "express")
|
||||
chk("Schneller", main.train.max_speed() > v0)
|
||||
chk("Ausbau", main.buy_upgrade("scanner"))
|
||||
chk("Ausbau nicht doppelt", not main.buy_upgrade("scanner"))
|
||||
var before = main.train.consist.size()
|
||||
main.sell_car(0)
|
||||
chk("Wagen verkauft", main.train.consist.size() == before - 1)
|
||||
wait = 0.3
|
||||
7:
|
||||
chk("Spieler im Zug", main.train.locate(main.player.s)["index"] == 0)
|
||||
main._srv_toggle_drive(1)
|
||||
chk("Steuerung übernommen", main.driver_id == 1)
|
||||
_key(KEY_W, true)
|
||||
wait = 3.0
|
||||
8:
|
||||
chk("Zug fährt", main.train.speed > 3.0, "v=%.1f m/s" % main.train.speed)
|
||||
_key(KEY_W, false)
|
||||
_key(KEY_S, true)
|
||||
wait = 4.0
|
||||
9:
|
||||
chk("Zug bremst", main.train.speed < 0.01, "v=%.2f" % main.train.speed)
|
||||
_key(KEY_S, false)
|
||||
# Durch den ganzen Zug laufen: hinten anfangen, nach vorn gehen.
|
||||
main._srv_toggle_drive(1)
|
||||
chk("Steuerung abgegeben", main.driver_id == 0)
|
||||
var span = main.train.walk_range()
|
||||
main.player.s = span.x + 0.2
|
||||
main.player.x = 0.0
|
||||
main.player.yaw = 0.0
|
||||
_key(KEY_W, true)
|
||||
_key(KEY_SHIFT, true)
|
||||
wait = 25.0
|
||||
10:
|
||||
_key(KEY_W, false)
|
||||
_key(KEY_SHIFT, false)
|
||||
var span2 = main.train.walk_range()
|
||||
chk("Ganzer Zug begehbar", main.player.s > span2.y - 1.0,
|
||||
"s=%.1f von %.1f" % [main.player.s, span2.y])
|
||||
chk("Im Führerstand angekommen", main.train.locate(main.player.s)["index"] == 0)
|
||||
main.player.x = 0.0
|
||||
_key(KEY_D, true)
|
||||
wait = 3.0
|
||||
11:
|
||||
_key(KEY_D, false)
|
||||
chk("Wand hält den Spieler", absf(main.player.x) < 1.1, "x=%.2f" % main.player.x)
|
||||
main.money = 200
|
||||
chk("Kein Kauf ohne Geld", not main.buy_car("saloon"))
|
||||
chk("Kein Kauf ohne Ansehen", not main.buy_upgrade("trolley") or main.reputation >= 20)
|
||||
print("== %d ok, %d fail ==" % [ok, fail])
|
||||
get_tree().quit(1 if fail > 0 else 0)
|
||||
step += 1
|
||||
1
tests/solo_test.gd.uid
Normal file
1
tests/solo_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bvosfi7jnndvs
|
||||
Reference in New Issue
Block a user