469 lines
17 KiB
GDScript
469 lines
17 KiB
GDScript
extends Node
|
|
## Fährt das echte Spiel headless und prüft die Implementierungen.
|
|
|
|
var fehler := 0
|
|
var main: Node3D
|
|
|
|
|
|
func p(ok: bool, text: String) -> void:
|
|
print((" OK " if ok else " FEHLT ") + text)
|
|
if not ok:
|
|
fehler += 1
|
|
|
|
|
|
func abschnitt(t: String) -> void:
|
|
print("\n== " + t)
|
|
|
|
|
|
func _ready() -> void:
|
|
seed(20240)
|
|
main = load("res://scenes/main.tscn").instantiate()
|
|
add_child(main)
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
|
|
await test_start()
|
|
await test_bibliothek()
|
|
await test_gebaeude_betreten()
|
|
await test_mission_annehmen()
|
|
await test_reise_zur_erde()
|
|
await test_tagesablauf()
|
|
await test_wege_machbar()
|
|
await test_geist()
|
|
await test_technik_anwenden()
|
|
await test_mission_abschliessen()
|
|
await test_ui_panels()
|
|
await test_steuerung()
|
|
|
|
print("\n=========================================")
|
|
print("Fehlende Prüfungen: %d" % fehler)
|
|
print("=========================================")
|
|
get_tree().quit(1 if fehler > 0 else 0)
|
|
|
|
|
|
func ui() -> UI:
|
|
return main.ui
|
|
|
|
|
|
func welt() -> Node3D:
|
|
return main.welt
|
|
|
|
|
|
func schritte(n: int, dt: float = 0.05, zeit: float = -1.0) -> void:
|
|
for i in range(n):
|
|
if zeit >= 0.0:
|
|
Game.time_h = zeit
|
|
main._process(dt)
|
|
if welt().has_method("_process"):
|
|
welt()._process(dt)
|
|
for kind in welt().get_children():
|
|
if kind is Human:
|
|
kind._process(dt)
|
|
|
|
|
|
# --------------------------------------------------------------------
|
|
|
|
func test_start() -> void:
|
|
abschnitt("Start")
|
|
p(ui().offen, "Intro-Panel ist offen")
|
|
ui().schliessen()
|
|
p(not ui().offen, "Intro lässt sich schließen")
|
|
p(welt() is DreamWorld, "Startwelt ist die Traumwelt")
|
|
p(main.player != null and main.player.mode == Player.Mode.TRAUM, "Spieler läuft (kein Geist)")
|
|
var inter := get_tree().get_nodes_in_group("interactable")
|
|
p(inter.size() == 12 + 4 + 1 + 1,
|
|
"18 Interaktionspunkte (12 Techniken, 4 Missionen, Meditation, Tor) — gefunden: %d" % inter.size())
|
|
p(Game.einsicht == 3, "Startkapital 3 Einsicht")
|
|
p(Game.known.is_empty(), "Noch keine Technik gelernt")
|
|
|
|
|
|
func test_bibliothek() -> void:
|
|
abschnitt("Techniken lernen (Bibliothek)")
|
|
for t in TechDB.LIST:
|
|
var vollstaendig: bool = t["was"] != "" and t["wann"] != "" and t["warum"] != ""
|
|
if not vollstaendig:
|
|
p(false, "Technik %s hat alle drei Hinweise" % t["id"])
|
|
p(true, "Alle %d Techniken tragen Was/Wann/Warum" % TechDB.LIST.size())
|
|
|
|
# Über den echten Interaktionsweg: Spieler an ein Pult stellen
|
|
var ziel: Node3D = null
|
|
for n in get_tree().get_nodes_in_group("interactable"):
|
|
if n.get_meta("kind") == "technik" and n.get_meta("arg") == "pomodoro":
|
|
ziel = n
|
|
p(ziel != null, "Pult für die Pomodoro-Technik existiert")
|
|
main.player.global_position = ziel.global_position + Vector3(0, 0.5, 1.0)
|
|
main._process(0.05)
|
|
p(main._fokus == ziel, "Spieler erfasst das Pult als Fokus")
|
|
main._interagieren()
|
|
p(ui().offen and ui()._kind == "technik", "Technik-Panel öffnet sich")
|
|
ui().schliessen()
|
|
|
|
p(Game.learn("pomodoro"), "Pomodoro gelernt")
|
|
p(Game.learn("zerlegen"), "Aufgabe zerlegen gelernt")
|
|
p(Game.learn("umgebung"), "Umgebung gestalten gelernt")
|
|
p(Game.einsicht == 0, "Einsicht auf 0 verbraucht")
|
|
p(not Game.learn("reframing"), "Ohne Einsicht keine weitere Technik")
|
|
p(Game.skill_of("pomodoro") == 1, "Frisch gelernte Technik ist Stufe 1")
|
|
Game.add_xp("pomodoro", 4)
|
|
p(Game.skill_of("pomodoro") == 2, "4 EP heben auf Stufe 2")
|
|
Game.add_xp("pomodoro", 24)
|
|
p(Game.skill_of("pomodoro") == 5, "28 EP erreichen Stufe 5")
|
|
Game.known["pomodoro"] = 0
|
|
|
|
|
|
func test_gebaeude_betreten() -> void:
|
|
abschnitt("Gebäude betreten")
|
|
var vorher: Vector3 = main.player.global_position
|
|
main.player.global_position = Vector3(-24, 1.0, -4.0)
|
|
Game.teleport_lock = 0.0
|
|
for i in range(12):
|
|
await get_tree().physics_frame
|
|
main.player.global_position = Vector3(-24, 1.0, -9.0)
|
|
for i in range(20):
|
|
await get_tree().physics_frame
|
|
var drin: bool = main.player.global_position.y < -250.0
|
|
p(drin, "Tür der Bibliothek versetzt in den Innenraum (y=%.1f)" % main.player.global_position.y)
|
|
|
|
# drinnen stehen die Pulte
|
|
var innen_pulte := 0
|
|
for n in get_tree().get_nodes_in_group("interactable"):
|
|
if n.get_meta("kind") == "technik" and n.global_position.y < -250.0:
|
|
innen_pulte += 1
|
|
p(innen_pulte == 12, "12 Technik-Pulte stehen im Innenraum (%d)" % innen_pulte)
|
|
|
|
Game.teleport_lock = 0.0
|
|
main.player.global_position = Vector3(0, -299.0, 6.0)
|
|
for i in range(10):
|
|
await get_tree().physics_frame
|
|
main.player.global_position = Vector3(0, -299.0, 12.0)
|
|
for i in range(20):
|
|
await get_tree().physics_frame
|
|
p(main.player.global_position.y > -100.0,
|
|
"Ausgang bringt zurück nach draußen (y=%.1f)" % main.player.global_position.y)
|
|
main.player.global_position = vorher
|
|
|
|
|
|
func test_mission_annehmen() -> void:
|
|
abschnitt("Mission annehmen")
|
|
var ziel: Node3D = null
|
|
for n in get_tree().get_nodes_in_group("interactable"):
|
|
if n.get_meta("kind") == "mission" and n.get_meta("arg") == "lena":
|
|
ziel = n
|
|
p(ziel != null, "Stele für Lena Vogt existiert")
|
|
Game.teleport_lock = 1.0
|
|
main.player.global_position = ziel.global_position + Vector3(0, 0, 1.2)
|
|
main._process(0.05)
|
|
p(main._fokus == ziel, "Stele wird als Fokus erkannt")
|
|
main._interagieren()
|
|
p(ui().offen and ui()._kind == "mission", "Missions-Panel öffnet sich")
|
|
ui().schliessen()
|
|
p(Game.accept_mission("lena"), "Mission Lena angenommen")
|
|
p(Game.active_mission == "lena", "Mission ist aktiv")
|
|
|
|
|
|
func test_reise_zur_erde() -> void:
|
|
abschnitt("Reise zur Erde")
|
|
var tor: Node3D = null
|
|
for n in get_tree().get_nodes_in_group("interactable"):
|
|
if n.get_meta("kind") == "erde":
|
|
tor = n
|
|
p(tor != null, "Tor zur Erde existiert")
|
|
Game.teleport_lock = 1.0
|
|
main.player.global_position = tor.global_position + Vector3(0, 0, 1.5)
|
|
main._process(0.05)
|
|
p(main._fokus == tor, "Tor wird als Fokus erkannt")
|
|
main._interagieren()
|
|
await get_tree().process_frame
|
|
p(main.auf_erde and welt() is EarthWorld, "Spieler ist auf der Erde")
|
|
p(welt().humans.size() == 4, "Vier Menschen leben in der Stadt")
|
|
|
|
|
|
func test_tagesablauf() -> void:
|
|
abschnitt("Tagesablauf der Menschen")
|
|
var lena: Human = null
|
|
for m in welt().humans:
|
|
if m.data["id"] == "lena":
|
|
lena = m
|
|
p(lena != null, "Lena existiert auf der Erde")
|
|
|
|
schritte(30, 0.05, 2.0)
|
|
p(lena.activity == "schlaeft", "Um 02:00 schläft Lena (%s)" % lena.activity)
|
|
p(lena.global_position.distance_to(HumanDB.place_center("haus_lena")) < 4.0,
|
|
"Sie schläft in ihrem Atelier")
|
|
|
|
var gewandert := false
|
|
for i in range(600):
|
|
schritte(1, 0.05, 8.1)
|
|
if lena.place == "cafe" and lena._path.is_empty():
|
|
gewandert = true
|
|
break
|
|
p(gewandert, "Um 08:00 geht Lena ins Café und kommt dort an")
|
|
p(lena.activity == "essen", "Im Café isst sie (%s)" % lena.activity)
|
|
p(lena.global_position.distance_to(HumanDB.place_center("cafe")) < 4.0,
|
|
"Sie steht im Café (Abstand %.1f)" %
|
|
lena.global_position.distance_to(HumanDB.place_center("cafe")))
|
|
|
|
var daheim := false
|
|
for i in range(600):
|
|
schritte(1, 0.05, 10.2)
|
|
if lena.place == "haus_lena" and lena._path.is_empty():
|
|
daheim = true
|
|
break
|
|
p(daheim, "Um 10:00 ist sie zurück im Atelier")
|
|
p(lena.activity == "zoegert", "Dort zögert sie vor der Arbeit (%s)" % lena.activity)
|
|
|
|
# alle vier haben zu jeder Stunde einen definierten Ort
|
|
var luecken := 0
|
|
for m in welt().humans:
|
|
for h in range(24):
|
|
var r: Array = m._routine_at(float(h) + 0.5)
|
|
if not HumanDB.PLACES.has(r[0]) or not HumanDB.ACTIVITY_TEXT.has(r[1]):
|
|
luecken += 1
|
|
p(luecken == 0, "Alle 4 Menschen haben rund um die Uhr Ort und Tätigkeit")
|
|
|
|
# ein voller Tag am Stück, mit echter Uhr: niemand bleibt hängen
|
|
Game.time_h = 0.0
|
|
var besuchte: Dictionary = {}
|
|
for m in welt().humans:
|
|
besuchte[m.data["id"]] = {}
|
|
for i in range(7200):
|
|
schritte(1)
|
|
for m in welt().humans:
|
|
if m._path.is_empty():
|
|
besuchte[m.data["id"]][m.place] = true
|
|
if Game.time_h >= 23.5 and Game.day >= 1:
|
|
break
|
|
var alle_ok := true
|
|
for m in welt().humans:
|
|
var orte: int = besuchte[m.data["id"]].size()
|
|
var soll: Dictionary = {}
|
|
for e in m.data["routine"]:
|
|
soll[e[1]] = true
|
|
if orte < soll.size():
|
|
alle_ok = false
|
|
p(false, "%s erreichte nur %d von %d Orten" % [m.data["name"], orte, soll.size()])
|
|
p(alle_ok, "Über einen ganzen Tag erreicht jeder Mensch jede Station seiner Routine")
|
|
|
|
|
|
func test_wege_machbar() -> void:
|
|
abschnitt("Wege passen in die Zeitfenster")
|
|
var sek_pro_stunde := Game.DAY_SECONDS / 24.0
|
|
var alles_ok := true
|
|
var engste := 999.0
|
|
var engste_text := ""
|
|
for m in HumanDB.LIST:
|
|
var r: Array = m["routine"]
|
|
for i in range(r.size()):
|
|
var von: String = String(r[i - 1][1]) if i > 0 else String(r[r.size() - 1][1])
|
|
var nach: String = String(r[i][1])
|
|
if von == nach:
|
|
continue
|
|
var dauer := _weg_laenge(von, nach) / Human.GEHTEMPO
|
|
var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() \
|
|
else (24.0 - float(r[i][0]))
|
|
var fenster := slot_h * sek_pro_stunde
|
|
if dauer >= fenster:
|
|
alles_ok = false
|
|
p(false, "%s: %s -> %s braucht %.0f s, Fenster nur %.0f s" %
|
|
[m["name"], von, nach, dauer, fenster])
|
|
elif fenster - dauer < engste:
|
|
engste = fenster - dauer
|
|
engste_text = ("%s: %s -> %s (%.0f s Weg, %.0f s Fenster)"
|
|
% [m["name"], von, nach, dauer, fenster])
|
|
p(alles_ok, "Jeder Weg passt in sein Zeitfenster — engster Fall: " + engste_text)
|
|
|
|
|
|
func _weg_laenge(von: String, nach: String) -> float:
|
|
var pts: Array = HumanDB.route(von, nach, Vector3.ZERO)
|
|
var l := 0.0
|
|
var pos := HumanDB.place_center(von)
|
|
for pt in pts:
|
|
l += pos.distance_to(pt)
|
|
pos = pt
|
|
return l
|
|
|
|
|
|
func test_geist() -> void:
|
|
abschnitt("Geist-Bewegung")
|
|
p(main.player.mode == Player.Mode.GEIST, "Spieler ist auf der Erde ein Geist")
|
|
p(main.player.collision_layer == 0 and main.player.collision_mask == 0,
|
|
"Der Geist kollidiert mit nichts — er schwebt durch Wände")
|
|
var vor: Vector3 = main.player.global_position
|
|
main.player.global_position = Vector3(0, 8.0, 0)
|
|
main.player.velocity = Vector3(6, 0, 0)
|
|
for i in range(30):
|
|
await get_tree().physics_frame
|
|
p(main.player.global_position.x > vor.x, "Der Geist bewegt sich frei durch den Raum")
|
|
p(absf(main.player.global_position.y - 8.0) < 0.5, "Ohne Schwerkraft fällt er nicht")
|
|
|
|
# keiner der Menschen reagiert auf ihn
|
|
var lena: Human = welt().humans[0]
|
|
var davor: Vector3 = lena.global_position
|
|
main.player.global_position = lena.global_position + Vector3(0, 1, 0.5)
|
|
schritte(10)
|
|
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
|
|
|
|
|
|
func test_technik_anwenden() -> void:
|
|
abschnitt("Techniken anwenden — Erfolgschance")
|
|
var lena: Human = null
|
|
for m in welt().humans:
|
|
if m.data["id"] == "lena":
|
|
lena = m
|
|
|
|
Game.time_h = 9.5
|
|
for i in range(900):
|
|
schritte(1)
|
|
if lena.place == "haus_lena" and lena._path.is_empty():
|
|
break
|
|
p(lena.activity == "zoegert", "Lena zögert gerade")
|
|
|
|
main.player.global_position = lena.global_position + Vector3(0, 1.0, 1.5)
|
|
main._process(0.05)
|
|
p(main._fokus_mensch == lena, "Der Geist erfasst Lena in Reichweite")
|
|
main._interagieren()
|
|
p(ui().offen and ui()._kind == "anwenden", "Anwenden-Panel öffnet sich")
|
|
ui().schliessen()
|
|
|
|
Game.known["pomodoro"] = 0
|
|
var s1: float = Game.evaluate("pomodoro", "lena", "zoegert", false)["chance"]
|
|
Game.known["pomodoro"] = 28
|
|
var s5: float = Game.evaluate("pomodoro", "lena", "zoegert", false)["chance"]
|
|
p(Game.skill_of("pomodoro") == 5, "Pomodoro auf Stufe 5 gebracht")
|
|
p(s5 > s1, "Höherer Skill = höhere Erfolgschance (%.0f%% -> %.0f%%)" % [s1 * 100, s5 * 100])
|
|
Game.known["pomodoro"] = 0
|
|
|
|
var passend: Dictionary = Game.evaluate("pomodoro", "lena", "zoegert", false)
|
|
var unpassend: Dictionary = Game.evaluate("pomodoro", "lena", "essen", false)
|
|
p(passend["moment"] and not unpassend["moment"], "Moment-Erkennung: zögert ja, isst nein")
|
|
p(passend["chance"] > unpassend["chance"],
|
|
"Passender Moment erhöht die Chance (%.0f%% > %.0f%%)" %
|
|
[passend["chance"] * 100, unpassend["chance"] * 100])
|
|
p(passend["tag"], "Pomodoro trifft Lenas Thema Prokrastination")
|
|
var falsches_thema: Dictionary = Game.evaluate("pomodoro", "mira", "zoegert", false)
|
|
p(not falsches_thema["tag"], "Bei Mira trifft Pomodoro das Thema nicht")
|
|
p(passend["chance"] > falsches_thema["chance"], "Passendes Thema erhöht die Chance")
|
|
|
|
var blockiert: Dictionary = Game.evaluate("pomodoro", "lena", "schlaeft", false)
|
|
p(blockiert["blocked"], "Schlafende Menschen sind nicht erreichbar")
|
|
var wehrt: Dictionary = Game.evaluate("pomodoro", "lena", "zoegert", true)
|
|
p(wehrt["chance"] < passend["chance"], "Wer abblockt, ist schwerer zu erreichen")
|
|
var ungelernt: Dictionary = Game.evaluate("nein_sagen", "lena", "zoegert", false)
|
|
p(ungelernt["blocked"], "Ungelernte Techniken lassen sich nicht anwenden")
|
|
|
|
var vorher_p: float = Game.mission_progress("lena")
|
|
var res := Game.apply_technique("pomodoro", "lena", "zoegert", false)
|
|
p(res["ok"], "Anwendung wird ausgeführt")
|
|
if res["success"]:
|
|
p(Game.mission_progress("lena") > vorher_p, "Erfolg bringt Missionsfortschritt")
|
|
else:
|
|
p(Game.mission_progress("lena") == vorher_p, "Misserfolg bringt keinen Fortschritt")
|
|
p(Game.known["pomodoro"] > 0, "Jede Anwendung bringt Erfahrung")
|
|
|
|
# Wiederholung derselben Technik wirkt schwächer
|
|
Game.progress["lena"] = 0.0
|
|
Game.used.clear()
|
|
Game.known["pomodoro"] = 28
|
|
var ertraege: Array = []
|
|
for i in range(6):
|
|
var r := Game.apply_technique("pomodoro", "lena", "zoegert", false)
|
|
if r["success"]:
|
|
ertraege.append(r["gain"])
|
|
p(ertraege.size() >= 2 and ertraege[ertraege.size() - 1] < ertraege[0],
|
|
"Dieselbe Technik immer wieder wirkt schwächer")
|
|
|
|
|
|
func test_mission_abschliessen() -> void:
|
|
abschnitt("Mission abschließen")
|
|
Game.progress["lena"] = 0.0
|
|
Game.used.clear()
|
|
var einsicht_vorher := Game.einsicht
|
|
var runden := 0
|
|
while Game.mission_progress("lena") < Game.MISSION_ZIEL and runden < 400:
|
|
Game.apply_technique("pomodoro", "lena", "zoegert", false)
|
|
Game.apply_technique("zerlegen", "lena", "gruebelt", false)
|
|
Game.apply_technique("umgebung", "lena", "aufraeumen", false)
|
|
runden += 1
|
|
p(Game.mission_progress("lena") >= Game.MISSION_ZIEL,
|
|
"Lenas Traum erreicht 100 %% (nach %d Runden)" % runden)
|
|
p(Game.completed.has("lena"), "Mission gilt als abgeschlossen")
|
|
p(Game.active_mission == "", "Aktive Mission wurde freigegeben")
|
|
p(Game.einsicht == einsicht_vorher + 3, "Abschluss bringt 3 Einsicht")
|
|
schritte(3)
|
|
var lena: Human = welt().humans[0]
|
|
p(lena.beschriftung().contains("erfüllt"), "Lena zeigt sichtbar den erfüllten Traum")
|
|
|
|
|
|
func test_ui_panels() -> void:
|
|
abschnitt("Oberfläche")
|
|
ui().kodex_panel()
|
|
p(ui().offen and ui()._content.get_child_count() > 0, "Kodex zeigt gelernte Techniken")
|
|
ui().schliessen()
|
|
ui().vertiefen_panel()
|
|
p(ui()._content.get_child_count() > 0, "Meditationskammer listet Techniken")
|
|
ui().schliessen()
|
|
var e_vor := Game.einsicht
|
|
p(Game.deepen("pomodoro"), "Vertiefen funktioniert")
|
|
p(Game.einsicht == e_vor - 1, "Vertiefen kostet 1 Einsicht")
|
|
ui().mission_panel("bruno")
|
|
p(ui()._content.get_child_count() > 0, "Missions-Panel für Bruno baut auf")
|
|
ui().schliessen()
|
|
ui().anwenden_panel(welt().humans[1])
|
|
p(ui()._content.get_child_count() > 0, "Anwenden-Panel für Bruno baut auf")
|
|
ui().schliessen()
|
|
p(main._fokus == null or true, "HUD bleibt fehlerfrei")
|
|
|
|
|
|
func test_steuerung() -> void:
|
|
abschnitt("Steuerung")
|
|
var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "sinken",
|
|
"interagieren", "kodex"]
|
|
var alle := true
|
|
for a in aktionen:
|
|
if not InputMap.has_action(a) or InputMap.action_get_events(a).is_empty():
|
|
alle = false
|
|
p(false, "Aktion '%s' ist belegt" % a)
|
|
p(alle, "Alle acht Steuerungsaktionen sind mit Tasten belegt")
|
|
|
|
main.ui.schliessen()
|
|
main.player.look_enabled = true
|
|
main.player.set_mode(Player.Mode.GEIST)
|
|
main.player.global_position = Vector3(0, 8.0, 0)
|
|
main.player.velocity = Vector3.ZERO
|
|
main.player.yaw = 0.0
|
|
var start: Vector3 = main.player.global_position
|
|
|
|
Input.action_press("vor")
|
|
for i in range(40):
|
|
await get_tree().physics_frame
|
|
Input.action_release("vor")
|
|
p(main.player.global_position.z < start.z - 1.0,
|
|
"W schiebt den Spieler nach vorn (dz = %.1f)" % (main.player.global_position.z - start.z))
|
|
|
|
main.player.global_position = Vector3(0, 8.0, 0)
|
|
main.player.velocity = Vector3.ZERO
|
|
Input.action_press("rechts")
|
|
for i in range(40):
|
|
await get_tree().physics_frame
|
|
Input.action_release("rechts")
|
|
p(main.player.global_position.x > 1.0,
|
|
"D schiebt den Spieler nach rechts (x = %.1f)" % main.player.global_position.x)
|
|
|
|
main.player.global_position = Vector3(0, 8.0, 0)
|
|
main.player.velocity = Vector3.ZERO
|
|
Input.action_press("steigen")
|
|
for i in range(40):
|
|
await get_tree().physics_frame
|
|
Input.action_release("steigen")
|
|
p(main.player.global_position.y > 9.0,
|
|
"Leertaste lässt den Geist steigen (y = %.1f)" % main.player.global_position.y)
|
|
|
|
main.player.velocity = Vector3.ZERO
|
|
var hoch: float = main.player.global_position.y
|
|
Input.action_press("sinken")
|
|
for i in range(40):
|
|
await get_tree().physics_frame
|
|
Input.action_release("sinken")
|
|
p(main.player.global_position.y < hoch - 1.0,
|
|
"Umschalt lässt den Geist sinken (y = %.1f)" % main.player.global_position.y)
|