3354 lines
128 KiB
GDScript
3354 lines
128 KiB
GDScript
extends Node
|
||
## Fährt das echte Spiel headless und prüft die Implementierungen.
|
||
|
||
var fehler := 0
|
||
var main: Node3D
|
||
|
||
|
||
const ZEITRAFFER := 8.0 # die Engine läuft im Test achtmal schneller
|
||
## Abschnitte, die ganze Tage oder viele Läufe simulieren -- im Schnelllauf übersprungen.
|
||
const LANGSAM := ["test_tagesablauf", "test_verfolgung", "test_erster_traum_schaffbar",
|
||
"test_huegel", "test_dorf_organisch", "test_welt_waechst", "test_zufallslauf"]
|
||
var schnell := false
|
||
var _uhr_abschnitt := 0
|
||
var _dauern: Array = []
|
||
var _uebersprungen := 0
|
||
|
||
|
||
func p(ok: bool, text: String) -> void:
|
||
print((" OK " if ok else " FEHLT ") + text)
|
||
if not ok:
|
||
fehler += 1
|
||
|
||
|
||
func abschnitt(t: String) -> void:
|
||
_abschnitt_ende()
|
||
print("\n== " + t)
|
||
_dauern.append([t, 0.0])
|
||
_uhr_abschnitt = Time.get_ticks_msec()
|
||
|
||
|
||
func _abschnitt_ende() -> void:
|
||
if _dauern.is_empty():
|
||
return
|
||
_dauern[_dauern.size() - 1][1] = float(Time.get_ticks_msec() - _uhr_abschnitt) / 1000.0
|
||
|
||
|
||
## Wer den Traum mit reinen Game-Aufrufen vorantreibt, tickt keine Momente:
|
||
## ein laufender Moment hielte den Fortschritt sonst für immer an.
|
||
func _ohne_momente() -> void:
|
||
Game.situation = {}
|
||
Game.situation_pause = 1.0e6
|
||
|
||
|
||
## Ruft einen Testabschnitt auf -- oder überspringt ihn im Schnelllauf.
|
||
func lauf(name: String) -> void:
|
||
if schnell and LANGSAM.has(name):
|
||
_uebersprungen += 1
|
||
return
|
||
# Ausgangslage für jeden Abschnitt: Oberfläche zu, niemand verfolgt, kein
|
||
# Moment, keine Taste gedrückt -- sonst erbt ein Abschnitt die Fehler des vorigen
|
||
ui().schliessen()
|
||
main.player.folgt = null
|
||
main.player.look_enabled = true
|
||
main.player.velocity = Vector3.ZERO
|
||
Game.situation = {}
|
||
Game.notiz_sperre = 0.0
|
||
for t in ["vor", "zurueck", "links", "rechts", "steigen", "drehen"]:
|
||
Input.action_release(t)
|
||
await Callable(self, name).call()
|
||
|
||
|
||
func _ready() -> void:
|
||
schnell = OS.get_cmdline_user_args().has("schnell")
|
||
# Zeitraffer: die Uhr läuft achtmal schneller, der Physikschritt bleibt 1/60 s --
|
||
# sonst fiele der Spieler bei großen Schritten durch den Boden.
|
||
Engine.time_scale = ZEITRAFFER
|
||
Engine.physics_ticks_per_second = int(60.0 * ZEITRAFFER)
|
||
Engine.max_physics_steps_per_frame = 64
|
||
seed(20240)
|
||
main = load("res://scenes/main.tscn").instantiate()
|
||
add_child(main)
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
|
||
await lauf("test_start")
|
||
await lauf("test_journal")
|
||
await lauf("test_traumdorf")
|
||
await lauf("test_dorf_organisch")
|
||
await lauf("test_huegel")
|
||
await lauf("test_haus_betreten")
|
||
await lauf("test_hoersaal")
|
||
await lauf("test_mission_annehmen")
|
||
await lauf("test_reise_zur_erde")
|
||
await lauf("test_welten_bleiben")
|
||
await lauf("test_spuren")
|
||
await lauf("test_ankunft")
|
||
await lauf("test_akte")
|
||
await lauf("test_passanten")
|
||
await lauf("test_wesen_helfen")
|
||
await lauf("test_bezirk")
|
||
await lauf("test_tagesablauf")
|
||
await lauf("test_wege_machbar")
|
||
await lauf("test_geist")
|
||
await lauf("test_verfolgung")
|
||
await lauf("test_charakter")
|
||
await lauf("test_kamera_und_etagen")
|
||
await lauf("test_zufallslauf")
|
||
await lauf("test_gehen_bei_tempo")
|
||
await lauf("test_diagnose")
|
||
await lauf("test_notieren_im_spiel")
|
||
await lauf("test_tagesablauf_anzeige")
|
||
await lauf("test_ablauf_im_hud")
|
||
await lauf("test_probleme_und_momente")
|
||
await lauf("test_eigene_menschen")
|
||
await lauf("test_systeme")
|
||
await lauf("test_tempo_des_traums")
|
||
await lauf("test_cooldown")
|
||
await lauf("test_chance_verdeckt")
|
||
await lauf("test_mission_abschliessen")
|
||
await lauf("test_welt_waechst")
|
||
await lauf("test_techniken_verbessern_sich")
|
||
await lauf("test_journal_ueberall")
|
||
await lauf("test_steuerung")
|
||
await lauf("test_erster_traum_schaffbar")
|
||
await lauf("test_brett_nie_leer")
|
||
await lauf("test_lesen_lohnt_sich")
|
||
await lauf("test_tempo")
|
||
|
||
_abschnitt_ende()
|
||
_dauern.sort_custom(func(x, y): return x[1] > y[1])
|
||
var langsam: Array = []
|
||
for i in range(mini(5, _dauern.size())):
|
||
langsam.append("%s %.0f s" % [_dauern[i][0], _dauern[i][1]])
|
||
print("\nLangsamste Abschnitte: " + " · ".join(langsam))
|
||
if schnell:
|
||
print("Schnelllauf: %d Abschnitte übersprungen" % _uebersprungen)
|
||
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 Bewohner:
|
||
kind._process(dt)
|
||
|
||
|
||
func _panel_text() -> String:
|
||
var out: Array = []
|
||
for c in _alle_kinder(ui()._content):
|
||
if c is RichTextLabel:
|
||
out.append((c as RichTextLabel).get_parsed_text())
|
||
return "\n".join(out)
|
||
|
||
|
||
func _journal_text() -> String:
|
||
return _panel_text()
|
||
|
||
|
||
func _lehre_text() -> String:
|
||
return _panel_text()
|
||
|
||
|
||
## Hebt alle Technik-Sperren auf. Nur für Messschleifen, die viele Anwendungen
|
||
## in Folge brauchen — der Cooldown selbst wird eigens geprüft.
|
||
func _frei() -> void:
|
||
Game.cooldown.clear()
|
||
|
||
|
||
## Welche Gebäude stehen wirklich in der Traumwelt? Erkennbar an ihren Schildern.
|
||
## Was der Hörsaal von Anfang an lehrt.
|
||
func _basiswege() -> Array:
|
||
return DreamDB.techniken_von("hoersaal")
|
||
|
||
|
||
func _gebaute_orte() -> Array:
|
||
var namen: Array = []
|
||
for key in DreamDB.PLACES:
|
||
if float(DreamDB.PLACES[key]["size"][0]) <= 0.0:
|
||
continue
|
||
namen.append(str(DreamDB.PLACES[key]["name"]))
|
||
var da: Array = []
|
||
for n in welt().get_children():
|
||
if n is Label3D and namen.has((n as Label3D).text):
|
||
da.append((n as Label3D).text)
|
||
return da
|
||
|
||
|
||
func _marke(kind: String) -> Node3D:
|
||
for n in get_tree().get_nodes_in_group("interactable"):
|
||
if n.get_meta("kind") == kind:
|
||
return n
|
||
return null
|
||
|
||
|
||
## Stellt den Spieler vor eine Marke und lässt den Fokus neu suchen.
|
||
func _hingehen(ziel: Node3D) -> void:
|
||
main.player.set_mode(Player.Mode.TRAUM)
|
||
main.player.global_position = ziel.global_position + Vector3(0, 0, 1.2)
|
||
main.player.velocity = Vector3.ZERO
|
||
ui().schliessen()
|
||
main._process(0.05)
|
||
|
||
|
||
## Drückt den ersten aktiven Knopf, dessen Beschriftung so beginnt.
|
||
func _knopf_texte() -> String:
|
||
var out: Array = []
|
||
for c in _alle_kinder(ui()._content):
|
||
if c is Button:
|
||
out.append((c as Button).text)
|
||
return "\n".join(out)
|
||
|
||
|
||
func _knopf_da(anfang: String) -> bool:
|
||
for c in _alle_kinder(ui()._content):
|
||
if c is Button and (c as Button).text.begins_with(anfang):
|
||
return true
|
||
return false
|
||
|
||
|
||
func _druecke(anfang: String) -> bool:
|
||
for c in _alle_kinder(ui()._content):
|
||
if c is Button and not (c as Button).disabled and (c as Button).text.begins_with(anfang):
|
||
(c as Button).pressed.emit()
|
||
return true
|
||
return false
|
||
|
||
|
||
func laufe(von: Vector3, yaw: float, frames: int) -> Vector3:
|
||
main.player.set_mode(Player.Mode.TRAUM)
|
||
main.player.look_enabled = true
|
||
if welt() is DreamWorld:
|
||
von.y = maxf(von.y, DreamDB.hoehe_bei(von.x, von.z) + 1.0) # nie im Hügel starten
|
||
main.player.global_position = von
|
||
main.player.velocity = Vector3.ZERO
|
||
main.player.yaw = yaw
|
||
Input.action_press("vor")
|
||
for i in range(frames):
|
||
await get_tree().physics_frame
|
||
Input.action_release("vor")
|
||
return main.player.global_position
|
||
|
||
|
||
# --------------------------------------------------------------------
|
||
|
||
|
||
func test_start() -> void:
|
||
abschnitt("Start")
|
||
p(not ui().offen, "Kein Startbildschirm — das Spiel läuft sofort")
|
||
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 arten: Dictionary = {}
|
||
for n in get_tree().get_nodes_in_group("interactable"):
|
||
arten[n.get_meta("kind")] = true
|
||
p(arten.has("erde") and arten.has("lehre") and arten.has("brett"),
|
||
"Traumwelt bietet Tor, Hörsaal und Schwarzes Brett (%s)" % ", ".join(arten.keys()))
|
||
p(Game.known.size() == 1, "Ein Weg ist von Anfang an gelernt (%d)" % Game.known.size())
|
||
p(Game.knows(str(_basiswege()[0])),
|
||
"Und zwar der des Hörsaals: %s" % TechDB.by_id(str(_basiswege()[0]))["name"])
|
||
p(Game.skill_of(str(_basiswege()[0])) == 1, "Auf Stufe 1, wie frisch gelernt")
|
||
p(not Game.offene_missionen().is_empty(),
|
||
"Damit hängt vom ersten Moment an eine Mission aus (%d)" %
|
||
Game.offene_missionen().size())
|
||
p((welt() as DreamWorld).bewohner.is_empty(),
|
||
"Das Traumdorf steht am Anfang leer (%d Wesen)" %
|
||
(welt() as DreamWorld).bewohner.size())
|
||
var lebt := false
|
||
for n in welt().get_children():
|
||
if n is Bewohner:
|
||
lebt = true
|
||
p(not lebt, "Auch kein Wesen im Szenenbaum")
|
||
|
||
var gebaut := _gebaute_orte()
|
||
p(gebaut.size() == 1 and gebaut.has("Hörsaal der Wege"),
|
||
"Nur der Hörsaal steht als Gebäude (%s)" % ", ".join(gebaut))
|
||
|
||
|
||
func test_journal() -> void:
|
||
abschnitt("Journal")
|
||
for t in TechDB.LIST:
|
||
var vollstaendig: bool = t["was"] != "" and t["wann"] != "" and t["warum"] != "" \
|
||
and t["woran"] != ""
|
||
if not vollstaendig:
|
||
p(false, "Technik %s hat alle vier Hinweise" % t["id"])
|
||
p(true, "Alle %d Techniken tragen Was/Wann/Warum/Woran" % TechDB.LIST.size())
|
||
|
||
# Der Woran-Hinweis muss die Tätigkeiten benennen, gegen die die Technik
|
||
# hilft — sonst ist im Spiel nicht herauszulesen, wohin sie gehört.
|
||
var stamm := {
|
||
"putzflucht": "aufräumt", "recherche": "recherchiert", "liste": "später-liste",
|
||
"jasagen": "ja sagt", "scrollen": "scrollt", "serie": "serie",
|
||
"wach": "wach", "kuehlschrank": "kühlschrank", "spielen": "spielt",
|
||
"umschalten": "umschalt", "klagen": "klagt", "vergleichen": "vergleicht",
|
||
"mails": "postfach", "kaffee": "kaffee", "perfekt": "detail", "plan_neu": "plan",
|
||
"snooze": "schlummer", "doesen": "döst", "bestellen": "bestellt",
|
||
"nachrichten": "nachrichten", "ueberstunden": "länger", "absage": "absag",
|
||
"waelzen": "wälzt",
|
||
}
|
||
var luecken: Array = []
|
||
var zuviel: Array = []
|
||
for t in TechDB.LIST:
|
||
var text: String = str(t["woran"]).to_lower()
|
||
var eigene := Ursachen.alle_blocker(str(t["ursache"]))
|
||
var trifft := 0
|
||
for b in eigene:
|
||
if text.contains(str(stamm[b])):
|
||
trifft += 1
|
||
if trifft == 0:
|
||
luecken.append("%s nennt keinen eigenen Blocker" % t["id"])
|
||
for b in stamm:
|
||
if eigene.has(b):
|
||
continue
|
||
if text.contains(str(stamm[b])):
|
||
zuviel.append("%s nennt fremdes »%s«" % [t["id"], b])
|
||
p(luecken.is_empty(), "Jeder Woran-Hinweis nennt eine Tätigkeit seiner Ursache%s" %
|
||
("" if luecken.is_empty() else " — " + ", ".join(luecken)))
|
||
p(zuviel.is_empty(), "Und keine, gegen die die Technik gar nicht hilft%s" %
|
||
("" if zuviel.is_empty() else " — " + ", ".join(zuviel)))
|
||
|
||
ui().journal_panel("techniken")
|
||
p(ui().offen and ui()._kind == "journal", "Journal öffnet sich")
|
||
p(_knopf_texte().contains(str(TechDB.by_id(str(_basiswege()[0]))["name"])),
|
||
"Das Journal führt den mitgebrachten Weg")
|
||
p(not _knopf_da("Diesen Weg"), "Das Journal lehrt nicht selbst")
|
||
ui().journal_panel("missionen")
|
||
p(_journal_text().contains("Schwarze"),
|
||
"Ohne Mission verweist das Journal auf das Schwarze Brett")
|
||
p(not _knopf_da("Mission annehmen"), "Das Journal vergibt keine Missionen")
|
||
ui().schliessen()
|
||
|
||
var erster := str(_basiswege()[0])
|
||
p(Game.offene_techniken() == 1,
|
||
"Am Anfang ist genau ein Weg entdeckt (%d)" % Game.offene_techniken())
|
||
p(Game.knows(erster), "Er ist schon gelernt: %s" % TechDB.by_id(erster)["name"])
|
||
p(not Game.learn(erster), "Zweimal lernen geht nicht")
|
||
p(not Game.learn(str(TechDB.LIST[1]["id"])),
|
||
"Der zweite Weg ist nirgends zu lernen — er ist noch nicht entdeckt")
|
||
p(Game.skill_of(erster) == 1, "Frisch gelernte Technik ist Stufe 1")
|
||
Game.add_xp(erster, 4)
|
||
p(Game.skill_of(erster) == 2, "4 EP heben auf Stufe 2")
|
||
Game.add_xp(erster, 24)
|
||
p(Game.skill_of(erster) == 5, "28 EP erreichen Stufe 5")
|
||
Game.known[erster] = 0
|
||
|
||
ui().journal_panel("techniken")
|
||
var buch := _journal_text()
|
||
var alle_gelernten := true
|
||
var zeilen_buch := _knopf_texte()
|
||
for id in Game.known:
|
||
if not zeilen_buch.contains(str(TechDB.by_id(id)["name"])):
|
||
alle_gelernten = false
|
||
p(alle_gelernten, "Das Journal führt alle %d gelernten Techniken" % Game.known.size())
|
||
var fremde := 0
|
||
for t in TechDB.LIST:
|
||
if not Game.knows(t["id"]) and buch.contains(str(t["name"])):
|
||
fremde += 1
|
||
p(fremde == 0, "Ungelernte Techniken stehen nicht im Journal (%d)" % fremde)
|
||
ui().schliessen()
|
||
|
||
|
||
func test_traumdorf() -> void:
|
||
abschnitt("Traumwesen ziehen ein und leben ihren Tag")
|
||
p(welt().bewohner.is_empty(), "Vorher wohnt niemand hier")
|
||
for i in range(4):
|
||
Game.completed.append("probe_%d" % i)
|
||
Game.welt_erzeugen()
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
if welt().bewohner.size() != i + 1:
|
||
p(false, "Nach %d Träumen wohnen %d Wesen hier" %
|
||
[i + 1, welt().bewohner.size()])
|
||
p(welt().bewohner.size() == 4,
|
||
"Je erfülltem Traum zieht eines ein, hier sind es vier (%d)" %
|
||
welt().bewohner.size())
|
||
var wesen: Array = welt().bewohner
|
||
|
||
var haeuser := 0
|
||
for key in DreamDB.PLACES:
|
||
if key.begins_with("haus_"):
|
||
haeuser += 1
|
||
p(haeuser == 4, "Vier Häuser sind erzeugt (%d)" % haeuser)
|
||
p(float(DreamDB.PLACES["hoersaal"]["size"][0]) > 0.0, "Dazu steht der Hörsaal")
|
||
|
||
var eigene: Dictionary = {}
|
||
for w in wesen:
|
||
var haus: String = str(w.data["haus"])
|
||
p(DreamDB.PLACES.has(haus), "%s hat ein eigenes Haus (%s)" % [w.data["name"], haus])
|
||
eigene[haus] = true
|
||
var kommt_vor := false
|
||
for e in w.data["routine"]:
|
||
if e[1] == haus:
|
||
kommt_vor = true
|
||
p(kommt_vor, "Die Routine von %s führt ins eigene Haus" % w.data["name"])
|
||
p(eigene.size() == 4, "Jedes Wesen hat ein anderes Haus")
|
||
|
||
var luecken := 0
|
||
for w in wesen:
|
||
for h in range(24):
|
||
var r: Array = w._routine_at(float(h) + 0.5)
|
||
if not DreamDB.PLACES.has(r[0]) or not Orte.kennt(str(r[1])):
|
||
luecken += 1
|
||
p(luecken == 0, "Alle vier haben rund um die Uhr Ort und Tätigkeit")
|
||
|
||
# Ein voller Tag am Stück: niemand bleibt hängen
|
||
Game.time_h = 0.0
|
||
Game.day = 1
|
||
var besucht: Dictionary = {}
|
||
for w in wesen:
|
||
besucht[w.data["id"]] = {}
|
||
for i in range(9000):
|
||
schritte(1)
|
||
for w in wesen:
|
||
if w._path.is_empty():
|
||
besucht[w.data["id"]][w.place] = true
|
||
if Game.time_h >= 23.5:
|
||
break
|
||
var alle_ok := true
|
||
for w in wesen:
|
||
var soll: Dictionary = {}
|
||
for e in w.data["routine"]:
|
||
soll[e[1]] = true
|
||
var ist: int = besucht[w.data["id"]].size()
|
||
if ist < soll.size():
|
||
alle_ok = false
|
||
p(false, "%s erreichte nur %d von %d Stationen" % [w.data["name"], ist, soll.size()])
|
||
p(alle_ok, "Über einen ganzen Tag erreicht jedes Wesen jede Station seiner Routine")
|
||
|
||
var zuhause := 0
|
||
schritte(20, 0.05, 1.0)
|
||
for w in wesen:
|
||
var r: Array = w._routine_at(1.0)
|
||
if w.place == r[0]:
|
||
zuhause += 1
|
||
p(zuhause == 4, "Um 01:00 sind alle vier an ihrem Nachtplatz (%d)" % zuhause)
|
||
var ohne_netz := 0
|
||
for w in welt().bewohner:
|
||
ohne_netz += int(w.nav_fehl)
|
||
p(ohne_netz == 0, "Auch sie laufen auf dem Navigationsnetz der Insel (%d Rückfälle)" % ohne_netz)
|
||
|
||
# Ausgangslage wiederherstellen: das Dorf ist wieder leer
|
||
Game.completed.clear()
|
||
Game.welt_erzeugen()
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
|
||
|
||
## Die Insel ist ein Hügel: innen eben, außen ansteigend, jedes Gebäude auf
|
||
## einer Terrasse. Spieler und Bewohner laufen darauf, statt hindurchzufallen.
|
||
func test_huegel() -> void:
|
||
abschnitt("Die Insel ist ein Hügel")
|
||
var uhr_vorher := Game.time_h
|
||
var tag_vorher := Game.day
|
||
p(is_equal_approx(DreamDB.hang(0.0), 0.0) and is_equal_approx(DreamDB.hang(DreamDB.FLACH), 0.0),
|
||
"Bis %.0f Einheiten ist es eben" % DreamDB.FLACH)
|
||
p(DreamDB.hang(DreamDB.FLACH + 20.0) > 3.0 and DreamDB.hang(200.0) <= DreamDB.GIPFEL,
|
||
"Dahinter steigt es an, bis zum Gipfel bei %.0f (bei +20: %.1f)" %
|
||
[DreamDB.GIPFEL, DreamDB.hang(DreamDB.FLACH + 20.0)])
|
||
p(DreamDB.hoehe_bei(0.0, 2.0) == 0.0, "Der Dorfplatz liegt eben")
|
||
var tor_terr := DreamDB.terrasse("tor")
|
||
p(is_equal_approx(DreamDB.hoehe_bei(0.0, -34.0), tor_terr)
|
||
and is_equal_approx(DreamDB.hoehe_bei(0.0, -34.0 + DreamDB.TOR_FLAECHE * 0.5 + 2.0), tor_terr),
|
||
"Das Tor steht auf einer Terrasse, eben mit seiner Dorfseite (%.1f)" % tor_terr)
|
||
var stufe := absf(DreamDB.hoehe_bei(0.0, 25.4) - DreamDB.terrasse("hoersaal"))
|
||
p(stufe < 0.1, "Vor der Hörsaaltür ist keine Stufe (%.2f)" % stufe)
|
||
|
||
# Ein Dorf mit vielen Häusern: manche stehen am Hang, jedes auf einer Terrasse
|
||
var merker: Array = Game.completed.duplicate()
|
||
Game.completed.clear()
|
||
for i in range(14):
|
||
Game.completed.append("huegel_%d" % i)
|
||
Game.welt_erzeugen()
|
||
var am_hang := 0
|
||
var schief: Array = []
|
||
for key in DreamDB.PLACES:
|
||
var s: Array = DreamDB.PLACES[key]["size"]
|
||
if float(s[0]) <= 0.0:
|
||
continue
|
||
var c := Orte.center(DreamDB.PLACES, key)
|
||
var terr := DreamDB.terrasse(key)
|
||
if terr > 0.5:
|
||
am_hang += 1
|
||
for dx in [-1.0, 1.0]:
|
||
for dz in [-1.0, 1.0]:
|
||
var h := DreamDB.hoehe_bei(c.x + dx * float(s[0]) * 0.45, c.z + dz * float(s[1]) * 0.45)
|
||
if absf(h - terr) > 0.01:
|
||
schief.append(key)
|
||
p(am_hang >= 3, "Mit 14 Häusern stehen welche am Hang (%d)" % am_hang)
|
||
p(schief.is_empty(), "Jedes Gebäude steht innen eben auf seiner Terrasse%s" %
|
||
("" if schief.is_empty() else " — nicht: " + ", ".join(schief)))
|
||
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
# Bewohner laufen einen Tag lang auf dem Boden, nicht darunter oder darüber
|
||
var abweichung := 0.0
|
||
Game.time_h = 0.0
|
||
for i in range(3000):
|
||
schritte(1)
|
||
for w in welt().bewohner:
|
||
var soll := DreamDB.hoehe_bei(w.global_position.x, w.global_position.z)
|
||
abweichung = maxf(abweichung, absf(w.global_position.y - soll))
|
||
p(abweichung < 0.05, "%d Bewohner bleiben auf dem Boden (Abweichung %.2f)" %
|
||
[welt().bewohner.size(), abweichung])
|
||
|
||
# Der Spieler kommt den Hang hinauf und fällt nicht hindurch. Dafür eine
|
||
# Richtung, in der kein Gebäude im Weg steht.
|
||
var richtung := Vector3.ZERO
|
||
var wenigste := 999
|
||
for k in range(24):
|
||
var a := TAU * float(k) / 24.0
|
||
var d := Vector3(cos(a), 0, sin(a))
|
||
var treffer := 0
|
||
for r in range(int(DreamDB.FLACH), int(DreamDB.FLACH) + 24, 2):
|
||
var q := d * float(r)
|
||
for key in DreamDB.PLACES:
|
||
var s: Array = DreamDB.PLACES[key]["size"]
|
||
if float(s[0]) <= 0.0:
|
||
continue
|
||
var c := Orte.center(DreamDB.PLACES, key)
|
||
if absf(q.x - c.x) < float(s[0]) * 0.5 + 2.5 and absf(q.z - c.z) < float(s[1]) * 0.5 + 2.5:
|
||
treffer += 1
|
||
if treffer < wenigste:
|
||
wenigste = treffer
|
||
richtung = d
|
||
p(wenigste == 0, "Eine freie Richtung den Hang hinauf gibt es (beste: %d Treffer)" % wenigste)
|
||
var start := richtung * (DreamDB.FLACH + 1.0) + Vector3(0, 1.0, 0)
|
||
var yaw := atan2(-richtung.x, -richtung.z)
|
||
var ende: Vector3 = await laufe(start, yaw, 140)
|
||
var boden_dort := DreamDB.hoehe_bei(ende.x, ende.z)
|
||
var weit := Vector2(ende.x, ende.z).length() - Vector2(start.x, start.z).length()
|
||
p(weit > 6.0, "Er läuft hangaufwärts (%.0f Einheiten weiter außen)" % weit)
|
||
p(ende.y > start.y + 1.0, "Und kommt dabei höher (y %.1f -> %.1f)" % [start.y, ende.y])
|
||
p(ende.y >= boden_dort - 0.2 and ende.y < boden_dort + 2.5,
|
||
"Er steht auf dem Boden, nicht darunter (y %.1f, Boden %.1f)" % [ende.y, boden_dort])
|
||
|
||
Game.completed.assign(merker)
|
||
Game.time_h = uhr_vorher
|
||
Game.day = tag_vorher
|
||
Game.welt_erzeugen()
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
|
||
|
||
func test_haus_betreten() -> void:
|
||
abschnitt("Orte betreten")
|
||
p(not Game.ort_offen(DreamDB.haus_key(0)), "Ohne Bewohner steht kein Haus")
|
||
Game.completed.append("probe_haus")
|
||
Game.welt_erzeugen()
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
var haus_name := str(DreamDB.PLACES[DreamDB.haus_key(0)]["name"])
|
||
p(_gebaute_orte().has(haus_name), "Mit dem ersten Bewohner steht sein Haus: %s" % haus_name)
|
||
var haus := Orte.center(DreamDB.PLACES, DreamDB.haus_key(0))
|
||
var tuer := Orte.door(DreamDB.PLACES, DreamDB.haus_key(0))
|
||
var hinein: Vector3 = (haus - tuer).normalized()
|
||
var yaw_hinein := atan2(-hinein.x, -hinein.z)
|
||
|
||
var ziel: Vector3 = await laufe(tuer - hinein * 1.5 + Vector3(0, 1.0, 0), yaw_hinein, 80)
|
||
var drin: bool = absf(ziel.x - haus.x) < 6.0 and absf(ziel.z - haus.z) < 5.0
|
||
p(drin, "Durch die Tür läuft man ins Haus (Ziel %.1f / %.1f, Haus %.1f / %.1f)" %
|
||
[ziel.x, ziel.z, haus.x, haus.z])
|
||
|
||
# Die Wand hält: von der türabgewandten Seite kommt niemand hinein
|
||
var rueck := haus + hinein * 11.0 + Vector3(0, 1.0, 0)
|
||
var gestoppt: Vector3 = await laufe(rueck, atan2(hinein.x, hinein.z), 80)
|
||
var drin2: bool = absf(gestoppt.x - haus.x) < 6.0 and absf(gestoppt.z - haus.z) < 5.0
|
||
p(not drin2, "Die Rückwand hält den Spieler auf (%.1f / %.1f)" % [gestoppt.x, gestoppt.z])
|
||
|
||
main.player.set_mode(Player.Mode.TRAUM)
|
||
main.player.global_position = (welt() as DreamWorld).spawn
|
||
Game.completed.clear()
|
||
Game.welt_erzeugen()
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
|
||
|
||
func test_hoersaal() -> void:
|
||
abschnitt("Techniken werden im Hörsaal gelehrt")
|
||
var pult := _marke("lehre")
|
||
p(pult != null, "Der Hörsaal hat ein Lehrpult")
|
||
var raum := Orte.center(DreamDB.PLACES, "hoersaal")
|
||
p(absf(pult.global_position.x - raum.x) < 8.0 and absf(pult.global_position.z - raum.z) < 6.0,
|
||
"Das Pult steht im Hörsaal (%.1f / %.1f)" % [pult.global_position.x, pult.global_position.z])
|
||
|
||
var im_saal: Vector3 = await laufe(Vector3(0, 1.0, 24.0), PI, 150)
|
||
p(im_saal.z > 33.0,
|
||
"Vom Eingang führt ein freier Mittelgang zum Pult (z = %.1f, Pult bei %.1f)" %
|
||
[im_saal.z, raum.z + 3.4])
|
||
p(absf(im_saal.x) < 2.0, "Der Gang hält den Spieler in der Mitte (x = %.1f)" % im_saal.x)
|
||
|
||
_hingehen(pult)
|
||
p(main._fokus == pult, "Das Pult wird als Fokus erkannt")
|
||
main._interagieren()
|
||
p(ui().offen and ui()._kind == "lehre", "Der Hörsaal öffnet sich")
|
||
var seite := _panel_text()
|
||
var basis_da := true
|
||
for id in _basiswege():
|
||
if not seite.contains(str(TechDB.by_id(id)["name"])):
|
||
basis_da = false
|
||
p(basis_da, "Die %d Basiswege werden gelehrt" % 1)
|
||
var lernbar := 0
|
||
for c in _alle_kinder(ui()._content):
|
||
if c is Button and (c as Button).text.begins_with("Diesen Weg"):
|
||
lernbar += 1
|
||
p(lernbar <= 1, "Und höchstens einer davon ist zu lernen (%d)" % lernbar)
|
||
p(seite.contains(str(TechDB.LIST[1]["name"])),
|
||
"Der Hörsaal nennt den nächsten noch unentdeckten Weg")
|
||
p(not Game.learn("box_atmung"), "Eine unentdeckte Technik lässt sich nicht lernen")
|
||
|
||
Game.known.clear()
|
||
ui().lehre_panel()
|
||
var vorher := Game.known.size()
|
||
p(_druecke("Diesen Weg"), "Ein Lern-Knopf steht bereit")
|
||
p(Game.known.size() == vorher + 1, "Im Hörsaal gelernt (%d -> %d)" % [vorher, Game.known.size()])
|
||
|
||
# Gelernt heißt fertig: der Lehrort bietet danach nichts mehr an
|
||
ui().lehre_panel()
|
||
var xp_summe := 0
|
||
for id in Game.known:
|
||
xp_summe += int(Game.known[id])
|
||
p(not _knopf_da("Vertiefen") and not _knopf_da("Üben") and not _knopf_da("Trainieren"),
|
||
"Kein Knopf trainiert eine gelernte Technik")
|
||
p(not _druecke("Diesen Weg"), "Auch der Lern-Knopf ist verschwunden")
|
||
var xp_neu := 0
|
||
for id in Game.known:
|
||
xp_neu += int(Game.known[id])
|
||
p(xp_neu == xp_summe, "Der Besuch im Hörsaal bringt keine Erfahrung (%d)" % xp_neu)
|
||
ui().schliessen()
|
||
|
||
# Ausgangslage für die folgenden Prüfungen wiederherstellen
|
||
Game.known = {"pomodoro": 0, "zerlegen": 0, "umgebung": 0}
|
||
|
||
|
||
func test_mission_annehmen() -> void:
|
||
abschnitt("Mission am Schwarzen Brett annehmen")
|
||
var brett := _marke("brett")
|
||
p(brett != null, "Das Schwarze Brett steht in der Traumwelt")
|
||
var spawn: Vector3 = (welt() as DreamWorld).spawn
|
||
p(absf(brett.global_position.x) > 2.5,
|
||
"Es steht neben der Achse Start–Tor, versperrt sie also nicht (x = %.1f)" %
|
||
brett.global_position.x)
|
||
p(brett.global_position.distance_to(spawn) < 14.0,
|
||
"Vom Start aus in Sichtweite (%.1f)" % brett.global_position.distance_to(spawn))
|
||
var hin: Vector3 = await laufe(Vector3(brett.global_position.x, 1.0, spawn.z), 0.0, 150)
|
||
p(hin.z < 14.0, "Man läuft vom Start ungehindert bis davor (z = %.1f)" % hin.z)
|
||
main._process(0.05)
|
||
p(main._fokus == brett, "Davor steht man in Reichweite")
|
||
ui().schliessen()
|
||
main._interagieren()
|
||
p(ui().offen and ui()._kind == "brett", "Das Brett öffnet sich")
|
||
# Nur wer mit dem Gelernten zu schaffen ist, hängt aus.
|
||
var alle_wege: Dictionary = Game.known.duplicate()
|
||
Game.known = {str(_basiswege()[0]): 0}
|
||
ui().brett_panel()
|
||
var aushang := _knopf_texte()
|
||
var gezeigt := 0
|
||
var fehlend := 0
|
||
for h in HumanDB.LIST:
|
||
var id: String = h["id"]
|
||
if aushang.contains(str(h["name"])):
|
||
gezeigt += 1
|
||
if not Game.machbar(id):
|
||
p(false, "%s hängt aus, obwohl kein Weg greift" % h["name"])
|
||
elif Game.machbar(id):
|
||
p(false, "%s wäre lösbar, hängt aber nicht aus" % h["name"])
|
||
else:
|
||
fehlend += 1
|
||
p(gezeigt > 0, "Mit einem Weg hängt mindestens ein Aushang (%d von %d)" %
|
||
[gezeigt, HumanDB.LIST.size()])
|
||
p(fehlend > 0, "Und mindestens einer bleibt verdeckt (%d)" % fehlend)
|
||
p(_panel_text().contains("verdeckt"), "Das Brett sagt, dass etwas verdeckt bleibt")
|
||
p(Game.offene_missionen().size() == gezeigt,
|
||
"Brett und offene_missionen() sind sich einig (%d)" % gezeigt)
|
||
|
||
# Ohne jede Technik bleibt das Brett leer, mit Hinweis auf den Hörsaal
|
||
Game.known.clear()
|
||
ui().brett_panel()
|
||
p(Game.offene_missionen().is_empty(), "Ohne Technik ist nichts zu holen")
|
||
p(_panel_text().contains("Hörsaal"), "Und das Brett schickt in den Hörsaal")
|
||
Game.known = alle_wege
|
||
ui().brett_panel()
|
||
p(Game.offene_missionen().size() >= gezeigt,
|
||
"Mehr Wege heißt mehr Aushänge (%d)" % Game.offene_missionen().size())
|
||
|
||
p(Game.active_mission == "", "Vorher keine Mission")
|
||
ui().aushang_panel("mensch_0")
|
||
p(_panel_text().contains(str(HumanDB.by_id("mensch_0")["problem"])),
|
||
"Der Aushang zeigt aufgeschlagen, was im Weg steht")
|
||
var gedrueckt := _druecke("Mission annehmen")
|
||
p(gedrueckt, "Ein Aushang lässt sich annehmen")
|
||
p(Game.active_mission == "mensch_0", "Mission Lena ist aktiv (%s)" % Game.active_mission)
|
||
p(Game.angenommen("mensch_0"), "Lena steht als angenommen im Buch")
|
||
ui().schliessen()
|
||
|
||
ui().journal_panel("missionen")
|
||
var buch := _panel_text()
|
||
p(_knopf_texte().contains(str(HumanDB.by_id("mensch_0")["name"])),
|
||
"Das Journal führt die angenommene Mission")
|
||
p(not buch.contains(str(HumanDB.by_id("mensch_1")["name"])),
|
||
"Nicht angenommene Menschen fehlen im Journal")
|
||
p(not _knopf_da("Mission annehmen"), "Im Journal lässt sich keine Mission annehmen")
|
||
ui().schliessen()
|
||
|
||
|
||
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")
|
||
|
||
# Zu Fuß hin: die Torplattform darf keine unüberwindliche Stufe sein
|
||
ui().schliessen()
|
||
main.player.set_mode(Player.Mode.TRAUM)
|
||
main.player.look_enabled = true
|
||
main.player.global_position = Vector3(0, 1.0, -20.0)
|
||
main.player.velocity = Vector3.ZERO
|
||
main.player.yaw = 0.0
|
||
Input.action_press("vor")
|
||
var erreicht := false
|
||
var weit: float = main.player.global_position.z
|
||
for i in range(200):
|
||
await get_tree().physics_frame
|
||
main._process(0.02)
|
||
weit = main.player.global_position.z
|
||
if main._fokus == tor:
|
||
erreicht = true
|
||
break
|
||
Input.action_release("vor")
|
||
p(erreicht, "Zu Fuß kommt man ans Tor — keine Stufe im Weg (z = %.1f, Tor bei %.1f)" %
|
||
[weit, tor.global_position.z])
|
||
|
||
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() == HumanDB.LIST.size() and welt().humans.size() >= 3,
|
||
"%d Menschen leben in der Stadt" % welt().humans.size())
|
||
|
||
|
||
## Beide Welten bleiben bestehen: wer zurückkehrt, findet dieselbe Stadt, und
|
||
## die Menschen haben derweil ihren Tag weitergelebt.
|
||
func test_welten_bleiben() -> void:
|
||
abschnitt("Die Welten bleiben bestehen")
|
||
var erde_vorher: Node3D = welt()
|
||
var mensch: Bewohner = welt().humans[0]
|
||
Game.time_h = 9.5
|
||
schritte(20, 0.05, 9.5)
|
||
var stand: Vector3 = mensch.global_position
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
p(welt() is DreamWorld and is_instance_valid(erde_vorher) and not erde_vorher.visible,
|
||
"Im Traum bleibt die Erde bestehen, nur unsichtbar")
|
||
var himmel := 0
|
||
for we in get_tree().root.find_children("*", "WorldEnvironment", true, false):
|
||
if (we as WorldEnvironment).environment != null:
|
||
himmel += 1
|
||
p(himmel == 1, "Genau ein Himmel ist aktiv (%d)" % himmel)
|
||
for i in range(400):
|
||
Game.time_h = 12.5
|
||
for m in erde_vorher.humans:
|
||
m._process(0.05)
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
p(welt() == erde_vorher, "Zurück auf der Erde ist es dieselbe Stadt")
|
||
p(mensch.global_position.distance_to(stand) > 3.0,
|
||
"%s hat derweil ihren Tag weitergelebt (%.0f m weiter)" % [mensch.data["name"],
|
||
mensch.global_position.distance_to(stand)])
|
||
p(main.player.collision_mask == 2, "Der Geist stößt nur an die Erde, nicht an die unsichtbare Insel")
|
||
|
||
|
||
## Spuren in der Wohnung: wer hineingeht und hinsieht, kennt die Ursache und
|
||
## weiß, welche Stunden im Tag sich lohnen.
|
||
func test_spuren() -> void:
|
||
abschnitt("Spuren in der Wohnung")
|
||
if not main.auf_erde:
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
ui().schliessen()
|
||
var sp: Player = main.player
|
||
sp.folgt = null
|
||
var mensch: Bewohner = welt().humans[0]
|
||
var id := str(mensch.data["id"])
|
||
var heim := str(mensch.data["heim"])
|
||
var ursachen: Array = mensch.data["ursachen"]
|
||
Game.active_mission = id
|
||
Game.spuren.erase(id)
|
||
var spuren: Array = []
|
||
for n in get_tree().get_nodes_in_group("interactable"):
|
||
if n.get_meta("kind", "") == "spur" and str(n.get_meta("human")) == id:
|
||
spuren.append(n)
|
||
p(spuren.size() >= 1 and spuren.size() <= ursachen.size(),
|
||
"In %s liegen %d Spuren, je Ursache höchstens eine (%d Ursachen)" %
|
||
[HumanDB.PLACES[heim]["name"], spuren.size(), ursachen.size()])
|
||
var c := Orte.center(HumanDB.PLACES, heim)
|
||
var g: Array = HumanDB.PLACES[heim]["size"]
|
||
var drinnen := 0
|
||
for n in spuren:
|
||
var q: Vector3 = (n as Node3D).global_position
|
||
if absf(q.x - c.x) < float(g[0]) * 0.5 and absf(q.z - c.z) < float(g[1]) * 0.5 \
|
||
and absf(q.y) < 2.0:
|
||
drinnen += 1
|
||
p(drinnen == spuren.size(), "Alle liegen in seiner Wohnung")
|
||
if spuren.is_empty():
|
||
Game.active_mission = ""
|
||
return
|
||
var spur: Node3D = spuren[0]
|
||
var u := str(spur.get_meta("arg"))
|
||
p(not Game.ursache_bekannt(id, u), "Vorher ist die Ursache '%s' unbekannt" % Ursachen.LISTE[u]["name"])
|
||
var verdaechtig_vorher := 0
|
||
for nr in range(Game.slots(id).size()):
|
||
if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG:
|
||
verdaechtig_vorher += 1
|
||
sp.global_position = spur.global_position + Vector3(0.8, 0.3, 0.8)
|
||
sp.velocity = Vector3.ZERO
|
||
main._process(0.05)
|
||
p(main._fokus == spur and ui()._hinweis.text.contains("Spur"),
|
||
"Vor der Spur zeigt der Hinweis sie an: %s" % ui()._hinweis.text)
|
||
main._interagieren()
|
||
p(Game.spur_bekannt(id, u) and Game.ursache_bekannt(id, u),
|
||
"E liest die Spur: die Ursache '%s' steht im Journal" % Ursachen.LISTE[u]["name"])
|
||
var verdaechtig := 0
|
||
for nr in range(Game.slots(id).size()):
|
||
if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG:
|
||
verdaechtig += 1
|
||
p(verdaechtig_vorher == 0 and verdaechtig >= 1,
|
||
"Die Stunden dieser Ursache sind jetzt verdächtig (%d), gesehen sind sie nicht" % verdaechtig)
|
||
main._process(0.05)
|
||
ui()._ablauf_aktualisieren()
|
||
p(ui()._ablauf_titel.text.contains("verdächtig"), "Die Zeitleiste sagt es: %s" % ui()._ablauf_titel.text)
|
||
p(not Game.spur_lesen(id, u), "Noch einmal lesen bringt nichts Neues")
|
||
Game.spuren.erase(id)
|
||
Game.active_mission = ""
|
||
|
||
|
||
## Wer mit Mission zur Erde reist, landet vor der Tür seines Ziels; ein Knopf
|
||
## bringt ihn jederzeit dorthin.
|
||
func test_ankunft() -> void:
|
||
abschnitt("Ankunft beim Ziel")
|
||
var ziel: Bewohner = welt().humans[0]
|
||
var id := str(ziel.data["id"])
|
||
Game.active_mission = id
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
var nah: float = _xz(main.player.global_position).distance_to(_xz(ziel.global_position))
|
||
var tuer := Orte.door(HumanDB.PLACES, ziel.place) if HumanDB.PLACES.has(ziel.place) \
|
||
and float(HumanDB.PLACES[ziel.place]["size"][0]) > 0.0 else ziel.global_position
|
||
var vor_tuer: float = _xz(main.player.global_position).distance_to(_xz(tuer))
|
||
p(minf(nah, vor_tuer) < 4.0 and main.player.global_position.y > -0.5,
|
||
"Mit Mission landet man vor der Tür von %s (%.1f m)" % [ziel.data["name"], minf(nah, vor_tuer)])
|
||
var andere: Bewohner = welt().humans[3]
|
||
main.hinfahren(str(andere.data["id"]))
|
||
var d2: float = _xz(main.player.global_position).distance_to(_xz(andere.global_position))
|
||
p(d2 < 12.0 and main.auf_erde, "„Hinfahren“ setzt einen zu %s (%.1f m)" % [andere.data["name"], d2])
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
main.hinfahren(id)
|
||
p(main.auf_erde, "Aus dem Traum heraus geht es ebenfalls, über das Tor")
|
||
Game.active_mission = ""
|
||
|
||
|
||
## Die Akte: ein Blatt je Mensch mit Person, Traum, Befund, Tag und Notizen.
|
||
func test_akte() -> void:
|
||
abschnitt("Die Akte")
|
||
var ziel: Bewohner = welt().humans[0]
|
||
var id := str(ziel.data["id"])
|
||
var h := HumanDB.by_id(id)
|
||
Game.active_mission = id
|
||
Game.progress[id] = Game.progress.get(id, 0.0)
|
||
Game.spuren.erase(id)
|
||
Game.notizen.erase(id)
|
||
ui().journal_panel("missionen")
|
||
p(_knopf_da(str(h["name"])), "Im Journal liegt eine Akte je Mensch mit Mission")
|
||
p(_druecke(str(h["name"])) and ui()._kind == "akte", "Sie lässt sich aufschlagen")
|
||
var text := _panel_text()
|
||
p(text.contains(str(h["name"])) and text.contains(str(h["traum"])) and text.contains(str(h["problem"])),
|
||
"Oben stehen Person, Traum und was im Weg steht")
|
||
p(text.contains("BEFUND") and text.contains("TAG") and text.contains("NOTIZEN"),
|
||
"Darunter Befund, Tag und Notizen")
|
||
p(text.contains("noch keine") and text.contains("noch nichts gefunden"),
|
||
"Ohne Spuren und Beobachtungen ist der Befund leer")
|
||
p(_knopf_da("Hinfahren"), "Ein Knopf bringt einen vor die Tür")
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
var breit := 0.0
|
||
for c in _alle_kinder(ui()._content):
|
||
if c is Control:
|
||
breit = maxf(breit, (c as Control).size.x)
|
||
p(ui()._drawer.size.x <= UI.DRAWER_BREITE + 1.0 and breit <= ui()._content.size.x + 1.0,
|
||
"Nichts ragt aus der Leiste (Leiste %.0f, breitestes Element %.0f)" % [ui()._drawer.size.x, breit])
|
||
ui().journal_panel("techniken")
|
||
p(not ui()._drawer.has_theme_stylebox_override("panel"), "Andere Seiten sind wieder dunkel")
|
||
# keine Seite der Leiste darf breiter sein als die Leiste -- und die Leiste nicht als das Fenster
|
||
var zu_breit: Array = []
|
||
for seite in ["techniken", "missionen"]:
|
||
ui().journal_panel(seite)
|
||
await get_tree().process_frame
|
||
var mind := ui()._content.get_combined_minimum_size().x
|
||
if mind > UI.DRAWER_BREITE - 30.0 or ui()._drawer.size.x > UI.DRAWER_BREITE + 1.0:
|
||
zu_breit.append("%s (%.0f)" % [seite, mind])
|
||
ui().akte_panel(id)
|
||
await get_tree().process_frame
|
||
if ui()._content.get_combined_minimum_size().x > UI.DRAWER_BREITE - 30.0 \
|
||
or ui()._drawer.size.x > UI.DRAWER_BREITE + 1.0:
|
||
zu_breit.append("akte (%.0f)" % ui()._content.get_combined_minimum_size().x)
|
||
p(zu_breit.is_empty(), "Journal und Akte bleiben in der Leistenbreite%s" %
|
||
("" if zu_breit.is_empty() else " — zu breit: " + ", ".join(zu_breit)))
|
||
var rand: float = ui()._drawer.position.x + ui()._drawer.size.x
|
||
p(rand <= ui()._root.size.x, "Die Leiste endet im Fenster (%.0f von %.0f)" % [rand, ui()._root.size.x])
|
||
var hinweis_rechts := 0.0
|
||
for c in ui()._root.get_children():
|
||
if c is Label and (c as Label).text.begins_with("J Journal"):
|
||
hinweis_rechts = (c as Control).position.x + (c as Control).size.x
|
||
p(hinweis_rechts > 0.0 and hinweis_rechts <= ui()._root.size.x,
|
||
"Der Steuerungshinweis endet im Fenster (%.0f von %.0f)" % [hinweis_rechts, ui()._root.size.x])
|
||
var u := str((h["ursachen"] as Array)[0])
|
||
Game.spur_lesen(id, u)
|
||
ui().akte_panel(id)
|
||
text = _panel_text()
|
||
p(text.contains(str(Ursachen.LISTE[u]["name"])) and text.contains("Spur gelesen"),
|
||
"Eine gelesene Spur steht als Befund: %s" % Ursachen.LISTE[u]["name"])
|
||
p(text.contains("Uhr —") and text.contains("Spur:"), "Und als Notiz mit Uhrzeit")
|
||
ui().schliessen()
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
ui().akte_panel(id)
|
||
p(_druecke("Hinfahren") and main.auf_erde and not ui().offen,
|
||
"„Hinfahren“ führt aus dem Traum vor die Tür von %s" % h["name"])
|
||
Game.active_mission = ""
|
||
Game.spuren.erase(id)
|
||
Game.notizen.erase(id)
|
||
|
||
|
||
## Passanten beleben die Straßen: ohne Namen, ohne Schild, ohne Mission.
|
||
func test_passanten() -> void:
|
||
abschnitt("Passanten beleben die Straßen")
|
||
if not main.auf_erde:
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
p(welt().passanten.size() == HumanDB.PASSANTEN and welt().humans.size() == HumanDB.LIST.size(),
|
||
"%d Passanten neben den %d Menschen mit Geschichte" % [welt().passanten.size(), welt().humans.size()])
|
||
var unterwegs := 0
|
||
var proben := 0
|
||
for i in range(1200):
|
||
schritte(1, 0.05, 9.0 + float(i) * 0.004)
|
||
if i % 100 == 99:
|
||
proben += 1
|
||
for pz in welt().passanten:
|
||
if not pz._path.is_empty():
|
||
unterwegs += 1
|
||
p(unterwegs >= proben, "Zu jeder Zeit ist im Schnitt mindestens einer unterwegs (%d in %d Proben)" %
|
||
[unterwegs, proben])
|
||
var schilder := 0
|
||
for pz in welt().passanten:
|
||
if (pz._label as Label3D).visible:
|
||
schilder += 1
|
||
p(schilder == 0, "Passanten tragen kein Schild")
|
||
var pz0: Bewohner = welt().passanten[0]
|
||
main.player.global_position = pz0.global_position + Vector3(0, 0.3, 0)
|
||
main._process(0.05)
|
||
p(main._fokus_mensch != pz0, "Und kommen nicht in den Fokus")
|
||
|
||
|
||
## Die Traumwesen helfen: ein Bewohner flüstert, was in der Wohnung des
|
||
## Menschen liegt, um den es gerade geht.
|
||
func test_wesen_helfen() -> void:
|
||
abschnitt("Die Traumwesen helfen")
|
||
var merker: Array = Game.completed.duplicate()
|
||
Game.completed.clear()
|
||
Game.completed.append("mensch_1")
|
||
Game.welt_erzeugen()
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
p(welt().bewohner.size() == 1, "Mit einem erfüllten Traum wohnt ein Wesen hier")
|
||
var wesen: Bewohner = welt().bewohner[0]
|
||
p(str(wesen.data["rolle"]).contains("Traum"), "Es trägt, aus wessen Traum es kam: %s" % wesen.data["rolle"])
|
||
var marke: Node3D = null
|
||
for n in get_tree().get_nodes_in_group("interactable"):
|
||
if n.get_meta("kind", "") == "wesen":
|
||
marke = n
|
||
p(marke != null, "Man kann es ansprechen")
|
||
Game.active_mission = "mensch_0"
|
||
Game.spuren.erase("mensch_0")
|
||
main.player.set_mode(Player.Mode.TRAUM)
|
||
main.player.global_position = wesen.global_position + Vector3(0.5, 0.3, 0.5)
|
||
main.player.velocity = Vector3.ZERO
|
||
main._process(0.05)
|
||
p(main._fokus == marke and ui()._hinweis.text.contains("zuhören"), "Davor steht: zuhören")
|
||
main._interagieren()
|
||
var bekannt := 0
|
||
for u in Game.ursachen_von("mensch_0"):
|
||
if Game.spur_bekannt("mensch_0", str(u)):
|
||
bekannt += 1
|
||
p(bekannt == 1, "Es flüstert eine Spur des Menschen, um den es geht (%d bekannt)" % bekannt)
|
||
Game.spuren.erase("mensch_0")
|
||
Game.active_mission = ""
|
||
Game.completed.assign(merker)
|
||
Game.welt_erzeugen()
|
||
main._erde() # die folgenden Abschnitte spielen auf der Erde
|
||
await get_tree().process_frame
|
||
|
||
|
||
func test_tagesablauf() -> void:
|
||
abschnitt("Tagesablauf der Menschen")
|
||
var mensch: Bewohner = null
|
||
for m in welt().humans:
|
||
if m.data["id"] == "mensch_0":
|
||
mensch = m
|
||
p(mensch != null, "Der erste Mensch lebt auf der Erde: %s" % mensch.data["name"])
|
||
|
||
for i in range(3000): # die Welt bleibt bestehen: er kann weit weg sein
|
||
schritte(1, 0.05, 2.0)
|
||
if mensch.place == str(mensch.data["heim"]) and mensch._path.is_empty():
|
||
break
|
||
p(mensch.activity == "schlaeft", "Um 02:00 schläft er (%s)" % mensch.activity)
|
||
p(_xz(mensch.global_position).distance_to(
|
||
_xz(Orte.center(HumanDB.PLACES, str(mensch.data["heim"])))) < 5.0,
|
||
"Und zwar zu Hause")
|
||
|
||
# Jede Station des eigenen Tagesablaufs wird pünktlich erreicht
|
||
var verfehlt: Array = []
|
||
for e in mensch.data["routine"]:
|
||
var stunde: float = float(e[0]) + 0.5
|
||
var da := false
|
||
for i in range(1200):
|
||
schritte(1, 0.05, stunde)
|
||
if mensch.place == e[1] and mensch._path.is_empty():
|
||
da = true
|
||
break
|
||
if not da or mensch.activity != e[2]:
|
||
verfehlt.append("%s um %d Uhr" % [e[1], int(e[0])])
|
||
p(verfehlt.is_empty(), "Alle %d Stationen seines Tages werden erreicht (%s)" %
|
||
[(mensch.data["routine"] as Array).size(),
|
||
"lückenlos" if verfehlt.is_empty() else ", ".join(verfehlt)])
|
||
|
||
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 Orte.kennt(str(r[1])):
|
||
luecken += 1
|
||
p(luecken == 0, "Alle %d Menschen haben rund um die Uhr Ort und Tätigkeit" %
|
||
welt().humans.size())
|
||
|
||
Game.time_h = 0.0
|
||
Game.day = 1
|
||
var besucht: Dictionary = {}
|
||
for m in welt().humans:
|
||
besucht[m.data["id"]] = {}
|
||
for i in range(9000):
|
||
schritte(1)
|
||
for m in welt().humans:
|
||
if m._path.is_empty():
|
||
besucht[m.data["id"]][m.place] = true
|
||
if Game.time_h >= 23.5:
|
||
break
|
||
var alle_ok := true
|
||
for m in welt().humans:
|
||
var soll: Dictionary = {}
|
||
for e in m.data["routine"]:
|
||
soll[e[1]] = true
|
||
if besucht[m.data["id"]].size() < soll.size():
|
||
alle_ok = false
|
||
p(false, "%s erreichte nur %d von %d Stationen" %
|
||
[m.data["name"], besucht[m.data["id"]].size(), 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")
|
||
_wege_pruefen("Erde", HumanDB.PLACES, HumanDB.LIST, main.erde.nav_map)
|
||
_wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST, main.traum.nav_map)
|
||
|
||
|
||
func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array, karte: RID) -> void:
|
||
var sek_pro_stunde := Game.DAY_SECONDS / 24.0
|
||
var alles_ok := true
|
||
var engste := 999.0
|
||
var engste_text := ""
|
||
for m in liste:
|
||
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(places, von, nach, karte) / Bewohner.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, "%s — jeder Weg passt, engster Fall: %s" % [welt_name, engste_text])
|
||
|
||
|
||
## Weglänge über das Navigationsnetz der Welt -- so, wie die Menschen wirklich laufen.
|
||
func _weg_laenge(places: Dictionary, von: String, nach: String, karte: RID) -> float:
|
||
var a := Orte.center(places, von)
|
||
var b := Orte.center(places, nach)
|
||
# freie Plätze: neben der Mitte stehen, wie die Bewohner auch (in der Mitte steht der Traumbaum)
|
||
if float(places[von]["size"][0]) <= 0.0:
|
||
a += Vector3(2.2, 0, 0)
|
||
if float(places[nach]["size"][0]) <= 0.0:
|
||
b += Vector3(2.2, 0, 0)
|
||
var pts := NavigationServer3D.map_get_path(karte, a, b, true)
|
||
if pts.size() < 2:
|
||
return 9999.0 # kein Weg: fällt als "passt nicht" auf
|
||
var l := 0.0
|
||
for i in range(1, pts.size()):
|
||
l += pts[i - 1].distance_to(pts[i])
|
||
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_mask != 0, "Der Geist stößt an Wände wie ein Mensch")
|
||
main.player.global_position = Vector3(0, 8.0, 0)
|
||
main.player.velocity = Vector3(6, 0, 0)
|
||
for i in range(60):
|
||
await get_tree().physics_frame
|
||
p(main.player.global_position.x > 0.5, "Der Geist bewegt sich durch den Park")
|
||
p(main.player.global_position.y < 0.6 and main.player.is_on_floor(),
|
||
"Die Schwerkraft holt ihn auf den Boden (y = %.2f)" % main.player.global_position.y)
|
||
|
||
var lena: Bewohner = welt().humans[0]
|
||
for i in range(1500): # erst, wenn sie irgendwo angekommen ist
|
||
if lena._path.is_empty():
|
||
break
|
||
schritte(1)
|
||
var davor: Vector3 = lena.global_position
|
||
var uhr_geist := Game.time_h
|
||
main.player.global_position = lena.global_position + Vector3(0, 1, 0.5)
|
||
schritte(10, 0.05, uhr_geist)
|
||
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
|
||
|
||
|
||
func _farbabstand(a: Color, b: Color) -> float:
|
||
return Vector3(a.r, a.g, a.b).distance_to(Vector3(b.r, b.g, b.b))
|
||
|
||
|
||
func test_charakter() -> void:
|
||
abschnitt("Der Geist und die Menschen sind am Körper lesbar")
|
||
ui().schliessen()
|
||
var sp: Player = main.player
|
||
sp.set_mode(Player.Mode.GEIST)
|
||
sp.look_enabled = true
|
||
sp.folgt = null
|
||
Game.situation = {} # ein laufender Moment ließe die Farbe flackern
|
||
p(sp._augen.size() == 2 and sp._augen[0].position.z < 0.0, "Zwei Augen, vorn am Kopf")
|
||
p(sp._funken.size() == Player.FUNKEN_MAX, "%d Funken stehen bereit" % Player.FUNKEN_MAX)
|
||
for i in range(60):
|
||
await get_tree().physics_frame
|
||
var sichtbar := 0
|
||
for f in sp._funken:
|
||
if f.visible:
|
||
sichtbar += 1
|
||
var soll := clampi(Game.known.size(), 1, Player.FUNKEN_MAX)
|
||
p(sichtbar == soll, "Je gelernter Technik kreist ein Funke (%d bei %d Techniken)" %
|
||
[sichtbar, Game.known.size()])
|
||
p(_farbabstand(sp._farbe, Player.FARBE_GEIST) < 0.1, "Beim Beobachten leuchtet er kühl")
|
||
var mensch: Bewohner = welt().humans[0]
|
||
sp.global_position = mensch.global_position + Vector3(0, 1.5, 2.0)
|
||
sp.folgt = mensch
|
||
for i in range(90):
|
||
await get_tree().physics_frame
|
||
p(_farbabstand(sp._farbe, Player.FARBE_FOLGE) < 0.1, "Beim Verfolgen leuchtet er warm")
|
||
sp.folgt = null
|
||
sp.global_position = Vector3(0, 0.5, 9.0) # freier Rasen im Park
|
||
sp.velocity = Vector3.ZERO
|
||
sp.yaw = 0.0
|
||
Input.action_press("vor")
|
||
for i in range(40):
|
||
await get_tree().physics_frame
|
||
Input.action_release("vor")
|
||
var kopf: Vector3 = sp.body.to_global(Vector3(0, 1.1, 0))
|
||
var hinten := 0
|
||
for k in sp._schleppe:
|
||
if k.visible and (k as Node3D).global_position.distance_to(kopf) > 0.3:
|
||
hinten += 1
|
||
p(hinten >= 2, "Beim Fliegen zieht er einen Schweif hinter sich her (%d von 3)" % hinten)
|
||
for i in range(40):
|
||
await get_tree().physics_frame
|
||
var ruht := true
|
||
for k in sp._schleppe:
|
||
if k.visible:
|
||
ruht = false
|
||
p(ruht, "Im Stand verschwindet der Schweif")
|
||
|
||
# Haltung der Menschen: Angst duckt, Erschöpfung beugt -- bis überwunden
|
||
var merker := Game.ueberwunden.duplicate(true)
|
||
var muede: Bewohner = null
|
||
var bang: Bewohner = null
|
||
var frei: Bewohner = null
|
||
for m in welt().humans:
|
||
var pr: Array = m.data.get("probleme", [])
|
||
if muede == null and pr.has("erschoepfung"):
|
||
muede = m
|
||
elif bang == null and pr.has("angst"):
|
||
bang = m
|
||
elif frei == null and not pr.has("angst") and not pr.has("erschoepfung"):
|
||
frei = m
|
||
p(muede != null and bang != null and frei != null, "Müde, ängstliche und freie Menschen sind da")
|
||
if muede != null:
|
||
Game.ueberwunden.erase(str(muede.data["id"]))
|
||
if bang != null:
|
||
Game.ueberwunden.erase(str(bang.data["id"]))
|
||
schritte(80)
|
||
p(muede != null and muede._ober.rotation.x < -0.25,
|
||
"Erschöpfung beugt den Oberkörper (%.2f)" % (muede._ober.rotation.x if muede else 0.0))
|
||
p(bang != null and bang._ober.scale.x < 0.9,
|
||
"Angst macht klein (%.2f)" % (bang._ober.scale.x if bang else 0.0))
|
||
p(frei != null and absf(frei._ober.rotation.x) < 0.05 and frei._ober.scale.x > 0.98,
|
||
"Ohne diese Klemmen steht man aufrecht")
|
||
if muede != null:
|
||
Game.ueberwunden[str(muede.data["id"])] = {"erschoepfung": true}
|
||
schritte(200)
|
||
p(muede._ober.rotation.x > -0.05, "Überwunden richtet er sich auf (%.2f)" % muede._ober.rotation.x)
|
||
Game.ueberwunden = merker
|
||
|
||
# Hüte nach Beruf
|
||
var kappen: Dictionary = {}
|
||
for m in welt().humans:
|
||
var typ := str(HumanDB.BERUF_TAGESTYP.get(str(m.data.get("beruf", "")), ""))
|
||
kappen[typ] = m._ober.get_child_count()
|
||
p(kappen.has("heim") and kappen.has("frueh") and kappen.has("buero")
|
||
and int(kappen["frueh"]) > int(kappen["heim"]) and int(kappen["buero"]) > int(kappen["heim"]),
|
||
"Berufe tragen verschiedene Kopfbedeckungen (%s)" % str(kappen))
|
||
p(mensch._arme.size() == 2, "Jeder hat zwei Arme")
|
||
sp.velocity = Vector3.ZERO
|
||
|
||
|
||
func _xz(v: Vector3) -> Vector2:
|
||
return Vector2(v.x, v.z)
|
||
|
||
|
||
func test_kamera_und_etagen() -> void:
|
||
abschnitt("Feste Kamera, kleine Menschen, geschlossene Häuser")
|
||
ui().schliessen()
|
||
var sp: Player = main.player
|
||
sp.set_mode(Player.Mode.GEIST)
|
||
sp.look_enabled = true
|
||
sp.folgt = null
|
||
p(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "Die Maus ist frei")
|
||
p(is_equal_approx(sp.arm.rotation.x, Player.PITCH) and sp.cam.get_parent() == sp.arm,
|
||
"Die Kamera schaut fest von schräg oben (%.0f°)" % rad_to_deg(-Player.PITCH))
|
||
sp.yaw = Player.CAM_YAW
|
||
var yaw_vorher: float = sp.yaw
|
||
var dreh := InputEventAction.new()
|
||
dreh.action = "drehen"
|
||
dreh.pressed = true
|
||
Input.parse_input_event(dreh)
|
||
for i in range(80):
|
||
await get_tree().physics_frame
|
||
p(absf(angle_difference(yaw_vorher + Player.DREHSCHRITT, sp.yaw)) < 0.05,
|
||
"Q dreht die Kamera um 90° (%.0f°)" % rad_to_deg(sp.yaw - yaw_vorher))
|
||
var rad := InputEventMouseButton.new()
|
||
rad.button_index = MOUSE_BUTTON_WHEEL_DOWN
|
||
rad.pressed = true
|
||
Input.parse_input_event(rad)
|
||
for i in range(150):
|
||
await get_tree().physics_frame
|
||
p(absf(sp.cam.position.z - float(Player.ZOOM[1])) < 0.5,
|
||
"Mausrad zoomt heraus (%.1f)" % sp.cam.position.z)
|
||
rad = InputEventMouseButton.new()
|
||
rad.button_index = MOUSE_BUTTON_WHEEL_UP
|
||
rad.pressed = true
|
||
Input.parse_input_event(rad)
|
||
for i in range(150):
|
||
await get_tree().physics_frame
|
||
p(absf(sp.cam.position.z - float(Player.ZOOM[0])) < 0.5,
|
||
"Und wieder heran (%.1f)" % sp.cam.position.z)
|
||
|
||
var mensch: Bewohner = welt().humans[0]
|
||
p(mensch._visual.scale.x < 0.85 and sp.body.scale.x < 0.7,
|
||
"Menschen und Geist sind kleiner (%.2f / %.2f)" % [mensch._visual.scale.x, sp.body.scale.x])
|
||
|
||
# Geschlossene Häuser: einstöckig, ein Raum mit Tür
|
||
var eng := 0
|
||
for key in HumanDB.PLACES:
|
||
var sz: Array = HumanDB.PLACES[key]["size"]
|
||
if float(sz[0]) > 0.0 and minf(float(sz[0]), float(sz[1])) < 6.0:
|
||
eng += 1
|
||
p(eng == 0, "Jedes Gebäude ist breit genug für Tür und Möbel (%d zu schmal)" % eng)
|
||
for b in welt()._bauten:
|
||
for mi in Prims._meshes_von(b):
|
||
var bm := (mi as MeshInstance3D).mesh as BoxMesh
|
||
if bm != null and (bm.size.x <= 0.0 or bm.size.y <= 0.0 or bm.size.z <= 0.0):
|
||
eng += 1
|
||
p(eng == 0, "Keine Platte mit negativer Größe")
|
||
var oben: Bewohner = welt().humans[0]
|
||
var heim := str(oben.data["heim"])
|
||
var c := Orte.center(HumanDB.PLACES, heim)
|
||
var g: Array = HumanDB.PLACES[heim]["size"]
|
||
var uhr := Game.time_h
|
||
var zuhause := false
|
||
for i in range(3000):
|
||
schritte(1, 0.05, 2.0)
|
||
if oben.place == heim and oben._path.is_empty():
|
||
zuhause = true
|
||
break
|
||
p(zuhause and absf(oben.global_position.y) < 0.3,
|
||
"Nachts liegt %s zu Hause, ebenerdig (y = %.1f)" % [oben.data["name"], oben.global_position.y])
|
||
|
||
# Schnitt wie bei den Sims: drinnen verschwindet das Dach, alle Wände werden Stümpfe
|
||
var bau: Dictionary = {}
|
||
for b in welt()._bauten:
|
||
if (b["c"] as Vector3).distance_to(c) < 0.1:
|
||
bau = b
|
||
p(not bau.is_empty() and (bau["teile"] as Array).size() >= 1 and (bau["waende"] as Array).size() >= 4,
|
||
"Sein Haus steht mit Dach und Wänden da")
|
||
sp.global_position = c + Vector3(0, 0.3, 0)
|
||
sp.velocity = Vector3.ZERO
|
||
sp.yaw = Player.CAM_YAW
|
||
await get_tree().physics_frame
|
||
welt()._process(0.05)
|
||
var stuempfe := 0
|
||
var hoch := 0
|
||
for w in bau["waende"]:
|
||
var bm := (w as MeshInstance3D).mesh as BoxMesh
|
||
if (w as Node3D).visible and bm.size.y <= Prims.STUMPF + 0.01:
|
||
stuempfe += 1
|
||
elif (w as Node3D).visible:
|
||
hoch += 1
|
||
p(not (bau["teile"][0] as Node3D).visible, "Steht der Geist im Haus, verschwindet das Dach")
|
||
p(stuempfe >= 4 and hoch == 0,
|
||
"Alle Wände sind kniehohe Stümpfe, keine bleibt hoch, nichts wird durchsichtig (%d Stümpfe)" % stuempfe)
|
||
var durchsichtig := 0
|
||
for b in welt()._bauten:
|
||
for mi in Prims._meshes_von(b):
|
||
if (mi as GeometryInstance3D).transparency > 0.0:
|
||
durchsichtig += 1
|
||
p(durchsichtig == 0, "Kein Bau ist durchsichtig")
|
||
# nichts über Kniehöhe verdeckt drinnen den Blick von oben: kein Band, keine Decke
|
||
var hoch_sichtbar: Array = []
|
||
var knoten: Array = (bau["teile"] as Array).duplicate()
|
||
knoten.append(bau["raum"])
|
||
for k in knoten:
|
||
for mi in (k as Node).find_children("*", "MeshInstance3D", true, false):
|
||
var m := mi as MeshInstance3D
|
||
if not m.is_visible_in_tree():
|
||
continue
|
||
var kasten := m.get_aabb()
|
||
var kante: float = m.global_position.y + kasten.end.y * m.scale.y
|
||
var ist_wand := (bau["waende"] as Array).has(m)
|
||
if kante > 1.9 and not ist_wand:
|
||
hoch_sichtbar.append("%s (bis %.1f m)" % [m.name, kante])
|
||
p(hoch_sichtbar.is_empty(), "Drinnen verdeckt nichts über Kniehöhe die Sicht%s" %
|
||
("" if hoch_sichtbar.is_empty() else " — " + ", ".join(hoch_sichtbar)))
|
||
sp.global_position = c + Vector3(float(g[0]) + 4.0, 0.3, 0)
|
||
await get_tree().physics_frame
|
||
welt()._process(0.05)
|
||
var alles := (bau["teile"][0] as Node3D).visible
|
||
for w in bau["waende"]:
|
||
var bm := (w as MeshInstance3D).mesh as BoxMesh
|
||
if not (w as Node3D).visible or bm.size.y <= Prims.STUMPF + 0.01:
|
||
alles = false # der Türsturz ist niedrig, aber kein Stumpf
|
||
p(alles, "Draußen steht das Haus wieder ganz, Wände in voller Höhe")
|
||
var hoechster_bau := 0.0
|
||
for b in welt()._bauten:
|
||
hoechster_bau = maxf(hoechster_bau, float(b["hoch"]))
|
||
p(hoechster_bau <= 3.5, "Kein Bau ist höher als ein Erdgeschoss (%.1f m)" % hoechster_bau)
|
||
|
||
# Klick auf einen Menschen
|
||
sp.global_position = oben.global_position + Vector3(2.0, 1.4, 2.0)
|
||
sp.velocity = Vector3.ZERO
|
||
await get_tree().physics_frame
|
||
var schirm := sp.cam.unproject_position(oben.global_position + Vector3(0, 0.8, 0))
|
||
main._klick(schirm)
|
||
p(sp.folgt == oben, "Ein Klick auf einen Menschen hängt den Geist an ihn")
|
||
main._klick(schirm)
|
||
p(sp.folgt == null, "Noch ein Klick löst ihn wieder")
|
||
|
||
# Anlauf: von weit weg zu Fuß über die Straße bis in seine Wohnung
|
||
for m in welt().humans:
|
||
m.nav_fehl = 0 # frühere Tests haben Leute versetzt
|
||
var daheim := false
|
||
for i in range(3000):
|
||
schritte(1, 0.05, 2.0)
|
||
if oben.place == heim and oben._path.is_empty():
|
||
daheim = true
|
||
break
|
||
sp.global_position = Vector3(0, 0.3, 12.0) # im Park, weit weg
|
||
sp.velocity = Vector3.ZERO
|
||
sp.spruenge = 0
|
||
sp.anhaengen(oben)
|
||
await get_tree().physics_frame
|
||
await get_tree().physics_frame
|
||
p(daheim and sp._leine_pfad.size() >= 4,
|
||
"Von weit weg plant das Navigationsnetz den Weg bis in seine Wohnung (%d Punkte)" %
|
||
sp._leine_pfad.size())
|
||
var angekommen := false
|
||
for i in range(2400):
|
||
await get_tree().physics_frame
|
||
if sp.global_position.distance_to(oben.global_position) < 4.0:
|
||
angekommen = true
|
||
break
|
||
p(angekommen and sp.spruenge == 0,
|
||
"Und kommt zu Fuß bei ihm an, ohne dass die Leine nachziehen muss (%d Sprünge, Abstand %.1f)" %
|
||
[sp.spruenge, sp.global_position.distance_to(oben.global_position)])
|
||
|
||
# Schilder: nur wer markiert ist
|
||
schritte(2)
|
||
var fremde_schilder := 0
|
||
for m in welt().humans:
|
||
if m != oben and (m._label as Label3D).visible and not Game.markiert.has(str(m.data["id"])):
|
||
fremde_schilder += 1
|
||
p((oben._label as Label3D).visible and fremde_schilder == 0,
|
||
"Nur der Verfolgte trägt ein Schild, die anderen nicht (%d fremde)" % fremde_schilder)
|
||
sp.folgt = null
|
||
var ohne_netz := 0
|
||
for m in welt().humans:
|
||
ohne_netz += int(m.nav_fehl)
|
||
p(ohne_netz == 0, "Alle Wege der Menschen kamen vom Navigationsnetz (%d Rückfälle)" % ohne_netz)
|
||
Game.time_h = uhr
|
||
|
||
|
||
## Wie ein Spieler, der einfach losläuft: 90 Sekunden kreuz und quer durch die
|
||
## Stadt, bei Wand Richtung wechseln. Dabei darf er nie hängen bleiben, nie
|
||
## unter den Boden und nie über die Dächer geraten.
|
||
func test_zufallslauf() -> void:
|
||
abschnitt("Freies Herumlaufen: nie eingeklemmt, nie unter dem Boden")
|
||
if not main.auf_erde:
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
ui().schliessen()
|
||
var sp: Player = main.player
|
||
sp.set_mode(Player.Mode.GEIST)
|
||
sp.look_enabled = true
|
||
sp.folgt = null
|
||
sp.spruenge = 0
|
||
sp.global_position = Vector3(0, 0.3, 12.0)
|
||
sp.velocity = Vector3.ZERO
|
||
var rng := RandomNumberGenerator.new()
|
||
rng.seed = 4711
|
||
var richtungen := [["vor"], ["vor", "rechts"], ["rechts"], ["zurueck", "rechts"],
|
||
["zurueck"], ["zurueck", "links"], ["links"], ["vor", "links"]]
|
||
var kurs := 0
|
||
var letzte := sp.global_position
|
||
var tiefster := 0.0
|
||
var hoechster := 0.0
|
||
var still := 0
|
||
var laengste_still := 0
|
||
var gesamt := 0.0
|
||
for runde in range(150):
|
||
for t in richtungen[kurs]:
|
||
Input.action_press(t)
|
||
var gemeldet := false
|
||
for i in range(36):
|
||
await get_tree().physics_frame
|
||
if sp.global_position.y < -0.2 and not gemeldet:
|
||
gemeldet = true
|
||
var k := sp.get_last_slide_collision()
|
||
var txt := "-"
|
||
if k != null and k.get_collider() != null:
|
||
txt = str((k.get_collider() as Node).get_parent().global_position)
|
||
print(" [unter dem Boden] Runde %d, %s, Kollision bei %s" % [runde, str(sp.global_position), txt])
|
||
for t in richtungen[kurs]:
|
||
Input.action_release(t)
|
||
var bewegt := _xz(sp.global_position).distance_to(_xz(letzte))
|
||
if bewegt < 0.3 and still >= 2:
|
||
print(" [steht] Runde %d bei %s, Kurs %d" % [runde, str(sp.global_position), kurs])
|
||
gesamt += bewegt
|
||
if bewegt < 0.3:
|
||
still += 1
|
||
laengste_still = maxi(laengste_still, still)
|
||
kurs = (kurs + 3 + rng.randi_range(0, 2)) % 8 # umdrehen, leicht versetzt
|
||
else:
|
||
still = 0
|
||
if rng.randf() < 0.2:
|
||
kurs = rng.randi_range(0, 7)
|
||
letzte = sp.global_position
|
||
tiefster = minf(tiefster, sp.global_position.y)
|
||
hoechster = maxf(hoechster, sp.global_position.y)
|
||
p(gesamt > 200.0, "Er kommt herum (%.0f m in 90 s)" % gesamt)
|
||
p(laengste_still <= 3, "Nie länger als drei Züge ohne Bewegung (längste Pause %d)" % laengste_still)
|
||
p(tiefster > -0.5, "Nie unter dem Boden (tiefster y %.2f)" % tiefster)
|
||
p(hoechster < 6.0, "Nie über den Dächern (höchster y %.1f)" % hoechster)
|
||
p(sp.spruenge == 0, "Nie eingeklemmt -- die Stadt musste ihn nicht befreien (%d)" % sp.spruenge)
|
||
var schwebend := 0
|
||
for m in welt().humans:
|
||
if m._path.is_empty() and m.global_position.y > 0.4:
|
||
schwebend += 1
|
||
p(schwebend == 0, "Kein stehender Mensch schwebt über dem Boden (%d)" % schwebend)
|
||
|
||
|
||
## Bei hohem Tempo wird der Schritt je Bild größer als die Ankunftsschwelle.
|
||
## Wer dann über den Wegpunkt hinausläuft, pendelt um ihn herum statt anzukommen.
|
||
func test_gehen_bei_tempo() -> void:
|
||
abschnitt("Gehen bleibt auch im Schnelllauf ruhig")
|
||
ui().schliessen()
|
||
var mensch: Bewohner = welt().humans[0]
|
||
var stufe_kehrt: Dictionary = {}
|
||
for stufe in [1, 3]:
|
||
Game.tempo_setzen(stufe)
|
||
Game.time_h = 12.9
|
||
for i in range(400):
|
||
schritte(1, 1.0 / 60.0)
|
||
if not mensch._path.is_empty():
|
||
break
|
||
var kehrt := 0
|
||
var bewegt := 0
|
||
var vorher: Vector3 = mensch.global_position
|
||
var richtung := Vector3.ZERO
|
||
for i in range(600):
|
||
schritte(1, 1.0 / 60.0)
|
||
if mensch._path.is_empty():
|
||
continue
|
||
var d: Vector3 = mensch.global_position - vorher
|
||
d.y = 0.0
|
||
if d.length() > 0.001:
|
||
bewegt += 1
|
||
if richtung != Vector3.ZERO and d.normalized().dot(richtung) < -0.5:
|
||
kehrt += 1
|
||
richtung = d.normalized()
|
||
vorher = mensch.global_position
|
||
stufe_kehrt[stufe] = kehrt
|
||
var weite := Bewohner.GEHTEMPO * (1.0 / 60.0) * Game.faktor_von(stufe)
|
||
p(kehrt <= 4, "Tempo %d (%d×, %.2f je Bild): %d Kehrtwenden in %d Bildern" %
|
||
[stufe, int(Game.faktor_von(stufe)), weite, kehrt, bewegt])
|
||
p(int(stufe_kehrt[3]) <= int(stufe_kehrt[1]) + 4,
|
||
"Schnelllauf zappelt nicht mehr als Normaltempo (%d gegen %d)" %
|
||
[stufe_kehrt[3], stufe_kehrt[1]])
|
||
Game.tempo_setzen(1)
|
||
|
||
|
||
## Sich an einen Menschen hängen: der Geist begleitet ihn durch seinen Tag,
|
||
## statt ihm hinterherzulaufen. Damit bleibt er die ganze Zeit beobachtbar.
|
||
func test_verfolgung() -> void:
|
||
abschnitt("Als Geist einen Menschen begleiten")
|
||
p(InputMap.has_action("folgen") and not InputMap.action_get_events("folgen").is_empty(),
|
||
"Eigene Taste fürs Folgen")
|
||
ui().schliessen()
|
||
main.player.set_mode(Player.Mode.GEIST)
|
||
main.player.look_enabled = true
|
||
main.player.yaw = 0.0
|
||
var mensch: Bewohner = welt().humans[0]
|
||
Game.active_mission = str(mensch.data["id"])
|
||
# kurz vor seinem nächsten Ortswechsel einsteigen, damit er auch losgeht
|
||
var routine: Array = mensch.data["routine"]
|
||
for k in range(routine.size()):
|
||
if str(routine[k][1]) != mensch.place and float(routine[k][0]) > 0.5:
|
||
Game.time_h = float(routine[k][0]) - 0.4
|
||
break
|
||
schritte(5)
|
||
main.player.global_position = mensch.global_position + Vector3(0, 0.3, 0)
|
||
main.player.velocity = Vector3.ZERO
|
||
main._process(0.05)
|
||
p(main._fokus_mensch == mensch, "Der Mensch ist im Fokus")
|
||
|
||
main._verfolgen()
|
||
p(main.player.folgt == mensch, "F hängt den Geist an ihn")
|
||
|
||
# Er lebt seinen Tag weiter, der Geist bleibt dran
|
||
var weiteste := 0.0
|
||
var gelaufen := false
|
||
var im_fokus := 0
|
||
for i in range(2400):
|
||
await get_tree().physics_frame # nur die Engine steppt: sonst laufen die Menschen doppelt
|
||
if not mensch._path.is_empty():
|
||
gelaufen = true
|
||
if main._fokus_mensch == mensch:
|
||
im_fokus += 1
|
||
weiteste = maxf(weiteste,
|
||
main.player.global_position.distance_to(mensch.global_position))
|
||
p(gelaufen, "Er wechselt in dieser Zeit den Ort")
|
||
p(weiteste < 8.0, "Der Geist bleibt die ganze Zeit dicht dabei (höchstens %.1f)" % weiteste)
|
||
p(im_fokus > 2000, "Und behält ihn im Fokus (%d von 2400 Schritten)" % im_fokus)
|
||
p(main._fokus_mensch == mensch or main.player.folgt == mensch,
|
||
"Unterwegs bleibt er notierbar")
|
||
|
||
# Eigene Bewegung löst die Verfolgung
|
||
Input.action_press("vor")
|
||
for i in range(10):
|
||
await get_tree().physics_frame
|
||
Input.action_release("vor")
|
||
p(main.player.folgt == null, "Wer selbst losgeht, lässt ihn los")
|
||
|
||
# Die Taste löst ebenfalls
|
||
main.player.global_position = mensch.global_position + Vector3(0, 0.3, 0)
|
||
main.player.velocity = Vector3.ZERO
|
||
main._process(0.05)
|
||
main._verfolgen()
|
||
p(main.player.folgt == mensch, "Wieder angehängt")
|
||
main._verfolgen()
|
||
p(main.player.folgt == null, "F lässt wieder los")
|
||
|
||
# In der Traumwelt gibt es niemanden zu verfolgen
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
main._verfolgen()
|
||
p(main.player.folgt == null, "In der Traumwelt hängt sich der Läufer an niemanden")
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
|
||
|
||
## Woran der Traum scheitert, steht nirgends. Man findet es, indem man
|
||
## dabeisteht -- awareness training, bevor irgendetwas ersetzt wird.
|
||
func test_diagnose() -> void:
|
||
abschnitt("Diagnose — die Ursache zeigt sich im Tag")
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.systeme.clear()
|
||
Game.erkannt_slots.clear()
|
||
Game.notiz_sperre = 0.0
|
||
|
||
var urs: Array = Game.ursachen_von(id)
|
||
p(urs.size() >= 2 and urs.size() <= 3,
|
||
"%s scheitert an %d Ursachen: %s" % [HumanDB.by_id(id)["name"], urs.size(), str(urs)])
|
||
var alle_bekannt := true
|
||
for u in urs:
|
||
if not Ursachen.LISTE.has(u):
|
||
alle_bekannt = false
|
||
p(alle_bekannt, "Alle davon stehen im Katalog")
|
||
|
||
# Zu jeder Ursache eine Tätigkeit im Tag
|
||
var luecken: Array = []
|
||
for u in urs:
|
||
var da := false
|
||
for nr in range(Game.slots(id).size()):
|
||
if Ursachen.ursache_von(Game.slot_grund(id, nr)) == str(u):
|
||
da = true
|
||
if not da:
|
||
luecken.append(str(u))
|
||
p(luecken.is_empty(), "Jede Ursache zeigt sich in einer Tätigkeit%s" %
|
||
("" if luecken.is_empty() else " — fehlt: " + ", ".join(luecken)))
|
||
|
||
# Ein Blocker-Slot ist zunächst verdeckt, ein neutraler nicht
|
||
var blocker_nr := -1
|
||
var neutral_nr := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
var g := Game.slot_grund(id, nr)
|
||
if Ursachen.ist_blocker(g) and blocker_nr < 0:
|
||
blocker_nr = nr
|
||
elif g != "schlaeft" and not Ursachen.ist_blocker(g) and neutral_nr < 0:
|
||
neutral_nr = nr
|
||
p(blocker_nr >= 0 and neutral_nr >= 0, "Ihr Tag hat Blocker und freie Slots")
|
||
p(not Game.erkannt(id, blocker_nr), "Was im Blocker-Slot läuft, ist verdeckt")
|
||
p(Game.erkannt(id, neutral_nr), "Ein freier Slot ist offen sichtbar")
|
||
|
||
var ursache := Ursachen.ursache_von(Game.slot_grund(id, blocker_nr))
|
||
p(not Game.ursache_bekannt(id, ursache), "Und die Ursache ist noch unbekannt")
|
||
|
||
# Solange man nicht lange genug dabeisteht, passiert nichts
|
||
# Zusehen allein deckt nichts auf -- der Spieler muss den Moment benennen
|
||
for i in range(60):
|
||
schritte(1)
|
||
p(not Game.erkannt(id, blocker_nr), "Bloßes Dabeistehen deckt nichts auf")
|
||
p(Game.notieren(id, neutral_nr) == "daneben",
|
||
"Im freien Slot geht die Notiz daneben")
|
||
p(Game.notiz_sperre > 0.0, "Ein Fehlgriff sperrt kurz (%.0f s)" % Game.notiz_sperre)
|
||
p(Game.notieren(id, blocker_nr) == "gesperrt", "Solange nimmt er keine Notiz an")
|
||
Game.notiz_sperre = 0.0
|
||
p(Game.notieren(id, blocker_nr) == "treffer", "Im Blocker-Slot sitzt sie")
|
||
p(Game.erkannt(id, blocker_nr), "Und deckt auf, was dort läuft")
|
||
p(is_equal_approx(Game.notiz_sperre, 0.0), "Ein Treffer sperrt nicht")
|
||
p(Game.notieren(id, blocker_nr) == "bekannt", "Zweimal notieren bringt nichts Neues")
|
||
p(Game.ursache_bekannt(id, ursache),
|
||
"Und damit die Ursache: %s" % Ursachen.LISTE[ursache]["name"])
|
||
|
||
# Ohne Diagnose lässt sich nichts einrichten
|
||
Game.known = {"pomodoro": 0, "zerlegen": 0, "box_atmung": 0, "nein_sagen": 0,
|
||
"habit_stack": 0, "reframing": 0}
|
||
var zweiter := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
if Ursachen.ist_blocker(Game.slot_grund(id, nr)) and nr != blocker_nr:
|
||
zweiter = nr
|
||
if zweiter >= 0:
|
||
var pr := Game.system_pruefen(id, zweiter, "pomodoro")
|
||
p(not pr["geht"] and str(pr["grund"]).contains("gesehen"),
|
||
"In einen unerkannten Slot lässt sich nichts bauen")
|
||
|
||
|
||
## Gegen eine Ursache baut man ein System. Passt die Belohnung, hält es besser.
|
||
## Die zweite Ebene: Probleme hängen nicht an Stunden, sondern an Momenten.
|
||
## Gewohnheiten laufen ohne dich weiter -- hier musst du dabei sein.
|
||
func test_probleme_und_momente() -> void:
|
||
abschnitt("Probleme zeigen sich in Momenten")
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.situation = {}
|
||
Game.situation_pause = 0.0
|
||
Game.probleme_bekannt.clear()
|
||
Game.problem_stand.clear()
|
||
Game.ueberwunden.clear()
|
||
Game.cooldown.clear()
|
||
ui().schliessen()
|
||
|
||
var prob: Array = Game.probleme_von(id)
|
||
p(prob.size() >= 1 and prob.size() <= 2,
|
||
"%s trägt %d Problem(e): %s" % [HumanDB.by_id(id)["name"], prob.size(), str(prob)])
|
||
var alle_bekannt := true
|
||
for x in prob:
|
||
if not Probleme.problem_da(str(x)):
|
||
alle_bekannt = false
|
||
p(alle_bekannt, "Alle stehen im Katalog")
|
||
var ohne_situation: Array = []
|
||
for x in Probleme.LISTE:
|
||
if Probleme.situationen_von(str(x)).is_empty():
|
||
ohne_situation.append(str(x))
|
||
p(ohne_situation.is_empty(), "Jedes Problem zeigt sich in Situationen%s" %
|
||
("" if ohne_situation.is_empty() else " — außer " + ", ".join(ohne_situation)))
|
||
var ohne_technik: Array = []
|
||
for x in Probleme.LISTE:
|
||
if Probleme.techniken_gegen(str(x)).is_empty():
|
||
ohne_technik.append(str(x))
|
||
p(ohne_technik.is_empty(), "Und gegen jedes gibt es Techniken%s" %
|
||
("" if ohne_technik.is_empty() else " — außer " + ", ".join(ohne_technik)))
|
||
|
||
# Sie kommt von allein, aber nicht sofort
|
||
p(not Game.situation_laeuft(), "Erst läuft kein Moment")
|
||
Game.situationen_ticken(1.0, false)
|
||
p(not Game.situation_laeuft(), "Bei einem Schlafenden kommt keiner")
|
||
Game.situationen_ticken(1.0, true)
|
||
p(Game.situation_laeuft(), "Bei einem wachen Menschen bricht einer auf")
|
||
var problem := Game.situation_problem()
|
||
p(prob.has(problem), "Und gehört zu einem seiner Probleme: %s" % problem)
|
||
|
||
# Solange sie läuft, steht der Traum still
|
||
Game.progress[id] = 10.0
|
||
Game.traum_voranbringen(6.0)
|
||
p(is_equal_approx(Game.mission_progress(id), 10.0),
|
||
"Im Moment kommt der Traum nicht voran (%.1f %%)" % Game.mission_progress(id))
|
||
|
||
# Dabeisein zeigt, was dahintersteckt
|
||
p(not Game.problem_bekannt(id, problem), "Was dahintersteckt, ist noch unbekannt")
|
||
Game.situation_sehen()
|
||
p(Game.problem_bekannt(id, problem), "Dabeisein zeigt es: %s" %
|
||
Probleme.name_von(problem))
|
||
|
||
# Eine unpassende Technik greift nicht
|
||
var passend := ""
|
||
var unpassend := ""
|
||
for tid in TechDB.ids():
|
||
if Probleme.hilft(tid, problem) and passend == "":
|
||
passend = tid
|
||
elif not Probleme.hilft(tid, problem) and unpassend == "":
|
||
unpassend = tid
|
||
Game.known[passend] = 0
|
||
Game.known[unpassend] = 0
|
||
var res_falsch := Game.situation_anwenden(unpassend)
|
||
p(res_falsch["ok"] and not res_falsch["treffer"],
|
||
"%s greift bei %s nicht" % [TechDB.by_id(unpassend)["name"],
|
||
Probleme.name_von(problem)])
|
||
p(Game.situation_laeuft(), "Der Moment läuft weiter")
|
||
|
||
# Die passende beendet ihn
|
||
var vorher: float = Game.mission_progress(id)
|
||
var res := Game.situation_anwenden(passend)
|
||
p(res["treffer"], "%s trägt hindurch" % TechDB.by_id(passend)["name"])
|
||
p(not Game.situation_laeuft(), "Der Moment ist vorbei")
|
||
p(Game.mission_progress(id) > vorher, "Und bringt den Traum voran (%.0f → %.0f %%)" %
|
||
[vorher, Game.mission_progress(id)])
|
||
p(Game.problem_fortschritt(id, problem) == 1, "Einmal geschafft")
|
||
|
||
# Oft genug, und das Problem kommt nicht wieder
|
||
for i in range(Game.PROBLEM_UEBERWINDEN):
|
||
Game.situation = {"mensch": id, "key": Probleme.situationen_von(problem)[0],
|
||
"problem": problem, "rest": Game.SITUATION_DAUER}
|
||
Game.cooldown.clear()
|
||
Game.situation_anwenden(passend)
|
||
p(Game.problem_ueberwunden(id, problem),
|
||
"Nach %d Malen ist %s überwunden" % [Game.PROBLEM_UEBERWINDEN,
|
||
Probleme.name_von(problem)])
|
||
Game.situation = {}
|
||
Game.situation_pause = 0.0
|
||
var kommt_wieder := false
|
||
for i in range(200):
|
||
Game.situationen_ticken(1.0, true)
|
||
if Game.situation_laeuft() and Game.situation_problem() == problem:
|
||
kommt_wieder = true
|
||
break
|
||
p(not kommt_wieder, "Dieser Moment kommt nicht wieder")
|
||
|
||
# Beide Ebenen stehen nebeneinander im HUD
|
||
Game.situation = {}
|
||
ui()._hud_aktualisieren()
|
||
p(ui()._ablauf.visible and ui()._probleme.visible,
|
||
"Tagesablauf und Momente stehen beide im HUD")
|
||
p(ui()._probleme.text.contains(Probleme.name_von(problem)),
|
||
"Die Momente nennen das Problem: %s" % ui()._probleme.text.replace("\n", " / "))
|
||
Game.situation = {"mensch": id, "key": Probleme.situationen_von(problem)[0],
|
||
"problem": problem, "rest": 9.0}
|
||
ui()._hud_aktualisieren()
|
||
p(ui()._probleme.text.contains("Jetzt:"), "Der laufende Moment steht oben")
|
||
|
||
# Und er kündigt sich am Menschen selbst an, nicht nur im HUD
|
||
var wer: Bewohner = null
|
||
for mm in welt().humans:
|
||
if mm.data["id"] == id:
|
||
wer = mm
|
||
p(wer.beschriftung().contains(str(Probleme.SITUATIONEN[Game.situation["key"]]["name"])),
|
||
"Am Menschen steht, was gerade aufbricht: %s" %
|
||
wer.beschriftung().replace("\n", " / "))
|
||
Game.situation = {}
|
||
p(not wer.beschriftung().contains("»"), "Ohne Moment steht dort nichts davon")
|
||
# E führt in den Moment, sonst in den Tagesablauf -- die zwei Ebenen
|
||
# teilen sich eine Taste, ohne sich in die Quere zu kommen
|
||
var mensch: Bewohner = null
|
||
for mm in welt().humans:
|
||
if mm.data["id"] == id:
|
||
mensch = mm
|
||
main.player.set_mode(Player.Mode.GEIST)
|
||
main.player.folgt = null
|
||
main.player.global_position = mensch.global_position + Vector3(0, 1.5, 1.5)
|
||
Game.situation = {}
|
||
main._process(0.05)
|
||
main._interagieren()
|
||
p(ui()._kind == "anwenden", "Ohne Moment führt E in den Tagesablauf (%s)" % ui()._kind)
|
||
ui().schliessen()
|
||
Game.situation = {"mensch": id, "key": Probleme.situationen_von(problem)[0],
|
||
"problem": problem, "rest": 12.0}
|
||
main._process(0.05)
|
||
main._interagieren()
|
||
p(ui()._kind == "situation", "Im Moment führt dieselbe Taste dorthin (%s)" % ui()._kind)
|
||
p(_panel_text().contains(str(Probleme.SITUATIONEN[Game.situation["key"]]["was"])),
|
||
"Das Panel zeigt, was gerade passiert")
|
||
ui().schliessen()
|
||
|
||
# Die Gewohnheitsebene läuft davon unberührt weiter
|
||
Game.situation = {}
|
||
Game.progress[id] = 0.0
|
||
Game.traum_voranbringen(24.0)
|
||
p(Game.mission_progress(id) > 0.0,
|
||
"Ohne Moment liefert der Tagesablauf wieder sein Tempo (%.0f %%)" %
|
||
Game.mission_progress(id))
|
||
|
||
Game.situation = {}
|
||
Game.ueberwunden.clear()
|
||
Game.probleme_bekannt.clear()
|
||
Game.problem_stand.clear()
|
||
|
||
|
||
## Eigene Menschen: aus Wörtern wird eine Psyche, aus dem Beruf ein Alltag,
|
||
## und beides lässt sich am Brett schreiben, speichern und wieder abnehmen.
|
||
func test_eigene_menschen() -> void:
|
||
abschnitt("Eigene Menschen aus Wörtern")
|
||
Aushaenge.PFAD = "user://aushaenge_test.cfg"
|
||
Aushaenge.alle_loeschen()
|
||
|
||
# --- Der Content ist gekoppelt: nichts ist unlösbar
|
||
var ohne_technik: Array = []
|
||
for b in Ursachen.BLOCKER:
|
||
if TechDB.fuer_ursache(Ursachen.ursache_von(str(b))).is_empty():
|
||
ohne_technik.append(str(b))
|
||
p(ohne_technik.is_empty(), "Jeder der %d Blocker hat Techniken gegen seine Ursache%s" %
|
||
[Ursachen.BLOCKER.size(), "" if ohne_technik.is_empty() else " — außer " + ", ".join(ohne_technik)])
|
||
var ohne: Array = []
|
||
for pr in Probleme.LISTE:
|
||
if Probleme.techniken_gegen(str(pr)).is_empty() or Probleme.situationen_von(str(pr)).is_empty():
|
||
ohne.append(str(pr))
|
||
p(ohne.is_empty(), "Jede der %d Klemmen hat Techniken und Situationen%s" %
|
||
[Probleme.LISTE.size(), "" if ohne.is_empty() else " — außer " + ", ".join(ohne)])
|
||
var tage: Dictionary = {}
|
||
for tid in TechDB.ids():
|
||
var t := TechDB.by_id(tid)
|
||
if str(t["was"]) == "" or str(t["wann"]) == "" or str(t["warum"]) == "" or str(t["woran"]) == "":
|
||
tage[tid] = true
|
||
p(tage.is_empty(), "Alle %d Techniken tragen Was, Wann, Warum, Woran" % TechDB.LIST.size())
|
||
|
||
# --- Die Wörter erreichen jeden Eintrag des Modells
|
||
var erreicht := {"ursachen": {}, "blocker": {}, "probleme": {}, "verhalten": {}}
|
||
var leer: Array = []
|
||
for w in Woerter.ids():
|
||
var e: Dictionary = Woerter.LISTE[w]
|
||
var wirkt := false
|
||
for u in e.get("ursachen", {}):
|
||
erreicht["ursachen"][u] = true
|
||
wirkt = true
|
||
for b in e.get("blocker", []):
|
||
erreicht["blocker"][b] = true
|
||
wirkt = true
|
||
for pr in e.get("probleme", {}):
|
||
erreicht["probleme"][pr] = true
|
||
wirkt = true
|
||
for v in e.get("verhalten", {}):
|
||
erreicht["verhalten"][v] = true
|
||
wirkt = true
|
||
if not wirkt:
|
||
leer.append(w)
|
||
p(Woerter.ids().size() >= 50, "%d Wörter stehen zur Wahl" % Woerter.ids().size())
|
||
p(leer.is_empty(), "Jedes Wort wirkt irgendwo%s" %
|
||
("" if leer.is_empty() else " — außer " + ", ".join(leer)))
|
||
p(erreicht["ursachen"].size() == Ursachen.LISTE.size(),
|
||
"Die Wörter erreichen alle %d Ursachen" % Ursachen.LISTE.size())
|
||
p(erreicht["blocker"].size() == Ursachen.BLOCKER.size(),
|
||
"Alle %d Blocker (%d)" % [Ursachen.BLOCKER.size(), erreicht["blocker"].size()])
|
||
p(erreicht["probleme"].size() == Probleme.LISTE.size(),
|
||
"Alle %d Klemmen" % Probleme.LISTE.size())
|
||
p(erreicht["verhalten"].size() == Woerter.VERHALTEN.size(),
|
||
"Und alle %d Verhaltenswerte" % Woerter.VERHALTEN.size())
|
||
var unbekannt: Array = []
|
||
for w in Woerter.ids():
|
||
var e: Dictionary = Woerter.LISTE[w]
|
||
for b in e.get("blocker", []):
|
||
if not Ursachen.BLOCKER.has(b):
|
||
unbekannt.append("%s->%s" % [w, b])
|
||
for pr in e.get("probleme", {}):
|
||
if not Probleme.LISTE.has(pr):
|
||
unbekannt.append("%s->%s" % [w, pr])
|
||
for u in e.get("ursachen", {}):
|
||
if not Ursachen.LISTE.has(u):
|
||
unbekannt.append("%s->%s" % [w, u])
|
||
p(unbekannt.is_empty(), "Kein Wort zeigt ins Leere%s" %
|
||
("" if unbekannt.is_empty() else ": " + ", ".join(unbekannt)))
|
||
|
||
# --- Aus Wörtern wird ein Bild
|
||
var bild := Woerter.profil(["aufschiebend", "perfektionistisch", "stur", "aengstlich",
|
||
"nachtaktiv", "zurueckgezogen"])
|
||
p(bild["ursachen"].size() >= 2 and bild["ursachen"].size() <= 3,
|
||
"Zwei bis drei Ursachen: %s" % str(bild["ursachen"]))
|
||
p(bild["ursachen"].has("anfang"), "Aufschiebend und perfektionistisch ergeben »kein Anfang«")
|
||
p(bild["probleme"].size() >= 1 and bild["probleme"].size() <= 2,
|
||
"Ein bis zwei Klemmen: %s" % str(bild["probleme"]))
|
||
p(bild["probleme"].has("angst") or bild["probleme"].has("perfektionismus"),
|
||
"Ängstlich und perfektionistisch schlagen durch")
|
||
p(bild["blocker"].size() >= 2, "Blocker zu den Ursachen: %s" % str(bild["blocker"]))
|
||
var fremd := false
|
||
for b in bild["blocker"]:
|
||
if not bild["ursachen"].has(Ursachen.ursache_von(str(b))):
|
||
fremd = true
|
||
p(not fremd, "Kein Blocker ohne passende Ursache")
|
||
p(int(bild["verhalten"]["abwehr"]) >= 2, "Stur und perfektionistisch: Abwehr %d" %
|
||
int(bild["verhalten"]["abwehr"]))
|
||
p(int(bild["verhalten"]["sichtbar"]) < 0, "Zurückgezogen: schwer zu durchschauen (%d)" %
|
||
int(bild["verhalten"]["sichtbar"]))
|
||
var gegen := Woerter.profil(["ordentlich", "chaotisch", "geduldig", "ungeduldig",
|
||
"gelassen", "reizbar"])
|
||
p(int(gegen["verhalten"]["guete"]) == 0 and int(gegen["verhalten"]["momente"]) == 0,
|
||
"Widersprüche heben sich auf (Güte %d, Momente %d)" %
|
||
[int(gegen["verhalten"]["guete"]), int(gegen["verhalten"]["momente"])])
|
||
var wenig := Woerter.profil(["heiter"])
|
||
p(wenig["ursachen"].size() >= 2 and wenig["probleme"].size() >= 1,
|
||
"Auch ein Wort ergibt einen ganzen Menschen (%s / %s)" %
|
||
[str(wenig["ursachen"]), str(wenig["probleme"])])
|
||
|
||
# --- Ein Mensch aus einem Aushang, mit eigenem Alltag
|
||
var pr := {"name": "Mo Test", "alter": 33, "beruf": "Bäcker",
|
||
"traum": "Wieder Gitarre spielen.", "geschichte": "Nach der Schicht ist nichts übrig.",
|
||
"woerter": ["erschoepft", "bequem", "konfliktscheu", "gruendlich", "stur", "aengstlich"]}
|
||
p(Aushaenge.pruefen(pr).is_empty(), "Der Aushang ist vollständig")
|
||
p(not Aushaenge.pruefen({"name": "X", "woerter": ["stur"]}).is_empty(),
|
||
"Ein lückenhafter wird abgewiesen: %s" %
|
||
", ".join(Aushaenge.pruefen({"name": "X", "woerter": ["stur"]})))
|
||
var id := Aushaenge.speichern(pr)
|
||
p(id.begins_with("eigen_"), "Gespeichert als %s" % id)
|
||
p(Aushaenge.laden().size() == 1 and Aushaenge.laden()[0]["name"] == "Mo Test",
|
||
"Und aus der Datei zurückgelesen")
|
||
Game.completed.clear()
|
||
Game.welt_erzeugen()
|
||
var m := HumanDB.by_id(id)
|
||
p(not m.is_empty(), "Er lebt in der Stadt")
|
||
p(HumanDB.LIST.size() == HumanDB.EINWOHNER, "Die Stadt bleibt bei %d Einwohnern" % HumanDB.EINWOHNER)
|
||
p(HumanDB.PLACES.has(str(m["heim"])), "Er hat ein Zuhause: %s" % m["heim"])
|
||
var frueh := false
|
||
for e in m["routine"]:
|
||
if int(e[0]) == 4 and str(e[1]) == "baeckerei":
|
||
frueh = true
|
||
p(frueh, "Als Bäcker steht er um vier in der Backstube")
|
||
var luecken := 0
|
||
for stunde in range(24):
|
||
if Game.slot_bei_stunde(id, stunde) < 0:
|
||
luecken += 1
|
||
p(luecken == 0, "Sein Tag ist lückenlos")
|
||
var blocker_im_tag: Array = []
|
||
for e in m["routine"]:
|
||
if Ursachen.ist_blocker(str(e[2])):
|
||
blocker_im_tag.append(str(e[2]))
|
||
p(blocker_im_tag.size() >= 2 and blocker_im_tag.size() <= HumanDB.BLOCKER_MAX,
|
||
"Zwei bis %d Blocker stehen im Tag: %s" % [HumanDB.BLOCKER_MAX, ", ".join(blocker_im_tag)])
|
||
var passt_alle := true
|
||
for b in blocker_im_tag:
|
||
if not (m["ursachen"] as Array).has(Ursachen.ursache_von(b)):
|
||
passt_alle = false
|
||
p(passt_alle, "Jeder davon gehört zu einer seiner Ursachen")
|
||
var ohne_blocker: Array = []
|
||
for u in m["ursachen"]:
|
||
var da := false
|
||
for b in blocker_im_tag:
|
||
if Ursachen.ursache_von(b) == str(u):
|
||
da = true
|
||
if not da:
|
||
ohne_blocker.append(str(u))
|
||
p(ohne_blocker.is_empty(), "Und jede seiner Ursachen zeigt sich im Tag%s" %
|
||
("" if ohne_blocker.is_empty() else " — nicht: " + ", ".join(ohne_blocker)))
|
||
# Das gilt für jeden Menschen der Stadt, sonst bremst eine Ursache für immer
|
||
var stumm: Array = []
|
||
for hh in HumanDB.LIST:
|
||
for u in hh["ursachen"]:
|
||
var zeigt := false
|
||
for e in hh["routine"]:
|
||
if Ursachen.ursache_von(str(e[2])) == str(u):
|
||
zeigt = true
|
||
if not zeigt:
|
||
stumm.append("%s/%s" % [hh["name"], u])
|
||
p(stumm.is_empty(), "Bei allen %d Menschen zeigt sich jede Ursache im Tag%s" %
|
||
[HumanDB.LIST.size(), "" if stumm.is_empty() else " — nicht: " + ", ".join(stumm)])
|
||
p(Game.machbar(id) or true, "Bereit fürs Brett")
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
_wege_pruefen("Stadt mit eigenem Menschen", HumanDB.PLACES, HumanDB.LIST, main.erde.nav_map)
|
||
|
||
# --- Die Wörter verändern, wie er sich verhält
|
||
p(Game.verhalten(id, "abwehr") >= 1, "Stur: Abwehr %d" % Game.verhalten(id, "abwehr"))
|
||
var std_id := "mensch_0"
|
||
p(Game.verhalten(std_id, "abwehr") == 0, "Ein erzeugter Mensch steht auf null")
|
||
Game.situation = {}
|
||
var pause_normal := Game._pause_fuer(std_id)
|
||
var reiz := Woerter.profil(["reizbar", "aengstlich", "rastlos", "spontan", "ehrgeizig", "stur"])
|
||
var pr2 := {"name": "Ria Test", "alter": 28, "beruf": "Studentin", "traum": "T",
|
||
"geschichte": "G", "woerter": ["reizbar", "aengstlich", "rastlos", "spontan", "ehrgeizig", "stur"]}
|
||
var id2 := Aushaenge.speichern(pr2)
|
||
Game.welt_erzeugen()
|
||
p(int(reiz["verhalten"]["momente"]) >= 2, "Reizbar, ängstlich, rastlos: Momente %d" %
|
||
int(reiz["verhalten"]["momente"]))
|
||
p(Game._pause_fuer(id2) < pause_normal,
|
||
"Bei ihr kommt der nächste Moment schneller (%.0f s statt %.0f s)" %
|
||
[Game._pause_fuer(id2), pause_normal])
|
||
var tempo_std := Game.traum_tempo(std_id)
|
||
var pr3 := {"name": "Zed Test", "alter": 40, "beruf": "Lehrer", "traum": "T",
|
||
"geschichte": "G", "woerter": ["diszipliniert", "zielstrebig", "optimistisch",
|
||
"sportlich", "verbissen", "ehrgeizig"]}
|
||
var id3 := Aushaenge.speichern(pr3)
|
||
Game.welt_erzeugen()
|
||
Game.systeme.clear()
|
||
p(Game.traum_tempo(id3) > Game.traum_tempo(id) or Game.verhalten(id3, "tempo") > Game.verhalten(id, "tempo"),
|
||
"Zielstrebig treibt den Traum von sich aus (Tempo %d gegen %d)" %
|
||
[Game.verhalten(id3, "tempo"), Game.verhalten(id, "tempo")])
|
||
p(HumanDB.LIST.size() == HumanDB.EINWOHNER, "Drei eigene, immer noch %d Einwohner" % HumanDB.LIST.size())
|
||
|
||
# --- Am Brett: schreiben, Zufall, speichern, abnehmen
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
ui().brett_panel()
|
||
p(_knopf_da("Aushang schreiben"), "Das Brett bietet an, einen Aushang zu schreiben")
|
||
p(_knopf_texte().contains("Mo Test"), "Der eigene Mensch hängt aus")
|
||
ui().schreiben_panel()
|
||
p(ui()._kind == "schreiben", "Das Formular öffnet sich")
|
||
var haken := 0
|
||
for c in _alle_kinder(ui()._content):
|
||
if c is CheckBox:
|
||
haken += 1
|
||
p(haken == Woerter.ids().size(), "Alle %d Wörter sind ankreuzbar (%d)" % [Woerter.ids().size(), haken])
|
||
ui().schreiben_panel(HumanDB.zufallsprofil())
|
||
p(str(ui()._entwurf["name"]) != "" and (ui()._entwurf["woerter"] as Array).size() >= Woerter.MINDESTENS,
|
||
"Zufall füllt alles: %s mit %d Wörtern" % [ui()._entwurf["name"],
|
||
(ui()._entwurf["woerter"] as Array).size()])
|
||
var vorher := Aushaenge.laden().size()
|
||
ui()._aushang_speichern()
|
||
p(Aushaenge.laden().size() == vorher + 1, "Speichern hängt ihn ans Brett (%d)" % Aushaenge.laden().size())
|
||
p(ui()._kind == "aushang", "Und zeigt den Aushang")
|
||
p(_knopf_da("Aushang abnehmen"), "Ein eigener lässt sich abnehmen")
|
||
_druecke("Aushang abnehmen")
|
||
p(Aushaenge.laden().size() == vorher, "Abgenommen (%d)" % Aushaenge.laden().size())
|
||
ui().schliessen()
|
||
|
||
Aushaenge.alle_loeschen()
|
||
Aushaenge.PFAD = "user://aushaenge.cfg"
|
||
Game.welt_erzeugen()
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
|
||
|
||
func _alle_kinder(n: Node) -> Array:
|
||
var out: Array = []
|
||
for c in n.get_children():
|
||
out.append(c)
|
||
out.append_array(_alle_kinder(c))
|
||
return out
|
||
|
||
|
||
## Der Ablauf steht auch im HUD, ohne dass man ein Panel öffnen muss.
|
||
func test_ablauf_im_hud() -> void:
|
||
abschnitt("Der Ablauf steht im HUD")
|
||
var id := "mensch_0"
|
||
Game.gesehen.clear()
|
||
Game.erkannt_slots.clear()
|
||
Game.systeme.clear()
|
||
Game.active_mission = ""
|
||
ui().schliessen()
|
||
ui()._hud_aktualisieren()
|
||
p(not ui()._ablauf.visible, "Ohne Mission zeigt das HUD keinen Ablauf")
|
||
|
||
Game.active_mission = id
|
||
Game.time_h = 9.5
|
||
ui()._hud_aktualisieren()
|
||
p(ui()._ablauf.visible, "Mit Mission schon")
|
||
p(ui()._zellen.size() == 24, "Ein Feld je Stunde (%d)" % ui()._zellen.size())
|
||
p(ui()._ablauf_titel.text.contains("0 – 23"), "Beschriftet von 0 bis 23 Uhr")
|
||
p(ui()._ablauf_titel.text.contains("24 Stunden unbekannt"),
|
||
"Am Anfang ist alles unbekannt: %s" % ui()._ablauf_titel.text)
|
||
|
||
var unbekannt: Color = ui()._ablauf_farbe(id, Game.slot_bei_stunde(id, 9))
|
||
var nr := Game.slot_bei_stunde(id, 9)
|
||
Game.sehen(id, nr)
|
||
ui()._hud_aktualisieren()
|
||
p(ui()._ablauf_farbe(id, nr) != unbekannt, "Eine gesehene Stunde färbt sich um")
|
||
p(not ui()._ablauf_titel.text.contains("24 Stunden"),
|
||
"Und der Zähler geht runter: %s" % ui()._ablauf_titel.text)
|
||
p(ui()._ablauf_jetzt.text.begins_with("09 Uhr"),
|
||
"Darunter steht die laufende Stunde: %s" % ui()._ablauf_jetzt.text)
|
||
|
||
# Die laufende Stunde ist hervorgehoben
|
||
var jetzt: ColorRect = ui()._zellen[9]
|
||
var andere: ColorRect = ui()._zellen[8]
|
||
p(jetzt.custom_minimum_size.y > andere.custom_minimum_size.y,
|
||
"Das Feld der laufenden Stunde steht heraus")
|
||
|
||
# Ein benannter Blocker sieht anders aus als eine harmlose Stunde
|
||
var blocker_nr := -1
|
||
var frei_nr := -1
|
||
for n in range(Game.slots(id).size()):
|
||
var g := Game.slot_grund(id, n)
|
||
if Ursachen.ist_blocker(g) and blocker_nr < 0:
|
||
blocker_nr = n
|
||
elif g != "schlaeft" and not Ursachen.ist_blocker(g) and frei_nr < 0:
|
||
frei_nr = n
|
||
Game.sehen(id, blocker_nr)
|
||
Game.sehen(id, frei_nr)
|
||
var vor_notiz: Color = ui()._ablauf_farbe(id, blocker_nr)
|
||
Game.notieren(id, blocker_nr)
|
||
p(ui()._ablauf_farbe(id, blocker_nr) != vor_notiz,
|
||
"Notiert wechselt der Blocker noch einmal die Farbe")
|
||
p(ui()._ablauf_farbe(id, blocker_nr) != ui()._ablauf_farbe(id, frei_nr),
|
||
"Und hebt sich von einer harmlosen Stunde ab")
|
||
Game.notiz_sperre = 0.0
|
||
|
||
|
||
## Der Ablauf 0-23 Uhr: am Anfang leer, füllt sich durchs Dabeisein.
|
||
func test_tagesablauf_anzeige() -> void:
|
||
abschnitt("Der Tag steht als Ablauf von 0 bis 23 Uhr")
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.gesehen.clear()
|
||
Game.erkannt_slots.clear()
|
||
Game.systeme.clear()
|
||
Game.notiz_sperre = 0.0
|
||
|
||
# Jede Stunde des Tages hat einen Slot, und jeder Tag ist derselbe
|
||
var luecken := 0
|
||
for stunde in range(24):
|
||
if Game.slot_bei_stunde(id, stunde) < 0:
|
||
luecken += 1
|
||
p(luecken == 0, "Alle 24 Stunden sind belegt")
|
||
Game.day = 1
|
||
var tag_eins: Array = []
|
||
for stunde in range(24):
|
||
tag_eins.append(Game.slot_bei_stunde(id, stunde))
|
||
Game.day = 9
|
||
var tag_neun: Array = []
|
||
for stunde in range(24):
|
||
tag_neun.append(Game.slot_bei_stunde(id, stunde))
|
||
p(tag_eins == tag_neun, "Tag 9 sieht aus wie Tag 1 — es gibt keine Wochentage")
|
||
Game.day = 1
|
||
|
||
ui().systeme_panel(id)
|
||
var zeilen := _knopf_texte()
|
||
p(zeilen.contains("00 – "), "Der Ablauf beginnt um 00 Uhr")
|
||
p(zeilen.contains(" – 24"), "Und reicht bis 24 Uhr")
|
||
var leer := 0
|
||
for stunde in range(24):
|
||
if not Game.gesehen_slot(id, Game.slot_bei_stunde(id, stunde)):
|
||
leer += 1
|
||
p(leer == 24, "Am Anfang ist jede der 24 Stunden unbekannt (%d)" % leer)
|
||
p(zeilen.contains("—") and not zeilen.contains("schläft"),
|
||
"Und im Ablauf steht nur ein Strich")
|
||
ui().schliessen()
|
||
|
||
# Dabeistehen füllt genau die Stunde, in der man steht
|
||
var mensch: Bewohner = null
|
||
for m in welt().humans:
|
||
if m.data["id"] == id:
|
||
mensch = m
|
||
var frei_nr := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
var g := Game.slot_grund(id, nr)
|
||
if g != "schlaeft" and not Ursachen.ist_blocker(g):
|
||
frei_nr = nr
|
||
break
|
||
await _hin_zu(mensch, float(Game.slots(id)[frei_nr][0]) + 0.5)
|
||
main._process(0.05)
|
||
p(Game.gesehen_slot(id, frei_nr), "Wer dabeisteht, füllt die Stunde")
|
||
ui().systeme_panel(id)
|
||
p(_knopf_texte().contains(Orte.text(Game.slot_grund(id, frei_nr))),
|
||
"Sie steht jetzt im Ablauf: %s" % Orte.text(Game.slot_grund(id, frei_nr)))
|
||
ui().schliessen()
|
||
|
||
# Ein Blocker bleibt ein Fragezeichen, bis er benannt ist
|
||
var blocker_nr := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
if Ursachen.ist_blocker(Game.slot_grund(id, nr)):
|
||
blocker_nr = nr
|
||
break
|
||
Game.sehen(id, blocker_nr)
|
||
ui().systeme_panel(id)
|
||
p(_knopf_texte().contains("?"), "Was schiefläuft, steht erst als Fragezeichen da")
|
||
ui().schliessen()
|
||
Game.notieren(id, blocker_nr)
|
||
ui().systeme_panel(id)
|
||
var b: Dictionary = Ursachen.BLOCKER[Game.slot_grund(id, blocker_nr)]
|
||
p(_knopf_texte().contains(str(b["name"])),
|
||
"Nach dem Notieren steht dort, was sie tut")
|
||
p(_knopf_texte().contains(str(Ursachen.LISTE[b["ursache"]]["name"])),
|
||
"Und warum: %s" % Ursachen.LISTE[b["ursache"]]["name"])
|
||
ui().schliessen()
|
||
|
||
|
||
## Der ganze Weg: dabeistehen, den Moment einschätzen, R drücken.
|
||
func test_notieren_im_spiel() -> void:
|
||
abschnitt("Notieren ist eine Entscheidung, kein Timer")
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.erkannt_slots.clear()
|
||
Game.notiz_sperre = 0.0
|
||
ui().schliessen()
|
||
main.player.set_mode(Player.Mode.GEIST)
|
||
main.player.look_enabled = true
|
||
|
||
var blocker_nr := -1
|
||
var frei_nr := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
var g := Game.slot_grund(id, nr)
|
||
if Ursachen.ist_blocker(g) and blocker_nr < 0:
|
||
blocker_nr = nr
|
||
elif g != "schlaeft" and not Ursachen.ist_blocker(g) and frei_nr < 0:
|
||
frei_nr = nr
|
||
p(blocker_nr >= 0 and frei_nr >= 0, "Ihr Tag hat einen Blocker und einen freien Slot")
|
||
|
||
var mensch: Bewohner = null
|
||
for m in welt().humans:
|
||
if m.data["id"] == id:
|
||
mensch = m
|
||
|
||
# Im freien Slot danebengegriffen
|
||
await _hin_zu(mensch, float(Game.slots(id)[frei_nr][0]) + 0.5)
|
||
p(mensch.slot == frei_nr, "Sie steht im freien Slot (%d)" % mensch.slot)
|
||
main._notieren()
|
||
p(not Game.erkannt(id, blocker_nr), "Hier gibt es nichts zu notieren")
|
||
p(Game.notiz_sperre > 0.0, "Der Fehlgriff kostet eine Sperre")
|
||
|
||
# Sperre läuft in Echtzeit ab
|
||
for i in range(200):
|
||
main._process(0.05)
|
||
p(is_equal_approx(Game.notiz_sperre, 0.0), "Sie läuft von allein ab")
|
||
|
||
# Im Blocker-Slot getroffen
|
||
await _hin_zu(mensch, float(Game.slots(id)[blocker_nr][0]) + 0.5)
|
||
p(mensch.slot == blocker_nr, "Jetzt steht sie im Blocker-Slot (%d)" % mensch.slot)
|
||
p(main._fokus_mensch == mensch, "Und ist in Reichweite")
|
||
main._notieren()
|
||
p(Game.erkannt(id, blocker_nr), "R notiert, was hier schiefläuft: %s" %
|
||
Orte.text(Game.slot_grund(id, blocker_nr)))
|
||
|
||
# Ein fremder Mensch geht niemanden etwas an
|
||
Game.erkannt_slots.clear()
|
||
Game.notiz_sperre = 0.0
|
||
var fremd: Bewohner = null
|
||
for m in welt().humans:
|
||
if m.data["id"] != id:
|
||
fremd = m
|
||
main.player.folgt = null
|
||
main.player.global_position = fremd.global_position + Vector3(0, 1.5, 1.5)
|
||
main._process(0.05)
|
||
main._notieren()
|
||
p(Game.erkannt_slots.is_empty(),
|
||
"Ohne angenommene Mission notiert er bei %s nichts" % fremd.data["name"])
|
||
p(is_equal_approx(Game.notiz_sperre, 0.0), "Und das kostet auch keine Sperre")
|
||
|
||
# Aus der Verfolgung heraus geht es ebenfalls
|
||
Game.erkannt_slots.clear()
|
||
await _hin_zu(mensch, float(Game.slots(id)[blocker_nr][0]) + 0.5)
|
||
main._verfolgen()
|
||
p(main.player.folgt == mensch, "An sie gehängt")
|
||
main._notieren()
|
||
p(Game.erkannt(id, blocker_nr), "Auch im Vorbeischweben lässt sich notieren")
|
||
main.player.folgt = null
|
||
|
||
|
||
## Stellt den Geist zur genannten Stunde neben den Menschen.
|
||
func _hin_zu(mensch: Bewohner, stunde: float) -> void:
|
||
for i in range(600):
|
||
schritte(1, 0.05, stunde)
|
||
if mensch._path.is_empty():
|
||
break
|
||
main.player.folgt = null
|
||
main.player.global_position = mensch.global_position + Vector3(0, 1.5, 1.5)
|
||
main.player.velocity = Vector3.ZERO
|
||
await get_tree().process_frame
|
||
main._process(0.05)
|
||
|
||
|
||
func test_systeme() -> void:
|
||
abschnitt("Systeme — gegen die Ursache, nicht gegen die Tätigkeit")
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.systeme.clear()
|
||
Game.cooldown.clear()
|
||
Game.known.clear()
|
||
for t in TechDB.LIST:
|
||
Game.known[str(t["id"])] = 28
|
||
Game.erkannt_slots[id] = {}
|
||
for nr in range(Game.slots(id).size()):
|
||
Game.erkannt_slots[id][nr] = true
|
||
|
||
var blocker_nr := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
if Ursachen.ist_blocker(Game.slot_grund(id, nr)):
|
||
blocker_nr = nr
|
||
break
|
||
var b: Dictionary = Ursachen.BLOCKER[Game.slot_grund(id, blocker_nr)]
|
||
p(blocker_nr >= 0, "Ein Blocker im Tag: %s" % b["name"])
|
||
|
||
# falsche Ursache geht nicht
|
||
var falsch := ""
|
||
for tid in TechDB.ids():
|
||
if str(TechDB.by_id(tid)["ursache"]) != str(b["ursache"]):
|
||
falsch = tid
|
||
break
|
||
var pr_falsch := Game.system_pruefen(id, blocker_nr, falsch)
|
||
p(not pr_falsch["geht"], "Eine Technik gegen eine andere Ursache passt nicht hier")
|
||
|
||
# richtige Ursache, gleiche Belohnung -> bessere Güte
|
||
var gleich := ""
|
||
var anders := ""
|
||
for tid in TechDB.fuer_ursache(str(b["ursache"])):
|
||
var t := TechDB.by_id(str(tid))
|
||
if str(t["belohnung"]) == str(b["belohnung"]) or TechDB.entfernt_ausloeser(str(tid)):
|
||
gleich = str(tid)
|
||
else:
|
||
anders = str(tid)
|
||
p(gleich != "", "Es gibt eine Technik mit derselben Belohnung: %s" %
|
||
TechDB.by_id(gleich)["name"])
|
||
var g1: float = float(Game.system_pruefen(id, blocker_nr, gleich)["guete"])
|
||
p(g1 > Game.GUETE_NEU, "Die hält besser (Güte %.2f)" % g1)
|
||
if anders != "":
|
||
var g2: float = float(Game.system_pruefen(id, blocker_nr, anders)["guete"])
|
||
p(g2 < g1, "Eine mit anderer Belohnung hält schlechter (%.2f)" % g2)
|
||
|
||
# einbauen
|
||
Game.cooldown.clear()
|
||
var vorher := Game.slot_aktivitaet(id, blocker_nr)
|
||
var versuche := 0
|
||
var res: Dictionary = {}
|
||
while versuche < 30:
|
||
Game.cooldown.clear()
|
||
res = Game.system_setzen(id, blocker_nr, gleich, false)
|
||
versuche += 1
|
||
if res.get("erfolg", false):
|
||
break
|
||
p(res.get("erfolg", false), "Nach höchstens %d Versuchen steht das System" % versuche)
|
||
p(Game.slot_aktivitaet(id, blocker_nr) != vorher,
|
||
"Der Slot zeigt jetzt die Ersatzhandlung: %s" % Game.slot_aktivitaet(id, blocker_nr))
|
||
p(Game.ursache_geloest(id, str(b["ursache"])) > 0.0,
|
||
"Und die Ursache gilt als gelöst")
|
||
Game.system_entfernen(id, blocker_nr)
|
||
p(Game.ursache_geloest(id, str(b["ursache"])) == 0.0,
|
||
"Herausnehmen macht es rückgängig")
|
||
p(Game.slot_aktivitaet(id, blocker_nr) == vorher, "Der Blocker ist wieder da")
|
||
|
||
# Schlaf bleibt tabu
|
||
var schlaf := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
if Game.slot_grund(id, nr) == "schlaeft":
|
||
schlaf = nr
|
||
p(schlaf >= 0 and not Game.system_pruefen(id, schlaf, gleich)["geht"],
|
||
"Im Schlaf lässt sich nichts einrichten")
|
||
|
||
# In einen freien Slot passt jede Technik, aber nur mit einfacher Güte
|
||
var frei := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
var g := Game.slot_grund(id, nr)
|
||
if g != "schlaeft" and not Ursachen.ist_blocker(g):
|
||
frei = nr
|
||
break
|
||
Game.cooldown.clear()
|
||
var pr_frei := Game.system_pruefen(id, frei, falsch)
|
||
p(pr_frei["geht"], "In einen freien Slot passt auch eine andere Technik")
|
||
p(is_equal_approx(float(pr_frei["guete"]), Game.GUETE_NEU),
|
||
"Aber nur mit einfacher Güte (%.2f)" % pr_frei["guete"])
|
||
|
||
|
||
## Je mehr Ursachen abgedeckt sind, desto schneller kommt der Traum voran.
|
||
func test_tempo_des_traums() -> void:
|
||
abschnitt("Tempo — je besser die Systeme, desto schneller")
|
||
_ohne_momente()
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.systeme.clear()
|
||
Game.cooldown.clear()
|
||
Game.known.clear()
|
||
for t in TechDB.LIST:
|
||
Game.known[str(t["id"])] = 28
|
||
Game.erkannt_slots[id] = {}
|
||
for nr in range(Game.slots(id).size()):
|
||
Game.erkannt_slots[id][nr] = true
|
||
|
||
var urs: Array = Game.ursachen_von(id)
|
||
var leer := Game.traum_tempo(id)
|
||
p(leer < Game.BASIS_TEMPO * 0.25,
|
||
"Ohne Systeme kriecht der Traum: %.0f %% je Tag" % leer)
|
||
|
||
var gesetzt: Array = []
|
||
for u in urs:
|
||
for nr in range(Game.slots(id).size()):
|
||
if Ursachen.ursache_von(Game.slot_grund(id, nr)) != str(u):
|
||
continue
|
||
for tid in TechDB.fuer_ursache(str(u)):
|
||
var pr := Game.system_pruefen(id, nr, str(tid))
|
||
if pr["geht"]:
|
||
Game.systeme[id] = Game.systeme.get(id, {})
|
||
Game.systeme[id][nr] = {"tech": str(tid), "guete": pr["guete"]}
|
||
gesetzt.append(Game.traum_tempo(id))
|
||
break
|
||
break
|
||
p(gesetzt.size() == urs.size(), "Gegen jede Ursache steht ein System")
|
||
var steigt := true
|
||
var vor := leer
|
||
for w in gesetzt:
|
||
if float(w) <= vor:
|
||
steigt = false
|
||
vor = float(w)
|
||
p(steigt, "Jedes weitere System hebt das Tempo: %.0f -> %s" %
|
||
[leer, str(gesetzt.map(func(x): return "%.0f" % float(x)))])
|
||
p(Game.traum_tempo(id) > leer * 8.0,
|
||
"Alles gelöst ist mehr als achtmal so schnell (%.0f statt %.0f)" %
|
||
[Game.traum_tempo(id), leer])
|
||
|
||
# Die Bremsen wirken multiplikativ
|
||
Game.systeme.clear()
|
||
var keine := Game.traum_tempo(id)
|
||
var erwartet := Game.BASIS_TEMPO
|
||
for u in urs:
|
||
erwartet *= float(Ursachen.LISTE[u]["faktor"])
|
||
p(is_equal_approx(keine, erwartet),
|
||
"Zwei Bremsen bremsen doppelt: %.1f wie gerechnet" % keine)
|
||
|
||
# Fortschritt läuft über die Zeit, nicht über Einzeltreffer
|
||
Game.progress[id] = 0.0
|
||
Game.traum_voranbringen(24.0)
|
||
p(is_equal_approx(Game.mission_progress(id), keine),
|
||
"Ein ganzer Tag bringt genau ein Tagestempo (%.1f %%)" % Game.mission_progress(id))
|
||
|
||
|
||
func test_mission_abschliessen() -> void:
|
||
abschnitt("Mission abschließen")
|
||
_ohne_momente()
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.progress[id] = 0.0
|
||
Game.completed.clear()
|
||
var tage := 0
|
||
while Game.mission_progress(id) < Game.MISSION_ZIEL and tage < 200:
|
||
Game.traum_voranbringen(24.0)
|
||
tage += 1
|
||
p(Game.completed.has(id), "Der Traum erfüllt sich nach %d Tagen" % tage)
|
||
p(Game.active_mission == "", "Aktive Mission wurde freigegeben")
|
||
p(DreamDB.LIST.size() == Game.completed.size(),
|
||
"Der Abschluss lässt das Dorf wachsen (%d Wesen)" % DreamDB.LIST.size())
|
||
schritte(3)
|
||
var lena: Bewohner = welt().humans[0]
|
||
p(lena.beschriftung().contains("erfüllt"), "Lena zeigt sichtbar den erfüllten Traum")
|
||
|
||
|
||
func test_welt_waechst() -> void:
|
||
abschnitt("Erfüllte Träume lassen das Dorf wachsen")
|
||
_ohne_momente()
|
||
var zugeordnet: Dictionary = {}
|
||
var doppelt: Array = []
|
||
for i in range(TechDB.LIST.size()):
|
||
var ort := DreamDB.ort_von_technik(str(TechDB.LIST[i]["id"]))
|
||
if zugeordnet.has(ort):
|
||
doppelt.append(ort)
|
||
zugeordnet[ort] = TechDB.LIST[i]["id"]
|
||
p(zugeordnet.size() == TechDB.LIST.size() and doppelt.is_empty(),
|
||
"Jede der %d Techniken hat einen eigenen Lehrort (%d Orte, %d doppelt)" %
|
||
[TechDB.LIST.size(), zugeordnet.size(), doppelt.size()])
|
||
p(DreamDB.ort_von_technik(str(TechDB.LIST[0]["id"])) == "hoersaal",
|
||
"Den ersten Weg lehrt der Hörsaal")
|
||
p(DreamDB.ort_von_technik(str(TechDB.LIST[1]["id"])) == DreamDB.haus_key(0),
|
||
"Den zweiten das erste Haus")
|
||
|
||
p(Game.completed.size() == 1, "Ein Traum ist erfüllt (%d)" % Game.completed.size())
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
var lehrorte: Dictionary = {}
|
||
for n in get_tree().get_nodes_in_group("interactable"):
|
||
if n.get_meta("kind") == "lehre":
|
||
lehrorte[n.get_meta("arg")] = n
|
||
p(lehrorte.has("hoersaal") and lehrorte.has(DreamDB.haus_key(0)),
|
||
"Hörsaal und das erste Haus lehren (%s)" % ", ".join(lehrorte.keys()))
|
||
p(not lehrorte.has(DreamDB.haus_key(1)), "Ein zweites Haus gibt es noch nicht")
|
||
p(Game.insel_radius() >= Game.INSEL_GRUND,
|
||
"Die Insel trägt das Dorf (%.0f)" % Game.insel_radius())
|
||
|
||
var neu_id := str(TechDB.LIST[1]["id"])
|
||
Game.known = {str(_basiswege()[0]): 0}
|
||
ui().lehre_panel(DreamDB.haus_key(0))
|
||
p(ui()._kind == "lehre", "Das neue Haus lehrt")
|
||
p(_panel_text().contains(str(TechDB.by_id(neu_id)["name"])),
|
||
"Und zwar den Weg, den sein Bewohner mitbrachte")
|
||
p(_druecke("Diesen Weg"), "Er lässt sich lernen")
|
||
p(Game.knows(neu_id), "Gelernt: %s" % TechDB.by_id(neu_id)["name"])
|
||
ui().schliessen()
|
||
p(not Game.technik_offen(str(TechDB.LIST[2]["id"])),
|
||
"%s bleibt unentdeckt" % TechDB.by_id(str(TechDB.LIST[2]["id"]))["name"])
|
||
p(Game.offene_techniken() == 2, "Entdeckt sind zwei Wege (%d)" % Game.offene_techniken())
|
||
|
||
await _dorf_platz_pruefen()
|
||
|
||
# Ausgangslage für die folgenden Prüfungen
|
||
Game.known.erase(neu_id)
|
||
Game.welt_erzeugen()
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
|
||
|
||
## Das Dorf wird erzeugt — also muss über viele Ausbaustufen gelten:
|
||
## nichts überlappt, nichts ragt über den Rand, jeder Weg passt ins Zeitfenster.
|
||
func _dorf_platz_pruefen() -> void:
|
||
var merker: Array = Game.completed.duplicate()
|
||
var stufen := 24
|
||
var engster := 999.0
|
||
var engster_text := ""
|
||
var ueberlappung := ""
|
||
var rand_fehler := ""
|
||
for stufe in range(stufen + 1):
|
||
Game.completed.clear()
|
||
for i in range(stufe):
|
||
Game.completed.append("stufe_%d" % i)
|
||
Game.welt_erzeugen()
|
||
var radius := Game.insel_radius()
|
||
var keys: Array = []
|
||
for key in DreamDB.PLACES:
|
||
if float(DreamDB.PLACES[key]["size"][0]) > 0.0:
|
||
keys.append(key)
|
||
for a in range(keys.size()):
|
||
var ka: String = keys[a]
|
||
var ca := Orte.center(DreamDB.PLACES, ka)
|
||
var sa: Array = DreamDB.PLACES[ka]["size"]
|
||
var ecke: float = Vector3(absf(ca.x) + float(sa[0]) * 0.5, 0,
|
||
absf(ca.z) + float(sa[1]) * 0.5).length()
|
||
if radius - ecke < engster:
|
||
engster = radius - ecke
|
||
engster_text = ("%s bei %d Häusern (%.1f von %.0f)"
|
||
% [DreamDB.PLACES[ka]["name"], stufe, ecke, radius])
|
||
if ecke >= radius and rand_fehler == "":
|
||
rand_fehler = "%s ragt bei %d Häusern über den Rand (%.1f >= %.0f)" % \
|
||
[DreamDB.PLACES[ka]["name"], stufe, ecke, radius]
|
||
for b in range(a + 1, keys.size()):
|
||
var kb: String = keys[b]
|
||
var cb := Orte.center(DreamDB.PLACES, kb)
|
||
var sb: Array = DreamDB.PLACES[kb]["size"]
|
||
var dx: float = absf(ca.x - cb.x) - (float(sa[0]) + float(sb[0])) * 0.5
|
||
var dz: float = absf(ca.z - cb.z) - (float(sa[1]) + float(sb[1])) * 0.5
|
||
if dx < 2.0 and dz < 2.0 and ueberlappung == "":
|
||
ueberlappung = "%s und %s stehen ineinander (%.1f / %.1f)" % \
|
||
[DreamDB.PLACES[ka]["name"], DreamDB.PLACES[kb]["name"], dx, dz]
|
||
p(ueberlappung == "", "Über %d Ausbaustufen berührt sich kein Gebäude (%s)" %
|
||
[stufen, "keins" if ueberlappung == "" else ueberlappung])
|
||
p(rand_fehler == "" and engster >= 4.0,
|
||
"Und keins ragt über den Rand — enger Fall: %s%s" %
|
||
[engster_text, "" if rand_fehler == "" else " — " + rand_fehler])
|
||
|
||
Game.completed.clear()
|
||
for i in range(stufen):
|
||
Game.completed.append("stufe_%d" % i)
|
||
Game.welt_erzeugen()
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
_wege_pruefen("Traumdorf mit %d Häusern" % stufen, DreamDB.PLACES, DreamDB.LIST, main.traum.nav_map)
|
||
# Jede Haustür muss zu Fuß erreichbar sein -- keine Klippe vor der Tür
|
||
var platz := Orte.center(DreamDB.PLACES, "lichtung") + Vector3(2.2, 0, 0)
|
||
var unerreichbar: Array = []
|
||
for key in DreamDB.PLACES:
|
||
if not str(key).begins_with("haus_"):
|
||
continue
|
||
var t := Orte.door(DreamDB.PLACES, key)
|
||
t.y = DreamDB.hoehe_bei(t.x, t.z)
|
||
var pf := NavigationServer3D.map_get_path(main.traum.nav_map, platz, t, true)
|
||
if pf.is_empty() or pf[pf.size() - 1].distance_to(t) > 1.5:
|
||
unerreichbar.append("%s (Terrasse %.1f)" % [key, DreamDB.terrasse(key)])
|
||
p(unerreichbar.is_empty(), "Bei %d Häusern ist jede Haustür vom Dorfplatz aus zu Fuß erreichbar%s" %
|
||
[stufen, "" if unerreichbar.is_empty() else " — nicht: " + ", ".join(unerreichbar)])
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
_wege_pruefen("Stadt mit %d Menschen" % HumanDB.LIST.size(), HumanDB.PLACES,
|
||
HumanDB.LIST, main.erde.nav_map)
|
||
Game.completed.assign(merker)
|
||
Game.welt_erzeugen()
|
||
|
||
|
||
func test_techniken_verbessern_sich() -> void:
|
||
abschnitt("Techniken verbessern sich nur durch Anwendung")
|
||
_ohne_momente()
|
||
Game.known.clear()
|
||
Game.systeme.clear()
|
||
Game.cooldown.clear()
|
||
p(Game.learn("zwei_minuten"), "Technik gelernt")
|
||
p(Game.skill_of("zwei_minuten") == 1, "Frisch gelernt heißt Stufe 1")
|
||
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.erkannt_slots[id] = {}
|
||
for nr in range(Game.slots(id).size()):
|
||
Game.erkannt_slots[id][nr] = true
|
||
var frei := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
var g := Game.slot_grund(id, nr)
|
||
if g != "schlaeft" and not Ursachen.ist_blocker(g):
|
||
frei = nr
|
||
break
|
||
|
||
var versuche := 0
|
||
while Game.skill_of("zwei_minuten") < 3 and versuche < 80:
|
||
Game.cooldown.clear()
|
||
Game.system_entfernen(id, frei)
|
||
Game.system_setzen(id, frei, "zwei_minuten", false)
|
||
versuche += 1
|
||
p(Game.skill_of("zwei_minuten") >= 3,
|
||
"Nach %d Einsätzen von allein auf Stufe %d" % [versuche, Game.skill_of("zwei_minuten")])
|
||
|
||
# Es gibt keinen zweiten Weg nach oben
|
||
p(not Game.has_method("deepen"), "Kein gezieltes Vertiefen mehr im Spiel")
|
||
p(not ("einsicht" in Game), "Und keine Währung, die man dafür ausgeben könnte")
|
||
var stand: int = Game.known["zwei_minuten"]
|
||
Game.advance_time(Game.DAY_SECONDS)
|
||
p(Game.known["zwei_minuten"] == stand, "Ein ganzer Tag bringt keine Erfahrung")
|
||
|
||
ui().lehre_panel(DreamDB.ort_von_technik("zwei_minuten"))
|
||
var geknoepft := _druecke("Diesen Weg") or _druecke("Vertiefen") or _druecke("Üben")
|
||
ui().schliessen()
|
||
p(not geknoepft and Game.known["zwei_minuten"] == stand,
|
||
"Ein zweiter Besuch am Lehrort ändert nichts (%d EP)" % stand)
|
||
|
||
|
||
func test_journal_ueberall() -> void:
|
||
abschnitt("Journal ist immer dabei")
|
||
p(InputMap.has_action("journal"), "Eigene Aktion für das Journal")
|
||
var tasten: Array = []
|
||
for ev in InputMap.action_get_events("journal"):
|
||
tasten.append(ev.physical_keycode)
|
||
p(tasten.has(KEY_J) and tasten.has(KEY_TAB), "Auf J und Tab gelegt")
|
||
|
||
p(main.auf_erde, "Test läuft auf der Erde")
|
||
ui().journal_panel()
|
||
p(ui().offen, "Journal öffnet auf der Erde")
|
||
ui().schliessen()
|
||
|
||
main._traumwelt()
|
||
await get_tree().process_frame
|
||
ui().journal_panel()
|
||
p(ui().offen, "Journal öffnet auch in der Traumwelt")
|
||
ui().schliessen()
|
||
|
||
# Traumwesen sind keine Missionsziele
|
||
var wesen: Bewohner = welt().bewohner[0]
|
||
main.player.set_mode(Player.Mode.TRAUM)
|
||
main.player.global_position = wesen.global_position + Vector3(0, 1.0, 1.2)
|
||
main._process(0.05)
|
||
p(main._fokus_mensch == null, "Traumwesen lassen sich nicht mit Techniken bearbeiten")
|
||
p(not wesen.mit_mission, "Sie tragen keine Missionsmarkierung")
|
||
p(wesen.beschriftung().contains(str(wesen.data["rolle"])),
|
||
"Ihre Beschriftung nennt Name und Rolle: %s" % wesen.beschriftung().replace("\n", " / "))
|
||
|
||
|
||
func test_steuerung() -> void:
|
||
abschnitt("Steuerung")
|
||
var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "drehen",
|
||
"interagieren", "folgen", "notieren", "journal", "tempo_1", "tempo_2", "tempo_3"]
|
||
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 %d Steuerungsaktionen sind mit Tasten belegt" % aktionen.size())
|
||
|
||
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)
|
||
|
||
# Ein geschlossener Klotz hält ihn auf: von der Straße aus gegen die Wand
|
||
var war_erde: bool = main.auf_erde
|
||
if not war_erde:
|
||
main._erde()
|
||
await get_tree().process_frame
|
||
main.player.look_enabled = true
|
||
var klotz: Dictionary = {}
|
||
var richtung := Vector3.ZERO
|
||
for b in welt()._bauten:
|
||
if not (b["waende"] as Array).is_empty():
|
||
continue # ein Haus mit Wänden ist kein Klotz
|
||
for pz in HumanDB.PARZELLEN:
|
||
if (pz["pos"] as Vector3).distance_to(b["c"]) < 0.1:
|
||
klotz = b
|
||
richtung = Orte.DIR_VEC[pz["dir"]]
|
||
if not klotz.is_empty():
|
||
break
|
||
p(not klotz.is_empty(), "Es gibt geschlossene Klötze")
|
||
var kc: Vector3 = klotz["c"]
|
||
var kg: Vector2 = klotz["size"]
|
||
var halb: float = (kg.y if absf(richtung.z) > 0.0 else kg.x) * 0.5
|
||
main.player.global_position = kc + richtung * (halb + 2.0) + Vector3(0, 0.3, 0)
|
||
main.player.velocity = Vector3.ZERO
|
||
main.player.yaw = atan2(richtung.x, richtung.z) # "vor" zeigt zum Klotz
|
||
Input.action_press("vor")
|
||
for i in range(60):
|
||
await get_tree().physics_frame
|
||
Input.action_release("vor")
|
||
var tiefe_im_klotz: float = halb - (main.player.global_position - kc).dot(richtung)
|
||
p(tiefe_im_klotz < 0.6, "Wände halten den Geist auf (%.1f m in der Wand)" % maxf(0.0, tiefe_im_klotz))
|
||
# Eingeklemmt zwischen zwei Klötzen: nach zwei Sekunden Drücken steht er wieder auf der Straße
|
||
var quer_r := Vector3(-richtung.z, 0, richtung.x)
|
||
var breit: float = (kg.x if absf(richtung.z) > 0.0 else kg.y) * 0.5
|
||
main.player.global_position = kc + quer_r * (breit + 0.4) + Vector3(0, 0.3, 0)
|
||
main.player.velocity = Vector3.ZERO
|
||
main.player.spruenge = 0
|
||
main.player.yaw = atan2(richtung.x, richtung.z)
|
||
Input.action_press("vor")
|
||
for i in range(200):
|
||
await get_tree().physics_frame
|
||
Input.action_release("vor")
|
||
var fuge: Vector3 = kc + quer_r * (breit + 0.4)
|
||
var weg_von_fuge: float = _xz(main.player.global_position).distance_to(_xz(fuge))
|
||
p(main.player.spruenge >= 1 or weg_von_fuge > 3.0,
|
||
"In der Fuge zwischen zwei Klötzen bleibt er nicht stecken (%.1f m weg, %d Befreiungen)" %
|
||
[weg_von_fuge, main.player.spruenge])
|
||
if not war_erde:
|
||
main._traumwelt() # die folgenden Messungen erwarten die Traumwelt
|
||
await get_tree().process_frame
|
||
|
||
|
||
## Nach jedem Einsatz braucht eine Technik Ruhe. Ein Weg allein deckt daher
|
||
## keinen ganzen Tag ab — das ist der Grund, mehrere zu lernen.
|
||
## Nach jedem Einsatz braucht eine Technik Ruhe. Ein Weg allein baut daher
|
||
## nicht den ganzen Tag um.
|
||
func test_cooldown() -> void:
|
||
abschnitt("Techniken haben einen Cooldown")
|
||
_ohne_momente()
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.cooldown.clear()
|
||
Game.systeme.clear()
|
||
Game.known.clear()
|
||
for t in TechDB.LIST:
|
||
Game.known[str(t["id"])] = 28
|
||
Game.erkannt_slots[id] = {}
|
||
for nr in range(Game.slots(id).size()):
|
||
Game.erkannt_slots[id][nr] = true
|
||
var frei: Array = []
|
||
for nr in range(Game.slots(id).size()):
|
||
var g := Game.slot_grund(id, nr)
|
||
if g != "schlaeft" and not Ursachen.ist_blocker(g):
|
||
frei.append(nr)
|
||
|
||
p(Game.bereit("pomodoro"), "Frisch ist eine Technik einsatzbereit")
|
||
var erste := Game.system_setzen(id, int(frei[0]), "pomodoro", false)
|
||
p(erste["ok"], "Der erste Einsatz geht durch")
|
||
p(not Game.bereit("pomodoro"), "Danach ist sie gesperrt")
|
||
p(is_equal_approx(Game.cooldown_rest("pomodoro"), Game.TECHNIK_COOLDOWN),
|
||
"Und zwar %.0f Sekunden lang" % Game.TECHNIK_COOLDOWN)
|
||
|
||
var zweite := Game.system_setzen(id, int(frei[1]), "pomodoro", false)
|
||
p(not zweite["ok"], "Ein zweiter Einsatz prallt an der Sperre ab")
|
||
p(str(zweite["grund"]).contains("einsatzbereit"), "Mit klarer Begründung")
|
||
p(Game.bereit("zerlegen"), "Die Sperre gilt nur für diese eine Technik")
|
||
p(Game.system_setzen(id, int(frei[1]), "zerlegen", false)["ok"],
|
||
"Eine andere Technik greift weiter")
|
||
|
||
Game.advance_time(Game.TECHNIK_COOLDOWN * 0.5)
|
||
p(not Game.bereit("pomodoro"), "Nach der halben Zeit noch immer gesperrt")
|
||
Game.advance_time(Game.TECHNIK_COOLDOWN * 0.6)
|
||
p(Game.bereit("pomodoro"), "Nach Ablauf wieder frei")
|
||
p(Game.cooldown_rest("pomodoro") == 0.0, "Und der Zähler steht auf null")
|
||
|
||
|
||
## Wie gut etwas steht, sagt das Spiel nicht. Es zeigt Auslöser und Belohnung
|
||
## -- entscheiden muss der Spieler.
|
||
func test_chance_verdeckt() -> void:
|
||
abschnitt("Die Erfolgschance bleibt verdeckt")
|
||
var id := "mensch_0"
|
||
Game.active_mission = id
|
||
Game.cooldown.clear()
|
||
Game.systeme.clear()
|
||
Game.known.clear()
|
||
for t in TechDB.LIST:
|
||
Game.known[str(t["id"])] = 28
|
||
Game.erkannt_slots[id] = {}
|
||
for nr in range(Game.slots(id).size()):
|
||
Game.erkannt_slots[id][nr] = true
|
||
var blocker_nr := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
if Ursachen.ist_blocker(Game.slot_grund(id, nr)):
|
||
blocker_nr = nr
|
||
break
|
||
|
||
ui().slot_panel(id, blocker_nr)
|
||
var wahl := _panel_text()
|
||
var knoepfe := _knopf_texte()
|
||
p(not knoepfe.contains("%"), "Kein Knopf nennt eine Prozentzahl")
|
||
p(not wahl.contains("Erfolgschance"), "Und das Wort Erfolgschance kommt nicht vor")
|
||
p(not wahl.contains("Wann:") and not wahl.contains("Zu sehen an:"),
|
||
"Die Auswahl zeigt nur Namen, keine Beschreibung")
|
||
p(wahl.contains("Auslöser") and wahl.contains("Belohnung"),
|
||
"Dafür stehen Auslöser und Belohnung des Blockers da")
|
||
ui().schliessen()
|
||
|
||
ui().systeme_panel(id)
|
||
p(not _knopf_texte().contains("%"), "Auch die Tageszeilen nennen keine Chance")
|
||
p(_panel_text().contains("je Tag"), "Das Panel nennt das Tempo des Traums")
|
||
ui().schliessen()
|
||
|
||
ui().journal_panel("techniken")
|
||
p(_knopf_texte().contains(str(TechDB.LIST[0]["name"])),
|
||
"Das Journal listet die Technik als eine Zeile")
|
||
ui().technik_panel(str(TechDB.LIST[0]["id"]))
|
||
var buch := _panel_text()
|
||
p(buch.contains("Wann:") and buch.contains("Zu sehen an:"),
|
||
"Aufgeschlagen stehen Was, Wann, Warum und Zu sehen an da")
|
||
ui().schliessen()
|
||
|
||
|
||
## Wenn nur lösbare Missionen aushängen, darf das Brett nie ganz leer werden —
|
||
## sonst hinge Loop 2 fest: ohne Traum kein neuer Weg, ohne Weg kein Aushang.
|
||
func test_brett_nie_leer() -> void:
|
||
abschnitt("Das Brett läuft nie leer")
|
||
_ohne_momente()
|
||
var merk_c: Array = Game.completed.duplicate()
|
||
var merk_k: Dictionary = Game.known.duplicate()
|
||
var leer_bei := -1
|
||
var kleinste := 99
|
||
var stufen := 14
|
||
for stufe in range(stufen):
|
||
# Ein Spieler, der alles lernt, was das Dorf hergibt
|
||
for t in TechDB.LIST:
|
||
if Game.technik_offen(str(t["id"])) and not Game.knows(str(t["id"])):
|
||
Game.known[str(t["id"])] = 0
|
||
var offen: Array = Game.offene_missionen()
|
||
kleinste = mini(kleinste, offen.size())
|
||
if offen.is_empty():
|
||
leer_bei = stufe
|
||
break
|
||
Game.completed.append(str(offen[0]))
|
||
Game.welt_erzeugen()
|
||
p(leer_bei < 0, "Über %d Ausbaustufen hängt immer etwas aus%s" %
|
||
[stufen, "" if leer_bei < 0 else " — leer bei Stufe %d" % leer_bei])
|
||
p(kleinste >= 1, "Im knappsten Fall bleibt %d Aushang übrig" % kleinste)
|
||
|
||
# Und der jeweils neueste Mensch ist mit dem neuesten Weg zu schaffen
|
||
Game.completed.clear()
|
||
Game.known.clear()
|
||
Game.welt_erzeugen()
|
||
var passt := 0
|
||
for i in range(TechDB.LIST.size()):
|
||
Game.completed.clear()
|
||
for k in range(i):
|
||
Game.completed.append("stufe_%d" % k)
|
||
Game.welt_erzeugen()
|
||
Game.known = {str(TechDB.LIST[i]["id"]): 0}
|
||
if not Game.offene_missionen().is_empty():
|
||
passt += 1
|
||
p(passt == TechDB.LIST.size(),
|
||
"Zu jedem der %d Wege gibt es einen Menschen, dem er hilft (%d)" %
|
||
[TechDB.LIST.size(), passt])
|
||
|
||
Game.completed.assign(merk_c)
|
||
Game.known = merk_k
|
||
Game.welt_erzeugen()
|
||
|
||
|
||
## Warten soll nicht wehtun: 1, 2, 3 stellen das Tempo. Über der Nacht des
|
||
## Menschen springt es von allein auf drei — dort ist ohnehin nichts zu holen.
|
||
func test_tempo() -> void:
|
||
abschnitt("Tempo lässt sich stellen")
|
||
Game.completed.clear()
|
||
Game.known = {"pomodoro": 0}
|
||
Game.welt_erzeugen()
|
||
Game.active_mission = ""
|
||
Game.tempo = 1
|
||
p(is_equal_approx(Game.zeit_faktor(), 1.0), "Ohne Zutun läuft die Welt einfach")
|
||
|
||
# Wie in Die Sims: die Stufe ist nicht der Faktor
|
||
var erwartet := {1: 1.0, 2: 3.0, 3: 7.0}
|
||
for stufe in [2, 3, 1]:
|
||
Game.tempo_setzen(stufe)
|
||
p(Game.tempo == stufe and is_equal_approx(Game.zeit_faktor(), float(erwartet[stufe])),
|
||
"Stufe %d läuft %.0f-fach" % [stufe, Game.zeit_faktor()])
|
||
Game.tempo_setzen(3)
|
||
p(not is_equal_approx(Game.zeit_faktor(), 3.0),
|
||
"Stufe 3 heißt gerade nicht dreifach (%.0f×)" % Game.zeit_faktor())
|
||
Game.tempo_setzen(1)
|
||
Game.tempo_setzen(9)
|
||
p(Game.tempo == 3, "Höher als drei geht nicht (%d)" % Game.tempo)
|
||
Game.tempo_setzen(0)
|
||
p(Game.tempo == 1, "Und tiefer als eins auch nicht (%d)" % Game.tempo)
|
||
|
||
# Über der Nacht des Menschen springt es von allein
|
||
Game.accept_mission("mensch_0")
|
||
var wach := -1.0
|
||
var nacht := -1.0
|
||
for stunde in range(24):
|
||
Game.time_h = float(stunde) + 0.5
|
||
if Game.schlaeft_ziel():
|
||
nacht = Game.zeit_faktor()
|
||
else:
|
||
wach = Game.zeit_faktor()
|
||
p(nacht > 0.0 and wach > 0.0, "Lenas Tag hat wache und schlafende Stunden")
|
||
p(is_equal_approx(nacht, Game.faktor_von(3)),
|
||
"Im Schlaf läuft die Welt auf der höchsten Stufe (%.0f×)" % nacht)
|
||
p(is_equal_approx(wach, 1.0), "Wach wieder auf der gewählten Stufe (%.0f)" % wach)
|
||
|
||
Game.tempo_setzen(1)
|
||
Game.time_h = 2.0
|
||
p(Game.schlaeft_ziel(), "Um 02:00 schläft sie")
|
||
p(is_equal_approx(Game.zeit_faktor(), Game.faktor_von(3)),
|
||
"Die eigene Wahl Stufe 1 gilt dann nicht — die Nacht springt")
|
||
|
||
# Ohne Mission gibt es nichts zu überspringen
|
||
Game.active_mission = ""
|
||
p(not Game.schlaeft_ziel(), "Ohne Mission kein Nachtsprung")
|
||
p(is_equal_approx(Game.zeit_faktor(), 1.0), "Dann zählt nur die Wahl")
|
||
|
||
# Der Faktor beschleunigt die Uhr wirklich
|
||
Game.tempo_setzen(3)
|
||
Game.time_h = 8.0
|
||
Game.day = 1
|
||
var vorher := Game.time_h
|
||
Game.advance_time(1.0 * Game.zeit_faktor())
|
||
var schnell := Game.time_h - vorher
|
||
Game.tempo_setzen(1)
|
||
Game.time_h = 8.0
|
||
vorher = Game.time_h
|
||
Game.advance_time(1.0 * Game.zeit_faktor())
|
||
var langsam := Game.time_h - vorher
|
||
p(is_equal_approx(schnell, langsam * Game.faktor_von(3)),
|
||
"Auf Stufe 3 vergeht %.0f-mal so viel Zeit je Sekunde (%.4f statt %.4f h)" %
|
||
[Game.faktor_von(3), schnell, langsam])
|
||
|
||
# Das HUD sagt, woran man ist
|
||
Game.tempo_setzen(2)
|
||
ui().journal_panel()
|
||
ui().schliessen()
|
||
ui()._hud_aktualisieren()
|
||
p(ui()._hud.text.contains("Tempo 2 (3×)"),
|
||
"Das HUD nennt Stufe und Faktor getrennt")
|
||
Game.accept_mission("mensch_0")
|
||
Game.time_h = 2.0
|
||
ui()._hud_aktualisieren()
|
||
p(ui()._hud.text.contains("Nacht"), "Und weist den Nachtsprung aus")
|
||
Game.tempo_setzen(1)
|
||
Game.active_mission = ""
|
||
|
||
|
||
## Das Dorf wird nicht gesetzt, sondern wächst. Also muss über viele
|
||
## Ausbaustufen gelten: nichts überlappt, kein Weg führt durch ein Haus, alles
|
||
## hängt zusammen, und was einmal steht, bleibt stehen.
|
||
func test_dorf_organisch() -> void:
|
||
abschnitt("Das Dorf wächst organisch")
|
||
var merker: Array = Game.completed.duplicate()
|
||
var stufen := 24
|
||
|
||
var ueberlappung := ""
|
||
var wegtreffer := ""
|
||
var lose := ""
|
||
var ohne_anker := ""
|
||
var unerreichbar := ""
|
||
var sektoren_12 := 0
|
||
var dichte := ""
|
||
for stufe in range(stufen + 1):
|
||
Game.completed.clear()
|
||
for i in range(stufe):
|
||
Game.completed.append("stufe_%d" % i)
|
||
Game.welt_erzeugen()
|
||
var netz: Netz = DreamDB.NETZ
|
||
var keys: Array = []
|
||
for key in DreamDB.PLACES:
|
||
if float(DreamDB.PLACES[key]["size"][0]) > 0.0:
|
||
keys.append(key)
|
||
|
||
# nichts steht ineinander
|
||
for a in range(keys.size()):
|
||
for b in range(a + 1, keys.size()):
|
||
var ca := Orte.center(DreamDB.PLACES, keys[a])
|
||
var cb := Orte.center(DreamDB.PLACES, keys[b])
|
||
var sa: Array = DreamDB.PLACES[keys[a]]["size"]
|
||
var sb: Array = DreamDB.PLACES[keys[b]]["size"]
|
||
if absf(ca.x - cb.x) < (float(sa[0]) + float(sb[0])) * 0.5 \
|
||
and absf(ca.z - cb.z) < (float(sa[1]) + float(sb[1])) * 0.5 \
|
||
and ueberlappung == "":
|
||
ueberlappung = ("%s und %s bei %d Häusern"
|
||
% [keys[a], keys[b], stufe])
|
||
|
||
# kein Weg führt durch ein Gebäude
|
||
for k in netz.kanten:
|
||
for key in keys:
|
||
var s2: Array = DreamDB.PLACES[key]["size"]
|
||
if Netz.trifft_rechteck(netz.knoten[k[0]], netz.knoten[k[1]],
|
||
Orte.center(DreamDB.PLACES, key),
|
||
Vector2(float(s2[0]), float(s2[1])), 0.0) and wegtreffer == "":
|
||
wegtreffer = "%s bei %d Häusern" % [key, stufe]
|
||
|
||
if not netz.zusammenhaengend() and lose == "":
|
||
lose = "bei %d Häusern" % stufe
|
||
|
||
# jedes Haus hängt am Netz und ist vom Dorfplatz aus zu erreichen
|
||
for i in range(stufe):
|
||
var key3 := DreamDB.haus_key(i)
|
||
if not DreamDB.ANKER.has(key3):
|
||
if ohne_anker == "":
|
||
ohne_anker = "%s bei %d Häusern" % [key3, stufe]
|
||
continue
|
||
if netz.weglaenge(int(DreamDB.ANKER["lichtung"]), int(DreamDB.ANKER[key3])) == INF \
|
||
and unerreichbar == "":
|
||
unerreichbar = "%s bei %d Häusern" % [key3, stufe]
|
||
|
||
if stufe == 12:
|
||
var sekt: Dictionary = {}
|
||
for i in range(stufe):
|
||
var c := Orte.center(DreamDB.PLACES, DreamDB.haus_key(i))
|
||
sekt[int(floor((atan2(c.z, c.x) + PI) / (TAU / 8.0))) % 8] = true
|
||
sektoren_12 = sekt.size()
|
||
if stufe == stufens_probe():
|
||
var flaeche := PI * pow(Game.insel_radius(), 2.0)
|
||
# Terrassen verschiedener Höhe brauchen Böschungen dazwischen: das kostet Fläche
|
||
if flaeche / float(stufe) > 3300.0:
|
||
dichte = "%.0f m² je Haus" % (flaeche / float(stufe))
|
||
|
||
p(ueberlappung == "", "Über %d Ausbaustufen steht kein Gebäude im anderen%s" %
|
||
[stufen, "" if ueberlappung == "" else " — " + ueberlappung])
|
||
p(wegtreffer == "", "Und kein Weg führt durch ein Gebäude%s" %
|
||
["" if wegtreffer == "" else " — " + wegtreffer])
|
||
p(lose == "", "Das Wegenetz hängt immer zusammen%s" %
|
||
["" if lose == "" else " — " + lose])
|
||
p(ohne_anker == "", "Jedes Haus hängt am Netz%s" %
|
||
["" if ohne_anker == "" else " — " + ohne_anker])
|
||
p(unerreichbar == "", "Und ist vom Dorfplatz aus erreichbar%s" %
|
||
["" if unerreichbar == "" else " — " + unerreichbar])
|
||
p(sektoren_12 >= 6,
|
||
"Bei 12 Häusern ist das Dorf rundum gewachsen (%d von 8 Richtungen)" % sektoren_12)
|
||
p(dichte == "", "Die Insel bleibt dicht besiedelt%s" %
|
||
["" if dichte == "" else " — " + dichte])
|
||
|
||
# Verwinkelt heißt: Gabeln und Schleifen, nicht Speichen zur Mitte
|
||
Game.completed.clear()
|
||
for i in range(18):
|
||
Game.completed.append("stufe_%d" % i)
|
||
Game.welt_erzeugen()
|
||
var netz2: Netz = DreamDB.NETZ
|
||
var gabeln := 0
|
||
for i in range(netz2.knoten.size()):
|
||
if netz2.nachbarn(i).size() >= 3:
|
||
gabeln += 1
|
||
var schleifen: int = netz2.kanten.size() - netz2.knoten.size() + 1
|
||
p(gabeln >= 6, "Bei 18 Häusern gabelt sich das Netz %d-mal" % gabeln)
|
||
p(schleifen >= 2, "Und trägt %d Schleifen statt lauter Sackgassen" % schleifen)
|
||
|
||
# Was einmal steht, bleibt stehen
|
||
var vorher: Array = []
|
||
for i in range(18):
|
||
vorher.append(Orte.center(DreamDB.PLACES, DreamDB.haus_key(i)))
|
||
Game.completed.append("stufe_18")
|
||
Game.welt_erzeugen()
|
||
var verrutscht := 0
|
||
for i in range(18):
|
||
if vorher[i].distance_to(Orte.center(DreamDB.PLACES, DreamDB.haus_key(i))) > 0.01:
|
||
verrutscht += 1
|
||
p(verrutscht == 0, "Das 19. Haus verschiebt keines der 18 alten (%d)" % verrutscht)
|
||
|
||
Game.completed.assign(merker)
|
||
Game.welt_erzeugen()
|
||
|
||
|
||
func stufens_probe() -> int:
|
||
return 24
|
||
|
||
|
||
## Die Erde ist ein erzeugter Bezirk. Also muss gelten: Parzellen liegen
|
||
## nebeneinander statt ineinander, jede hat Straßenanschluss, das Netz hängt
|
||
## zusammen -- und anders als im Dorf wächst hier nichts.
|
||
func test_bezirk() -> void:
|
||
abschnitt("Die Erde ist ein Bezirk")
|
||
var netz: Netz = HumanDB.NETZ
|
||
p(HumanDB.PARZELLEN.size() >= 60,
|
||
"Der Bezirk ist in %d Parzellen geteilt" % HumanDB.PARZELLEN.size())
|
||
|
||
var ueber := 0
|
||
for i in range(HumanDB.PARZELLEN.size()):
|
||
for j in range(i + 1, HumanDB.PARZELLEN.size()):
|
||
var a: Dictionary = HumanDB.PARZELLEN[i]
|
||
var b: Dictionary = HumanDB.PARZELLEN[j]
|
||
if absf(a["pos"].x - b["pos"].x) < (a["size"].x + b["size"].x) * 0.5 - 0.05 \
|
||
and absf(a["pos"].z - b["pos"].z) < (a["size"].y + b["size"].y) * 0.5 - 0.05:
|
||
ueber += 1
|
||
p(ueber == 0, "Keine Parzelle steht in einer anderen (%d)" % ueber)
|
||
|
||
var ohne := 0
|
||
var weiteste := 0.0
|
||
for pz in HumanDB.PARZELLEN:
|
||
var d := INF
|
||
for st in HumanDB.STRASSEN:
|
||
var f := Geometry3D.get_closest_point_to_segment(pz["pos"], st["a"], st["b"])
|
||
d = minf(d, f.distance_to(pz["pos"]) - maxf(pz["size"].x, pz["size"].y) * 0.5
|
||
- float(st["breite"]) * 0.5)
|
||
weiteste = maxf(weiteste, d)
|
||
if d > 2.0:
|
||
ohne += 1
|
||
p(ohne == 0, "Jede Parzelle liegt an einer Straße (weiteste Lücke %.1f m)" % weiteste)
|
||
|
||
p(netz.zusammenhaengend(),
|
||
"Das Straßennetz hängt zusammen (%d Knoten, %d Kanten)" %
|
||
[netz.knoten.size(), netz.kanten.size()])
|
||
var unerreichbar := 0
|
||
var keys: Array = HumanDB.PLACES.keys()
|
||
for i in range(keys.size()):
|
||
for j in range(i + 1, keys.size()):
|
||
if netz.weglaenge(int(HumanDB.ANKER[keys[i]]), int(HumanDB.ANKER[keys[j]])) == INF:
|
||
unerreichbar += 1
|
||
p(unerreichbar == 0, "Jeder Ort ist von jedem anderen aus zu erreichen (%d Lücken)" %
|
||
unerreichbar)
|
||
|
||
# Arterien im Raster, roji schmal und als Sackgasse ins Blockinnere
|
||
var arterien := 0
|
||
var gassen := 0
|
||
for st in HumanDB.STRASSEN:
|
||
if float(st["breite"]) >= HumanDB.ARTERIE:
|
||
arterien += 1
|
||
else:
|
||
gassen += 1
|
||
p(arterien == HumanDB.ACHSEN.size() * 2,
|
||
"%d Arterien spannen das Raster auf" % arterien)
|
||
p(gassen >= 4, "Und %d roji führen ins Blockinnere" % gassen)
|
||
p(HumanDB.ROJI < HumanDB.ARTERIE * 0.6,
|
||
"Eine roji ist mit %.1f m deutlich schmaler als eine Arterie (%.1f m)" %
|
||
[HumanDB.ROJI, HumanDB.ARTERIE])
|
||
var sackgassen := 0
|
||
for st in HumanDB.STRASSEN:
|
||
if float(st["breite"]) < HumanDB.ARTERIE:
|
||
var ende: Vector3 = st["b"]
|
||
if netz.nachbarn(netz.fuege_knoten(ende)).size() <= 1:
|
||
sackgassen += 1
|
||
p(sackgassen >= gassen / 2, "Die meisten roji enden blind (%d von %d)" %
|
||
[sackgassen, gassen])
|
||
|
||
# Alle Menschen sind da, und sie werden nicht mehr
|
||
var vorher := HumanDB.LIST.size()
|
||
p(vorher == HumanDB.EINWOHNER, "Alle %d Menschen sind von Anfang an da" % vorher)
|
||
var merker: Array = Game.completed.duplicate()
|
||
for i in range(6):
|
||
Game.completed.append("stufe_%d" % i)
|
||
Game.welt_erzeugen()
|
||
p(HumanDB.LIST.size() == vorher,
|
||
"Sechs erfüllte Träume ändern daran nichts (%d)" % HumanDB.LIST.size())
|
||
Game.completed.assign(merker)
|
||
Game.welt_erzeugen()
|
||
|
||
# Jeder wohnt in einer eigenen Wohnung
|
||
var heime: Dictionary = {}
|
||
for m in HumanDB.LIST:
|
||
heime[str(m["heim"])] = true
|
||
p(heime.size() == HumanDB.LIST.size(),
|
||
"Jeder Mensch hat seine eigene Wohnung (%d)" % heime.size())
|
||
|
||
# Der mittlere Block bleibt frei
|
||
var im_park := 0
|
||
var halb: float = (float(HumanDB.ACHSEN[2]) - float(HumanDB.ACHSEN[1])) * 0.5 \
|
||
- HumanDB.ARTERIE
|
||
for pz in HumanDB.PARZELLEN:
|
||
var q: Vector3 = pz["pos"]
|
||
if absf(q.x) < halb and absf(q.z) < halb:
|
||
im_park += 1
|
||
p(im_park == 0, "Im mittleren Block steht kein Haus — er ist der Park (%d)" % im_park)
|
||
|
||
|
||
## Die Probe auf Loop 2: ohne einen erfüllten Traum gibt es keine neuen Wege.
|
||
## Also muss der erste Traum mit dem Startkapital allein zu schaffen sein.
|
||
func _erster_traum_tage(saat: int) -> int:
|
||
seed(saat)
|
||
Game.cooldown.clear()
|
||
Game.known.clear()
|
||
Game.systeme.clear()
|
||
Game.erkannt_slots.clear()
|
||
Game.notiz_sperre = 0.0
|
||
Game.progress.clear()
|
||
Game.completed.clear()
|
||
Game.active_mission = ""
|
||
_ohne_momente()
|
||
Game.welt_erzeugen()
|
||
for id in _basiswege():
|
||
Game.learn(str(id))
|
||
Game.accept_mission("mensch_0")
|
||
_alles_erkennen("mensch_0")
|
||
_bestes_system("mensch_0")
|
||
Game.progress["mensch_0"] = 0.0
|
||
Game.time_h = 7.0
|
||
Game.day = 1
|
||
for i in range(3000):
|
||
Game.advance_time(1.0)
|
||
Game.traum_voranbringen(24.0 / Game.DAY_SECONDS)
|
||
if Game.completed.has("mensch_0") or Game.day > 30:
|
||
break
|
||
return Game.day
|
||
|
||
|
||
## Deckt alle Slots eines Menschen auf -- für Messungen, nicht fürs Spiel.
|
||
func _alles_erkennen(id: String) -> void:
|
||
Game.erkannt_slots[id] = {}
|
||
for nr in range(Game.slots(id).size()):
|
||
Game.erkannt_slots[id][nr] = true
|
||
|
||
|
||
## Setzt gegen jede Ursache das beste verfügbare System.
|
||
func _bestes_system(id: String) -> int:
|
||
var n := 0
|
||
for u in Game.ursachen_von(id):
|
||
var bester := ""
|
||
var beste := 0.0
|
||
var beste_nr := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
for tid in TechDB.fuer_ursache(str(u)):
|
||
if not Game.knows(str(tid)):
|
||
continue
|
||
Game.cooldown.clear()
|
||
var pr := Game.system_pruefen(id, nr, str(tid))
|
||
if pr["geht"] and float(pr["guete"]) > beste:
|
||
beste = float(pr["guete"])
|
||
bester = str(tid)
|
||
beste_nr = nr
|
||
if bester != "":
|
||
Game.systeme[id] = Game.systeme.get(id, {})
|
||
Game.systeme[id][beste_nr] = {"tech": bester, "guete": beste}
|
||
n += 1
|
||
return n
|
||
|
||
|
||
func test_erster_traum_schaffbar() -> void:
|
||
abschnitt("Der erste Traum ist mit dem Startkapital zu schaffen")
|
||
_ohne_momente()
|
||
Game.completed.clear()
|
||
Game.welt_erzeugen()
|
||
p(Game.offene_techniken() == 1,
|
||
"Am Anfang ist genau ein Weg entdeckt (%d)" % Game.offene_techniken())
|
||
|
||
var tage: Array = []
|
||
for saat in [1, 7, 555, 20240, 90210]:
|
||
tage.append(await _erster_traum_tage(saat))
|
||
p(Game.known.size() == 1, "Man startet mit einem Weg (%d)" % Game.known.size())
|
||
p(Game.system_anzahl("mensch_0") == 1,
|
||
"Damit steht genau ein System (%d)" % Game.system_anzahl("mensch_0"))
|
||
var schlimmster: int = tage.max()
|
||
var bester: int = tage.min()
|
||
p(schlimmster <= 12, "Der Traum erfüllt sich in allen %d Läufen (%s)" %
|
||
[tage.size(), str(tage)])
|
||
p(bester >= 2, "Nie geschenkt — mindestens %d Tage" % bester)
|
||
p(schlimmster <= 8, "Nie zäh — höchstens %d Tage" % schlimmster)
|
||
p(Game.offene_techniken() > 1,
|
||
"Und öffnet neue Wege: %d entdeckt" % Game.offene_techniken())
|
||
|
||
|
||
## Die Probe auf den Kern: wer die Ursache trifft und die Belohnung hält,
|
||
## kommt schneller ans Ziel als wer irgendwo irgendwas einbaut.
|
||
func test_lesen_lohnt_sich() -> void:
|
||
abschnitt("Die Technik zu lesen zahlt sich aus")
|
||
_ohne_momente()
|
||
var id := "mensch_0"
|
||
Game.known.clear()
|
||
for t in TechDB.LIST:
|
||
Game.known[str(t["id"])] = 0
|
||
Game.active_mission = id
|
||
_alles_erkennen(id)
|
||
|
||
Game.systeme.clear()
|
||
var n := _bestes_system(id)
|
||
var gezielt := Game.traum_tempo(id)
|
||
|
||
# blind: dieselbe Zahl Systeme, aber in freie Slots gesetzt
|
||
Game.systeme.clear()
|
||
var frei: Array = []
|
||
for nr in range(Game.slots(id).size()):
|
||
var g := Game.slot_grund(id, nr)
|
||
if g != "schlaeft" and not Ursachen.ist_blocker(g):
|
||
frei.append(nr)
|
||
var gesetzt := 0
|
||
for tid in TechDB.ids():
|
||
if gesetzt >= n or gesetzt >= frei.size():
|
||
break
|
||
Game.cooldown.clear()
|
||
var pr := Game.system_pruefen(id, int(frei[gesetzt]), str(tid))
|
||
if pr["geht"]:
|
||
Game.systeme[id] = Game.systeme.get(id, {})
|
||
Game.systeme[id][int(frei[gesetzt])] = {"tech": str(tid), "guete": pr["guete"]}
|
||
gesetzt += 1
|
||
var blind := Game.traum_tempo(id)
|
||
p(gezielt > blind, "Gezielt gesetzt läuft der Traum schneller: %.0f statt %.0f %% je Tag" %
|
||
[gezielt, blind])
|
||
|
||
# Der Grund: die Belohnung entscheidet über die Güte
|
||
Game.systeme.clear()
|
||
var blocker_nr := -1
|
||
for nr in range(Game.slots(id).size()):
|
||
if Ursachen.ist_blocker(Game.slot_grund(id, nr)):
|
||
blocker_nr = nr
|
||
break
|
||
var b: Dictionary = Ursachen.BLOCKER[Game.slot_grund(id, blocker_nr)]
|
||
var gleich := ""
|
||
var anders := ""
|
||
for tid in TechDB.fuer_ursache(str(b["ursache"])):
|
||
if str(TechDB.by_id(str(tid))["belohnung"]) == str(b["belohnung"]):
|
||
gleich = str(tid)
|
||
elif not TechDB.entfernt_ausloeser(str(tid)):
|
||
anders = str(tid)
|
||
if gleich != "" and anders != "":
|
||
var g1: float = float(Game.system_pruefen(id, blocker_nr, gleich)["guete"])
|
||
var g2: float = float(Game.system_pruefen(id, blocker_nr, anders)["guete"])
|
||
p(g1 > g2, "Gleiche Belohnung hält besser: Güte %.2f statt %.2f" % [g1, g2])
|
||
p(Game.system_pruefen(id, blocker_nr, "schlaffenster")["geht"]
|
||
== (str(TechDB.by_id("schlaffenster")["ursache"]) == str(b["ursache"])),
|
||
"Eine Technik gegen eine andere Ursache greift hier gar nicht")
|