diff --git a/README.md b/README.md index 85c72a3..572c657 100644 --- a/README.md +++ b/README.md @@ -12,24 +12,56 @@ sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt. Steuerung: **WASD** bewegen · **Maus** umsehen · **E** interagieren · **J** Journal · **Esc** Maus freigeben. -Auf der Erde zusätzlich **Leertaste** steigen und **Umschalt** sinken. +In der Traumwelt springt **Leertaste**; auf der Erde steigt sie und **Umschalt** +lässt sinken. ## Spielschleife **Journal** (Taste **J**, in beiden Welten dabei) -* **Techniken** — zwölf echte Lösungswege. Jede nennt *Was* sie tut, *Wann* sie - passt und *Warum* sie hilft. Lernen kostet 1 Einsicht. Anwenden verbessert eine - Technik von allein; Vertiefen für 1 Einsicht geht zusätzlich. -* **Missionen** — vier Menschen mit ihrem Traum und dem, was im Weg steht. +Das Journal führt Buch — es lehrt und vergibt nichts. -**Traumwelt** — eine schwebende Insel mit einem Dorf +* **Techniken** — was du gelernt hast, mit Stufe und Erfahrung. +* **Missionen** — die Menschen, für die du dich entschieden hast, jeweils mit + ihrem **Regelwerk**. -* Vier Wesen wohnen hier: Ailen die Traumweberin, Morr der Erinnerungssammler, - Ivo der Bote, Sela die Wächterin des Tors. -* Jedes hat ein eigenes Haus und einen eigenen Tagesablauf. Die Häuser lassen - sich betreten, auf der Lichtung treffen sich die Wesen. -* **Tor zur Erde** — öffnet sich nur mit angenommener Mission. +**Regelwerk** — der Kern des Spiels + +Jede Tätigkeit im Tag eines Menschen kann eine Technik auslösen: *wenn Lena +zögert, dann Pomodoro*. Wechselt sie in die Tätigkeit, greift die Regel von +allein, solange du auf der Erde bist. Neben jeder Wahl steht die Erfolgschance +für genau diesen Moment — du siehst also vorher, ob eine Regel taugt. + +Jede Technik hält nur einen Posten — sie an einen neuen Moment zu hängen nimmt +sie vom alten weg. Mehr Momente abzudecken heißt also, mehr Techniken zu +kennen, und die gibt es nur auf der gewachsenen Insel. + +So laufen die zwei Schleifen: + + Techniken lernen → Regelwerk anpassen → Probleme lösen + Träume erfüllen → Dorf wächst → neue Wege entdecken + +Ein erfüllter Traum bringt ein Wesen mit seinem Haus ins Dorf, und in diesem +Haus wird ein neuer Weg gelehrt. Zu Beginn kennt das Dorf nur einen. + +Schlafende sind unerreichbar, dieselbe Technik wirkt bei Wiederholung +schwächer, ein Fehlversuch macht kurz dicht. Von Hand geht es weiter: nah +herangehen und **E** wirkt sofort. + +**Traumwelt** — eine schwebende Insel mit einem Dorf, das erzeugt wird + +* Fest sind nur drei Dinge: der **Hörsaal der Wege**, der **Dorfplatz** und das + **Tor zur Erde**. Alles andere entsteht beim Wachsen. +* **Die Insel beginnt kahl.** Ein Gebäude, kein Bewohner, ein einziger + gelehrter Weg. +* Jeder erfüllte Traum bringt einen Bewohner mit seinem Haus — Lage, Größe, + Farbe, Türrichtung, Einrichtung und Tagesablauf werden erzeugt, ringförmig + nach außen. **In jedem Haus wird ein Weg gelehrt, den es vorher nirgends + gab.** Ein Mensch, ein Haus, eine Technik. +* Die Insel dehnt sich mit, damit jedes Haus festen Grund hat. Das Dorf hört + nicht auf zu wachsen: die Kleinstadt schickt immer neue Menschen. +* **Schwarzes Brett** am Dorfplatz — hier hängen die Menschen aus, die nicht + weiterkommen. Mission annehmen. **Erde** — eine Kleinstadt, ein Tag dauert 6 Minuten @@ -57,26 +89,37 @@ bringen 3 Einsicht zurück. project.godot Projekt, Autoload `Game` scenes/main.tscn Einstieg - scripts/game.gd Zustand, Regeln, Uhr (Autoload) + scripts/game.gd Zustand, Regelwerk, wachsende Welt, Uhr (Autoload) scripts/technique_db.gd die zwölf Techniken mit ihren Hinweisen - scripts/human_db.gd Kleinstadt und Menschen - scripts/dream_db.gd Traumdorf und Traumwesen + scripts/human_db.gd Kleinstadt, erzeugte Wohnhäuser und Menschen + scripts/dream_db.gd Traumdorf: erzeugt Häuser, Bewohner und Lehrorte scripts/orte.gd Orte, Türen, Wege, Tätigkeiten (beide Welten) - scripts/bewohner.gd Bewohner mit Tagesablauf und Reaktionen + scripts/bewohner.gd Bewohner mit Tagesablauf, löst Regeln aus scripts/main.gd Weltenwechsel, Fokus, Interaktion scripts/player.gd Läufer in der Traumwelt, Geist auf der Erde scripts/dream_world.gd Insel mit Dorf scripts/earth_world.gd Stadt mit Tag-Nacht-Wechsel - scripts/ui.gd HUD, Journal und alle Panels + scripts/ui.gd HUD, Journal, Regelwerk, Hörsaal, Brett, Panels scripts/prims.gd Bausteine aus Primitiven ## Prüfen godot --headless --path . tests/verify.tscn # Prüfungen der Spiellogik - godot --path . tests/shots.tscn # 15 Screenshots nach user://shots/ + godot --path . tests/shots.tscn # 28 Screenshots nach user://shots/ -`verify` fährt das echte Spiel: Traumdorf mit vier Wesen und ihren Tagesabläufen, -Häuser betreten, Journal in beiden Welten, Technik lernen und vertiefen, -Verbessern allein durchs Anwenden, Mission annehmen, zur Erde reisen, +`verify` fährt das echte Spiel: leeres Traumdorf, einziehende Wesen mit ihren +Tagesabläufen, +Häuser betreten, Journal in beiden Welten, im Hörsaal lernen und vertiefen, +Verbessern allein durchs Anwenden, Mission am Schwarzen Brett annehmen, +Regelwerk bauen und einen Tag unbeaufsichtigt laufen lassen, zur Erde reisen, Tagesabläufe über einen ganzen Tag, Erfolgschancen, Mission abschließen, -Tastensteuerung. Beendet sich mit Code 1, wenn etwas fehlt. +Insel wachsen lassen, Tastensteuerung. Beendet sich mit Code 1, wenn etwas +fehlt. + +Drei Prüfungen messen statt zu behaupten. Über 24 Ausbaustufen darf sich kein +erzeugtes Gebäude mit einem anderen berühren und keines über den Inselrand +ragen; jeder Weg eines Bewohners muss in sein Zeitfenster passen — auch in +einem Dorf mit 24 Häusern und einer Stadt mit 27 Menschen. Und der erste Traum +muss mit dem einen Startweg allein zu schaffen sein: über fünf Zufallsläufe +vier bis acht Tage, ohne dass der Spieler eingreift. Ohne das hinge Loop 2 +fest, denn ohne ersten Traum gibt es keine zweite Technik. diff --git a/anforderungen.md b/anforderungen.md index 2b03834..0823389 100644 --- a/anforderungen.md +++ b/anforderungen.md @@ -2,23 +2,14 @@ Kern - Wesen aus einer Traumwelt werden auf die Erde geschickt, um Menschen zu helfen, ihre Träume zu erfüllen. -Journal (immer dabei) -- Techniken lernen -- Techniken verbessern -- Missionen annehmen +Loops +- Techniken lernen -> Systeme anpassen -> Probleme lösen +- Träume erfüllen -> Welt vergrößeren -> Techniken entdecken -Traumwelt -- Bewegen -- Orte betreten -- Traumwesen leben ihren Tag - -Erde -- Bewegen als unsichtbarer Geist -- Menschen leben ihren Tag -- Menschen lesen -- Menschen beeinflussen - -Übergreifend -- Zwischen den Welten reisen -- Zeit vergeht -- Missionen gelingen oder scheitern +Funktionen +- Bewegung: Bewegen, Springen, Geist +- Journal: Gelernte Techniken, Akzetierte Missionen +- Techniken: Lernen, Verbessern +- Missionen: Pipeline +- Menschen: Tagesablauf +- Prozedurale Generierung: Dorf, Erde diff --git a/project.godot b/project.godot index 77c3085..efc836a 100644 --- a/project.godot +++ b/project.godot @@ -14,9 +14,11 @@ Game="*res://scripts/game.gd" [display] -window/size/viewport_width=1280 -window/size/viewport_height=720 +window/size/viewport_width=1920 +window/size/viewport_height=1080 +window/size/mode=2 window/stretch/mode="canvas_items" +window/stretch/aspect="expand" [rendering] diff --git a/scripts/bewohner.gd b/scripts/bewohner.gd index 30b6917..27d103e 100644 --- a/scripts/bewohner.gd +++ b/scripts/bewohner.gd @@ -20,6 +20,7 @@ var _kopf: MeshInstance3D var _aura: MeshInstance3D var _strahl: MeshInstance3D var _path: Array = [] +var _letzte_aktivitaet: String = "" var _bob: float = 0.0 var _offset: Vector3 @@ -59,6 +60,7 @@ func setup(d: Dictionary, index: int, orte: Dictionary) -> void: var start := _routine_at(Game.time_h) place = start[0] activity = start[1] + _letzte_aktivitaet = activity global_position = Orte.center(places, place) + _offset @@ -108,6 +110,10 @@ func _process(delta: float) -> void: _visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0, 1.0 - pow(0.01, delta)) + if activity != _letzte_aktivitaet: + _letzte_aktivitaet = activity + _regel_pruefen() + var erledigt: bool = mit_mission and Game.completed.has(data["id"]) var aktiv: bool = mit_mission and Game.active_mission == data["id"] _aura.visible = aktiv or erledigt @@ -138,6 +144,20 @@ func beschriftung() -> String: return s +## Wechselt der Mensch die Tätigkeit, greift das Regelwerk der Mission von allein. +func _regel_pruefen() -> void: + if not mit_mission: + return + var res := Game.regel_ausloesen(data["id"], activity, resist_left > 0.0) + if res.is_empty(): + return + var t := TechDB.by_id(str(res["tech"])) + if res["success"]: + reagiere(true, "%s\n+%d %%" % [t["name"], int(round(float(res["gain"])))]) + else: + reagiere(false, "%s prallt ab" % t["name"]) + + ## Sichtbare Reaktion auf eine angewandte Technik. func reagiere(success: bool, text: String) -> void: var l := Prims.label(self, Vector3(0, 3.5, 0), text, 0.34, diff --git a/scripts/dream_db.gd b/scripts/dream_db.gd index 9ca4d4d..dd48fb2 100644 --- a/scripts/dream_db.gd +++ b/scripts/dream_db.gd @@ -1,25 +1,23 @@ class_name DreamDB -## Die Traumwelt: ein Dorf. Jedes Wesen hat ein Haus und einen Tagesablauf. +## Die Traumwelt: ein Dorf, das mit jedem erfüllten Traum wächst. +## Fest sind nur Hörsaal, Dorfplatz und Tor zur Erde. Jedes Haus und sein +## Bewohner werden erzeugt — der Reihe nach, immer weiter nach außen. -static var PLACES: Dictionary = { - "haus_ailen": { - "name": "Ailens Webstube", "pos": [-28.0, -22.0], "size": [12.0, 10.0], - "dir": "back", "color": Color(0.72, 0.62, 0.92), - }, - "haus_morr": { - "name": "Morrs Kammer", "pos": [28.0, -22.0], "size": [12.0, 10.0], - "dir": "back", "color": Color(0.58, 0.72, 0.90), - }, - "haus_ivo": { - "name": "Ivos Unterstand", "pos": [-28.0, 22.0], "size": [12.0, 10.0], - "dir": "forward", "color": Color(0.88, 0.66, 0.80), - }, - "haus_sela": { - "name": "Selas Turmstube", "pos": [28.0, 22.0], "size": [12.0, 10.0], - "dir": "forward", "color": Color(0.62, 0.82, 0.88), +const HAEUSER_JE_RING := 6 +const RING_ERSTER := 30.0 +const RING_ABSTAND := 24.0 + +## Häuser sitzen auf Achtelrichtungen. Die Achsen 0 (Hörsaal) und 4 (Tor) +## bleiben frei, damit der Weg vom Dorfplatz zu beiden offen bleibt. +const SLOTS := [1, 2, 3, 5, 6, 7] + +static var FEST: Dictionary = { + "hoersaal": { + "name": "Hörsaal der Wege", "pos": [0.0, 34.0], "size": [16.0, 12.0], + "dir": "forward", "color": Color(0.66, 0.72, 0.94), }, "lichtung": { - "name": "Die Lichtung", "pos": [0.0, 2.0], "size": [0.0, 0.0], + "name": "Der Dorfplatz", "pos": [0.0, 2.0], "size": [0.0, 0.0], "dir": "none", "color": Color(0.60, 0.50, 0.85), }, "tor": { @@ -28,73 +26,145 @@ static var PLACES: Dictionary = { }, } -static var LIST: Array = [ - { - "id": "ailen", - "name": "Ailen", - "rolle": "Traumweberin", - "leuchtet": true, - "farbe": Color(0.78, 0.55, 0.95), - "routine": [ - [0, "haus_ailen", "webt"], - [6, "lichtung", "ruht"], - [8, "haus_ailen", "webt"], - [14, "lichtung", "singt"], - [16, "haus_ailen", "webt"], - [20, "haus_ailen", "traeumt"], - ], - }, - { - "id": "morr", - "name": "Morr", - "rolle": "Erinnerungssammler", - "leuchtet": true, - "farbe": Color(0.45, 0.70, 0.95), - "routine": [ - [0, "haus_morr", "traeumt"], - [5, "lichtung", "sammelt"], - [9, "haus_morr", "sortiert"], - [15, "lichtung", "sammelt"], - [19, "haus_morr", "sortiert"], - [22, "haus_morr", "traeumt"], - ], - }, - { - "id": "ivo", - "name": "Ivo", - "rolle": "Bote", - "leuchtet": true, - "farbe": Color(0.95, 0.60, 0.75), - "routine": [ - [0, "haus_ivo", "traeumt"], - [4, "tor", "beobachtet"], - [8, "lichtung", "wandert"], - [10, "haus_ivo", "ruht"], - [13, "tor", "beobachtet"], - [18, "lichtung", "singt"], - [21, "haus_ivo", "traeumt"], - ], - }, - { - "id": "sela", - "name": "Sela", - "rolle": "Wächterin des Tors", - "leuchtet": true, - "farbe": Color(0.55, 0.95, 0.90), - "routine": [ - [0, "tor", "wacht"], - [7, "lichtung", "ruht"], - [9, "tor", "wacht"], - [17, "haus_sela", "ruht"], - [19, "lichtung", "singt"], - [21, "haus_sela", "traeumt"], - ], - }, +## Wird von erzeuge() gefüllt und wächst mit dem Dorf. +static var PLACES: Dictionary = FEST.duplicate(true) +static var LIST: Array = [] + +static var FESTE_NAMEN: Array = ["Ailen", "Morr", "Ivo", "Sela"] +static var FESTE_ROLLEN: Array = ["Traumweberin", "Erinnerungssammler", "Bote", + "Wächterin des Tors"] +static var SILBE_A: Array = ["Nu", "Ta", "Es", "Wre", "O", "Ma", "Si", "Te"] +static var SILBE_B: Array = ["un", "lia", "kar", "n", "vin", "lu", "rin", "nne"] +static var ROLLEN: Array = ["Lichtgießer", "Kartenzeichnerin", "Schwellenhüter", + "Fadensucherin", "Stillenzähler", "Wegbereiterin", "Klangsammler", "Nachtgärtnerin"] + +## Vier Tagesabläufe, alle mit reichlich Zeit zwischen den Stationen — +## auch ein Haus am äußeren Ring ist so bequem zu erreichen. +static var ABLAEUFE: Array = [ + [[0, "haus", "traeumt"], [6, "lichtung", "ruht"], [10, "haus", "webt"], + [14, "lichtung", "singt"], [18, "haus", "sortiert"], [21, "haus", "traeumt"]], + [[0, "haus", "traeumt"], [6, "lichtung", "sammelt"], [10, "haus", "sortiert"], + [15, "tor", "beobachtet"], [19, "haus", "webt"], [21, "haus", "traeumt"]], + [[0, "tor", "wacht"], [7, "lichtung", "ruht"], [11, "haus", "sortiert"], + [15, "tor", "wacht"], [19, "lichtung", "singt"], [21, "haus", "traeumt"]], + [[0, "haus", "traeumt"], [4, "tor", "beobachtet"], [8, "lichtung", "wandert"], + [12, "haus", "ruht"], [16, "lichtung", "singt"], [20, "haus", "traeumt"]], ] +static func haus_key(i: int) -> String: + return "haus_%d" % i + + +## Baut Orte und Bewohner für ein Dorf mit so vielen Häusern neu auf. +static func erzeuge(haeuser: int) -> void: + PLACES = FEST.duplicate(true) + LIST = [] + for i in range(maxi(haeuser, 0)): + PLACES[haus_key(i)] = _haus(i) + LIST.append(_bewohner(i)) + + +## Wie weit reicht das Dorf hinaus? +static func aeusserster_ring(haeuser: int) -> float: + if haeuser <= 0: + return 0.0 + var ring := (haeuser - 1) / HAEUSER_JE_RING + return RING_ERSTER + float(ring) * RING_ABSTAND + + +static func _haus(i: int) -> Dictionary: + var ring := i / HAEUSER_JE_RING + var winkel := float(SLOTS[i % HAEUSER_JE_RING]) * TAU / 8.0 + var r := RING_ERSTER + float(ring) * RING_ABSTAND + var x := sin(winkel) * r + var z := cos(winkel) * r + var breite := 12.0 + float(i % 2) * 2.0 + var tiefe := 10.0 + float(i % 3) + return { + "name": "%s Haus" % _besitz(_name(i)), + "pos": [x, z], + "size": [breite, tiefe], + "dir": _tuer_zur_mitte(x, z), + "color": Color.from_hsv(fmod(0.60 + float(i) * 0.137, 1.0), 0.34, 0.86), + } + + +## Die Tür zeigt zur Dorfmitte — auf die Seite, die ihr am nächsten liegt. +static func _tuer_zur_mitte(x: float, z: float) -> String: + if absf(x) > absf(z): + return "left" if x > 0.0 else "right" + return "forward" if z > 0.0 else "back" + + +static func _bewohner(i: int) -> Dictionary: + var haus := haus_key(i) + var ablauf: Array = [] + for e in ABLAEUFE[i % ABLAEUFE.size()]: + ablauf.append([e[0], haus if e[1] == "haus" else e[1], e[2]]) + return { + "id": "wesen_%d" % i, + "name": _name(i), + "rolle": _rolle(i), + "haus": haus, + "leuchtet": true, + "farbe": Color.from_hsv(fmod(0.72 + float(i) * 0.161, 1.0), 0.55, 0.95), + "routine": ablauf, + } + + +static func _name(i: int) -> String: + if i < FESTE_NAMEN.size(): + return str(FESTE_NAMEN[i]) + var j := i - FESTE_NAMEN.size() + return str(SILBE_A[j % SILBE_A.size()]) + str(SILBE_B[(j / SILBE_A.size()) % SILBE_B.size()]) + + +## Genitiv: "Ailens", aber "Morrs" und "Esas'" — Apostroph nur nach Zischlaut. +static func _besitz(n: String) -> String: + var letzter := n.substr(n.length() - 1).to_lower() + return n + "'" if ["s", "z", "x", "ß"].has(letzter) else n + "s" + + +static func _rolle(i: int) -> String: + if i < FESTE_ROLLEN.size(): + return str(FESTE_ROLLEN[i]) + return str(ROLLEN[(i - FESTE_ROLLEN.size()) % ROLLEN.size()]) + + static func by_id(id: String) -> Dictionary: for w in LIST: if w["id"] == id: return w return {} + + +# ------------------------------------------------------------------ Lehrorte + +## Der Hörsaal lehrt den ersten Weg. Jedes Haus bringt einen weiteren mit. +static func techniken_von(place_key: String) -> Array: + var nr := -1 + if place_key == "hoersaal": + nr = 0 + elif place_key.begins_with("haus_"): + nr = int(place_key.substr(5)) + 1 + if nr < 0 or nr >= TechDB.LIST.size(): + return [] + return [TechDB.LIST[nr]["id"]] + + +## An welchem Ort wird diese Technik gelehrt? "" wenn nirgends. +static func ort_von_technik(tech_id: String) -> String: + for i in range(TechDB.LIST.size()): + if TechDB.LIST[i]["id"] == tech_id: + return "hoersaal" if i == 0 else haus_key(i - 1) + return "" + + +## Alle Orte, an denen im aktuellen Dorf gelehrt wird. +static func lehrorte() -> Array: + var out: Array = ["hoersaal"] + for i in range(LIST.size()): + if not techniken_von(haus_key(i)).is_empty(): + out.append(haus_key(i)) + return out diff --git a/scripts/dream_world.gd b/scripts/dream_world.gd index b592e6d..2762fed 100644 --- a/scripts/dream_world.gd +++ b/scripts/dream_world.gd @@ -13,6 +13,8 @@ func _ready() -> void: _environment() _insel() _haeuser() + _lehrpulte() + _schwarzes_brett() _lichtung() _tor_zur_erde() _wesen() @@ -52,9 +54,10 @@ func _environment() -> void: func _insel() -> void: - Prims.cyl(self, Vector3(0, -1.03, 0), 52.0, 2.0, Color(0.48, 0.40, 0.64)) - Prims.cyl(self, Vector3(0, -4.0, 0), 40.0, 4.0, Color(0.34, 0.26, 0.50), false) - Prims.cyl(self, Vector3(0, -9.0, 0), 22.0, 6.0, Color(0.24, 0.17, 0.38), false) + var radius := Game.insel_radius() + Prims.cyl(self, Vector3(0, -1.03, 0), radius, 2.0, Color(0.48, 0.40, 0.64)) + Prims.cyl(self, Vector3(0, -4.0, 0), radius * 0.77, 4.0, Color(0.34, 0.26, 0.50), false) + Prims.cyl(self, Vector3(0, -9.0, 0), radius * 0.42, 6.0, Color(0.24, 0.17, 0.38), false) # Ringweg, auf dem die Wesen gehen for i in range(36): @@ -67,7 +70,7 @@ func _insel() -> void: rng.seed = 7 for i in range(34): var a := rng.randf() * TAU - var r := rng.randf_range(18.0, 48.0) + var r := rng.randf_range(18.0, Game.insel_radius() - 4.0) var p := Vector3(cos(a) * r, rng.randf_range(5.0, 24.0), sin(a) * r) var s := rng.randf_range(0.6, 2.0) var c := Color(0.6 + rng.randf() * 0.3, 0.5 + rng.randf() * 0.4, 1.0) @@ -80,7 +83,7 @@ func _haeuser() -> void: for key in DreamDB.PLACES: var pl: Dictionary = DreamDB.PLACES[key] var s: Array = pl["size"] - if s[0] <= 0.0: + if s[0] <= 0.0 or not Game.ort_offen(key): continue var c := Orte.center(DreamDB.PLACES, key) var groesse := Vector3(s[0], 3.2, s[1]) @@ -106,33 +109,103 @@ func _haeuser() -> void: func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void: + if key == "hoersaal": + _hoersaal_einrichten(c, groesse, farbe, key) + return var w := groesse.x * 0.5 - 1.8 var h := groesse.z * 0.5 - 1.8 - # Schlafnest hat jedes Haus Prims.cyl(self, c + Vector3(-w, 0.3, -h), 1.4, 0.5, farbe.darkened(0.2), false) - match key: - "haus_ailen": # Webstuhl + match int(key.substr(5)) % 4: + 0: # Webstuhl Prims.box(self, c + Vector3(w, 1.1, 0), Vector3(0.3, 2.2, 3.0), farbe.darkened(0.45)) for i in range(5): Prims.box(self, c + Vector3(w, 1.1, -1.2 + i * 0.6), Vector3(0.1, 1.8, 0.06), Color(0.9, 0.85, 1.0), false, 1.4) - "haus_morr": # Regale voller Fundstücke + 1: # Regale voller Fundstücke for i in range(3): Prims.box(self, c + Vector3(w, 0.6 + i * 0.7, 0), Vector3(1.2, 0.12, 4.0), farbe.darkened(0.45)) for j in range(4): Prims.box(self, c + Vector3(w, 0.85 + i * 0.7, -1.5 + j * 1.0), Vector3(0.4, 0.4, 0.4), Color(0.8, 0.9, 1.0), false, 0.8) - "haus_ivo": # Karten und ein Fernrohr + 2: # Karten und ein Fernrohr Prims.box(self, c + Vector3(w, 0.75, 0), Vector3(1.6, 0.12, 2.6), farbe.darkened(0.45)) var rohr := Prims.cyl(self, c + Vector3(w, 1.6, 0), 0.18, 2.0, Color(0.85, 0.9, 1.0), false, 0.6) rohr.rotation.x = 0.9 - "haus_sela": # Schale mit Licht + _: # Schale mit Licht Prims.cyl(self, c + Vector3(w, 0.5, 0), 1.0, 1.0, farbe.darkened(0.4), false) Prims.sphere(self, c + Vector3(w, 1.4, 0), 0.55, Color(0.8, 1.0, 1.0), 2.0) +## Bankreihen mit Mittelgang, Tafel an der Rückwand. +func _hoersaal_einrichten(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void: + var richtung: Vector3 = Orte.DIR_VEC[DreamDB.PLACES[key]["dir"]] * -1.0 + var quer := absf(richtung.z) > 0.0 + var bank := ((groesse.x if quer else groesse.z) - 5.0) * 0.5 + var laengs: Vector3 = Vector3(1, 0, 0) if quer else Vector3(0, 0, 1) + for i in range(3): + var tief := -3.6 + float(i) * 1.8 + for sx: float in [-1.0, 1.0]: + var mitte: Vector3 = c + richtung * tief + laengs * (sx * (bank * 0.5 + 1.0)) + var sitz: Vector3 = Vector3(bank, 0.25, 0.9) if quer else Vector3(0.9, 0.25, bank) + var lehne: Vector3 = Vector3(bank, 0.9, 0.15) if quer else Vector3(0.15, 0.9, bank) + Prims.box(self, mitte + Vector3(0, 0.5, 0), sitz, farbe.darkened(0.35)) + Prims.box(self, mitte + richtung * 0.7 + Vector3(0, 0.9, 0), lehne, + farbe.darkened(0.45)) + var tafel: Vector3 = Vector3(groesse.x - 5.0, 2.0, 0.15) if quer \ + else Vector3(0.15, 2.0, groesse.z - 5.0) + Prims.box(self, c + richtung * 4.4 + Vector3(0, 1.4, 0), tafel, Color(0.16, 0.18, 0.30)) + + +## Vorn in jedem offenen Lehrort steht ein Pult. +func _lehrpulte() -> void: + for key in DreamDB.lehrorte(): + if not Game.ort_offen(key) or DreamDB.techniken_von(key).is_empty(): + continue + var pl: Dictionary = DreamDB.PLACES[key] + var tief: float = float(pl["size"][1]) + var innen: Vector3 = Orte.DIR_VEC[pl["dir"]] * -1.0 + var c := Orte.center(DreamDB.PLACES, key) + var pos := c + innen * (tief * 0.5 - 2.6) + var farbe: Color = pl["color"] + Prims.box(self, pos + Vector3(0, 0.55, 0), Vector3(2.4, 1.1, 0.9), farbe.darkened(0.35)) + Prims.box(self, pos + Vector3(0, 1.18, 0), Vector3(2.8, 0.16, 1.2), farbe.darkened(0.2)) + Prims.sphere(self, pos + Vector3(0, 1.7, 0), 0.34, Color(0.85, 0.92, 1.0), 2.0) + var lehrt: Array = DreamDB.techniken_von(key) + Prims.label(self, pos + Vector3(0, 2.6, 0), + TechDB.by_id(str(lehrt[0]))["name"], 0.42, Color(0.85, 0.92, 1)) + var n := _marke(pos - innen * 1.8 + Vector3(0, 1.4, 0), "lehre", str(pl["name"])) + n.set_meta("arg", key) + + +## An der Lichtung: hier hängen die Menschen aus, die Hilfe brauchen. +func _schwarzes_brett() -> void: + var pos := Orte.center(DreamDB.PLACES, "lichtung") + Vector3(-6.0, 0, 9.0) + for s: float in [-1.4, 1.4]: + Prims.cyl(self, pos + Vector3(s, 1.3, 0), 0.16, 2.6, Color(0.45, 0.38, 0.60)) + Prims.box(self, pos + Vector3(0, 2.2, 0), Vector3(3.6, 2.4, 0.18), Color(0.18, 0.16, 0.26)) + for i in range(HumanDB.LIST.size()): + var x := -1.2 + float(i % 2) * 2.4 + var y := 2.8 - float(i / 2) * 1.1 + Prims.box(self, pos + Vector3(x, y, 0.12), Vector3(1.1, 0.8, 0.04), + Color(0.92, 0.90, 0.80), false, 0.5) + Prims.label(self, pos + Vector3(0, 4.2, 0), "Schwarzes Brett", 0.5, Color(1, 0.95, 0.85)) + _marke(pos + Vector3(0, 1.6, 1.2), "brett", "Schwarzes Brett") + + +## Unsichtbarer Punkt, an dem die Interaktion hängt. +func _marke(pos: Vector3, kind: String, titel: String) -> Node3D: + var n := Node3D.new() + n.position = pos + add_child(n) + n.add_to_group("interactable") + n.set_meta("kind", kind) + n.set_meta("arg", "") + n.set_meta("titel", titel) + return n + + func _lichtung() -> void: var c := Orte.center(DreamDB.PLACES, "lichtung") Prims.cyl(self, c + Vector3(0, 0.04, 0), 11.0, 0.12, Color(0.60, 0.52, 0.78), false) @@ -142,13 +215,15 @@ func _lichtung() -> void: var p := c + Vector3(cos(a) * 8.5, 0, sin(a) * 8.5) Prims.cyl(self, p + Vector3(0, 1.6, 0), 0.28, 3.2, Color(0.72, 0.64, 0.88)) Prims.sphere(self, p + Vector3(0, 3.6, 0), 0.42, Color(0.75, 0.85, 1.0), 2.0) - Prims.label(self, c + Vector3(0, 5.2, 0), DreamDB.PLACES["lichtung"]["name"], 0.6, + Prims.label(self, c + Vector3(0, 5.2, 0), str(DreamDB.PLACES["lichtung"]["name"]), 0.6, Color(1, 0.95, 0.85)) func _tor_zur_erde() -> void: var pos := Orte.center(DreamDB.PLACES, "tor") - Prims.cyl(self, pos + Vector3(0, 0.3, 0), 5.5, 0.6, Color(0.35, 0.30, 0.50)) + # flach und ohne Kollision wie die Lichtung — eine 0,6 hohe Stufe hätte den + # Spieler vor dem Tor stehen lassen + Prims.cyl(self, pos + Vector3(0, 0.05, 0), 5.5, 0.1, Color(0.35, 0.30, 0.50), false) for i in range(10): var a := PI * float(i) / 9.0 Prims.box(self, pos + Vector3(cos(a) * 4.2, sin(a) * 4.2 + 0.6, 0), @@ -158,17 +233,14 @@ func _tor_zur_erde() -> void: scheibe.rotation.x = PI * 0.5 Prims.label(self, pos + Vector3(0, 6.4, 0), "Tor zur Erde", 0.8, Color(0.7, 0.95, 1)) - var n := Node3D.new() - n.position = pos + Vector3(0, 1.6, 0) - add_child(n) - n.add_to_group("interactable") - n.set_meta("kind", "erde") - n.set_meta("arg", "") - n.set_meta("titel", "Tor zur Erde") + _marke(pos + Vector3(0, 1.6, 0), "erde", "Tor zur Erde") +## Am Anfang ist das Dorf leer; mit jedem erfüllten Traum zieht ein Wesen ein. func _wesen() -> void: for i in range(DreamDB.LIST.size()): + if not Game.wesen_eingezogen(i): + continue var w := Bewohner.new() add_child(w) w.setup(DreamDB.LIST[i], i, DreamDB.PLACES) diff --git a/scripts/earth_world.gd b/scripts/earth_world.gd index caa335a..51f055a 100644 --- a/scripts/earth_world.gd +++ b/scripts/earth_world.gd @@ -44,7 +44,14 @@ func _environment() -> void: func _boden() -> void: - Prims.box(self, Vector3(0, -0.53, 0), Vector3(240, 1, 240), Color(0.36, 0.52, 0.30)) + var weite := 240.0 + for key in HumanDB.PLACES: + var pl: Dictionary = HumanDB.PLACES[key] + var p: Array = pl["pos"] + var s2: Array = pl["size"] + weite = maxf(weite, (absf(float(p[0])) + float(s2[0])) * 2.6) + weite = maxf(weite, (absf(float(p[1])) + float(s2[1])) * 2.6) + Prims.box(self, Vector3(0, -0.53, 0), Vector3(weite, 1, weite), Color(0.36, 0.52, 0.30)) # Ringweg, auf dem die Menschen gehen for i in range(36): var a := TAU * float(i) / 36.0 @@ -54,9 +61,9 @@ func _boden() -> void: func _orte() -> void: for key in HumanDB.PLACES: - if key == "park": - continue var pl: Dictionary = HumanDB.PLACES[key] + if float(pl["size"][0]) <= 0.0: + continue var c := Orte.center(HumanDB.PLACES, key) var s: Array = pl["size"] var groesse := Vector3(s[0], 2.3, s[1]) @@ -84,7 +91,7 @@ func _orte() -> void: func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void: var h := groesse.z * 0.5 - 1.6 var w := groesse.x * 0.5 - 1.6 - if key.begins_with("haus") or key == "wohnheim": + if key.begins_with("heim"): Prims.box(self, c + Vector3(-w + 1.0, 0.35, -h + 1.2), Vector3(2.0, 0.7, 3.4), Color(0.85, 0.85, 0.90)) # Bett Prims.box(self, c + Vector3(w - 1.0, 0.75, 0), Vector3(1.6, 0.15, 2.6), @@ -110,6 +117,12 @@ func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> vo Prims.box(self, c + Vector3(0, 0.5, -2.0 + i * 2.2), Vector3(groesse.x - 4.0, 0.2, 0.8), Color(0.66, 0.62, 0.58)) Prims.box(self, c + Vector3(0, 1.1, -4.0), Vector3(5.0, 1.6, 0.15), Color(0.15, 0.25, 0.20)) + elif key == "werkstatt": + Prims.box(self, c + Vector3(0, 0.55, 0), Vector3(groesse.x - 4.0, 1.1, 1.4), + Color(0.55, 0.50, 0.48)) + for i in range(3): + Prims.box(self, c + Vector3(-2.0 + i * 2.0, 1.5, -h + 0.6), Vector3(0.5, 0.9, 0.15), + Color(0.72, 0.68, 0.60), false) func _park() -> void: diff --git a/scripts/game.gd b/scripts/game.gd index fbfa5b3..c538d7c 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -15,6 +15,7 @@ var known: Dictionary = {} # tech_id -> xp var active_mission: String = "" # human_id oder "" var progress: Dictionary = {} # human_id -> 0..100 var used: Dictionary = {} # "human:tech" -> Anzahl +var pipeline: Dictionary = {} # human_id -> {activity -> tech_id} var completed: Array = [] var time_h: float = 7.0 var day: int = 1 @@ -23,6 +24,7 @@ var teleport_lock: float = 0.0 func _ready() -> void: _setup_input() + welt_erzeugen() func _setup_input() -> void: @@ -50,6 +52,15 @@ func advance_time(delta: float) -> void: while time_h >= 24.0: time_h -= 24.0 day += 1 + _gewoehnung_klingt_ab() + + +## Abstand macht eine Technik wieder wirksam: über Nacht verfällt die Hälfte +## der Gewöhnung. Wer täglich denselben Weg geht, hält ihn brauchbar — wer ihn +## an einem Tag immer wieder auffährt, stumpft ihn ab. +func _gewoehnung_klingt_ab() -> void: + for key in used: + used[key] = int(used[key]) / 2 func time_string() -> String: @@ -58,6 +69,54 @@ func time_string() -> String: return "Tag %d · %02d:%02d" % [day, h, m] +# ------------------------------------------------------------ Wachsende Welt +# Das Traumdorf wird erzeugt. Fest sind nur Hörsaal, Dorfplatz und Tor. +# Jeder erfüllte Traum bringt einen Bewohner mit seinem Haus, und jedes Haus +# lehrt eine Technik, die es vorher nirgends gab. + +const INSEL_GRUND := 46.0 +const OFFENE_MISSIONEN := 3 + + +func haeuser() -> int: + return completed.size() + + +func insel_radius() -> float: + var ring := DreamDB.aeusserster_ring(haeuser()) + return maxf(INSEL_GRUND, ring + 14.0) + + +## Baut Traumdorf und Menschheit auf den Stand der erfüllten Träume. +func welt_erzeugen() -> void: + DreamDB.erzeuge(haeuser()) + HumanDB.erzeuge(completed.size() + OFFENE_MISSIONEN) + + +## Das Traumdorf beginnt leer. Mit jedem erfüllten Traum zieht ein Wesen ein. +func wesen_eingezogen(index: int) -> bool: + return index < haeuser() + + +## Steht dieser Ort schon auf der Insel? Erzeugt wird nur, was schon da ist. +func ort_offen(place_key: String) -> bool: + return DreamDB.PLACES.has(place_key) + + +## Ist die Technik überhaupt schon irgendwo zu finden? +func technik_offen(tech_id: String) -> bool: + var ort := DreamDB.ort_von_technik(tech_id) + return ort != "" and ort_offen(ort) + + +func offene_techniken() -> int: + var n := 0 + for t in TechDB.LIST: + if technik_offen(t["id"]): + n += 1 + return n + + # ---------------------------------------------------------------- Fertigkeiten func knows(tech_id: String) -> bool: @@ -86,6 +145,10 @@ func xp_to_next(tech_id: String) -> String: func learn(tech_id: String) -> bool: if known.has(tech_id): return false + if not technik_offen(tech_id): + notified.emit("Diesen Weg kennt im Dorf noch niemand. Erfülle einen Traum.", + Color(1, 0.8, 0.5)) + return false if einsicht < 1: notified.emit("Dafür fehlt dir Einsicht. Hilf erst einem Menschen.", Color(1, 0.6, 0.4)) return false @@ -136,12 +199,107 @@ func accept_mission(human_id: String) -> bool: return true +## Angenommen ist, wer schon einmal im Auftragsbuch stand. +func angenommen(human_id: String) -> bool: + return progress.has(human_id) + + func mission_progress(human_id: String) -> float: return progress.get(human_id, 0.0) +## Ein erfüllter Traum lässt das Dorf wachsen: ein Wesen zieht mit seinem +## Haus ein, und in diesem Haus wird ein neuer Weg gelehrt. +func _insel_waechst() -> void: + welt_erzeugen() + var i := haeuser() - 1 + if i < 0 or i >= DreamDB.LIST.size(): + return + var w: Dictionary = DreamDB.LIST[i] + notified.emit("%s, %s, ist ins Traumdorf gezogen." % [w["name"], w["rolle"]], + Color(0.85, 0.75, 1)) + var neu: Array = DreamDB.techniken_von(DreamDB.haus_key(i)) + if neu.is_empty(): + return + notified.emit("In %s wird ein neuer Weg gelehrt: %s." % + [DreamDB.PLACES[DreamDB.haus_key(i)]["name"], TechDB.by_id(str(neu[0]))["name"]], + Color(0.75, 0.95, 1)) + + +# ------------------------------------------------------------------- Regelwerk +# Eine Regel ist ein Wenn-Dann: tut der Mensch etwas Bestimmtes, greift die +# zugeordnete Technik von allein. Damit automatisiert der Spieler den Fortschritt. + +func regeln(human_id: String) -> Dictionary: + return pipeline.get(human_id, {}) + + +func regel_fuer(human_id: String, activity: String) -> String: + return regeln(human_id).get(activity, "") + + +func regel_anzahl(human_id: String) -> int: + return regeln(human_id).size() + + +## Wo hängt diese Technik im Regelwerk gerade? "" wenn nirgends. +func regel_moment(human_id: String, tech_id: String) -> String: + for activity in regeln(human_id): + if regeln(human_id)[activity] == tech_id: + return activity + return "" + + +## Leere tech_id löscht die Regel. Eine Technik hält immer nur einen Posten — +## sie an einen neuen Moment zu hängen nimmt sie vom alten weg. Das zwingt zur +## Wahl und macht das Lernen weiterer Techniken erst wertvoll. +func regel_setzen(human_id: String, activity: String, tech_id: String) -> void: + if not pipeline.has(human_id): + pipeline[human_id] = {} + var r: Dictionary = pipeline[human_id] + if tech_id == "": + r.erase(activity) + else: + var vorher := regel_moment(human_id, tech_id) + if vorher != "" and vorher != activity: + r.erase(vorher) + notified.emit("%s hängt jetzt an »%s« statt an »%s«." % + [TechDB.by_id(tech_id)["name"], Orte.text(activity), Orte.text(vorher)], + Color(0.85, 0.9, 1)) + r[activity] = tech_id + state_changed.emit() + + +## Der Mensch wechselt in eine Tätigkeit: greift dafür eine Regel? +func regel_ausloesen(human_id: String, activity: String, resisting: bool) -> Dictionary: + if human_id != active_mission: + return {} + var tech_id := regel_fuer(human_id, activity) + if tech_id == "" or not known.has(tech_id): + return {} + var res := apply_technique(tech_id, human_id, activity, resisting) + if not res["ok"]: + return {} + res["tech"] = tech_id + var t := TechDB.by_id(tech_id) + if res["success"]: + notified.emit("Regel greift — %s bei »%s« (+%d %%)" % + [t["name"], Orte.text(activity), int(round(float(res["gain"])))], + Color(0.6, 1, 0.7)) + else: + notified.emit("Regel läuft ins Leere — %s bei »%s« (%d %% Chance)" % + [t["name"], Orte.text(activity), int(round(float(res["chance"]) * 100.0))], + Color(1, 0.7, 0.5)) + return res + + # ----------------------------------------------------- Technik auf Mensch anwenden +## Schlafende erreicht keine Technik — weder von Hand noch über eine Regel. +func erreichbar(activity: String) -> bool: + return activity != "schlaeft" + + ## Bewertet eine Technik in genau dieser Situation, ohne sie anzuwenden. func evaluate(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary: var t := TechDB.by_id(tech_id) @@ -151,7 +309,7 @@ func evaluate(tech_id: String, human_id: String, activity: String, resisting: bo "chance": 0.0, "tag": false, "moment": false, "blocked": false, "grund": "", "skill": skill, } - if activity == "schlaeft": + if not erreichbar(activity): res["blocked"] = true res["grund"] = "Sie oder er schläft. Techniken erreichen nur wache Menschen." return res @@ -214,6 +372,7 @@ func apply_technique(tech_id: String, human_id: String, activity: String, resist mission_done.emit(human_id) notified.emit("%s hat es geschafft. (+3 Einsicht)" % HumanDB.by_id(human_id)["name"], Color(1, 0.85, 0.4)) + _insel_waechst() state_changed.emit() return { diff --git a/scripts/human_db.gd b/scripts/human_db.gd index b01af66..a8dcd3c 100644 --- a/scripts/human_db.gd +++ b/scripts/human_db.gd @@ -1,9 +1,13 @@ class_name HumanDB -## Die Kleinstadt auf der Erde und die Menschen mit ihren Tagesabläufen. +## Die Kleinstadt auf der Erde. Die Gebäude im Ring um den Park stehen fest; +## Wohnhäuser und ihre Bewohner kommen dazu, solange Menschen Hilfe brauchen. + +const HAEUSER_JE_RING := 8 +const RING_ERSTER := 62.0 +const RING_ABSTAND := 16.0 ## Gebäude liegen im Ring um den Park. Alle Türen zeigen zur Mitte. -## pos = Standplatz drinnen, size = Grundfläche, dir = Richtung der Türlücke. -static var PLACES: Dictionary = { +static var FEST: Dictionary = { "uni": { "name": "Hochschule", "pos": [-28.0, -27.0], "size": [14.0, 10.0], "dir": "back", "color": Color(0.72, 0.70, 0.66), @@ -20,125 +24,184 @@ static var PLACES: Dictionary = { "name": "Bäckerei Keller", "pos": [-31.0, 0.0], "size": [10.0, 12.0], "dir": "right", "color": Color(0.85, 0.72, 0.50), }, - "haus_tobias": { - "name": "Wohnung Frank", "pos": [31.0, 0.0], "size": [10.0, 12.0], + "werkstatt": { + "name": "Werkstatt Loos", "pos": [31.0, 0.0], "size": [10.0, 12.0], "dir": "left", "color": Color(0.70, 0.58, 0.55), }, - "haus_lena": { - "name": "Atelier Vogt", "pos": [-28.0, 27.0], "size": [12.0, 10.0], - "dir": "forward", "color": Color(0.66, 0.58, 0.72), - }, - "haus_mira": { - "name": "Wohnheim", "pos": [0.0, 27.0], "size": [12.0, 10.0], - "dir": "forward", "color": Color(0.60, 0.68, 0.62), - }, - "haus_bruno": { - "name": "Haus Keller", "pos": [28.0, 27.0], "size": [12.0, 10.0], - "dir": "forward", "color": Color(0.78, 0.66, 0.58), - }, "park": { "name": "Stadtpark", "pos": [0.0, 4.0], "size": [0.0, 0.0], "dir": "none", "color": Color(0.45, 0.65, 0.38), }, } -static var LIST: Array = [ +static var PLACES: Dictionary = FEST.duplicate(true) +static var LIST: Array = [] + +## Arbeitsplätze, die ein erzeugter Mensch bekommen kann. +static var ARBEIT: Array = ["uni", "cafe", "buero", "baeckerei", "werkstatt"] + +static var VORNAMEN: Array = ["Lena", "Bruno", "Mira", "Tobias", "Halim", "Jorunn", + "Pia", "Anselm", "Ceyda", "Rune", "Malte", "Ida", "Vito", "Nour", "Greta", "Kasper"] +static var NACHNAMEN: Array = ["Vogt", "Keller", "Sahin", "Frank", "Berg", "Ostermann", + "Ruiz", "Lind", "Aslan", "Weiss", "Petit", "Novak"] +static var BERUFE: Array = ["Illustratorin", "Bäcker", "Studentin", "Sachbearbeiter", + "Pfleger", "Tischlerin", "Lehrer", "Programmiererin", "Fahrer", "Archivarin", + "Köchin", "Musiker"] + +## Anliegen sind echte Klemmen, keine erzeugten Sätze — sie tragen den Sinn +## des Spiels. Erzeugt werden Person, Wohnort und Tagesablauf drumherum. +static var ANLIEGEN: Array = [ { - "id": "lena", - "name": "Lena Vogt", - "alter": 29, - "beruf": "Illustratorin", "traum": "Ihr Bilderbuch endlich zu Ende zeichnen.", "problem": "Sie schiebt die letzten zwölf Seiten seit acht Monaten vor sich her. Jeder Entwurf kommt ihr zu schlecht vor, also fängt sie lieber gar nicht erst an.", "tags": ["prokrastination", "selbstzweifel", "ablenkung"], - "mission": true, - "farbe": Color(0.85, 0.45, 0.55), - "routine": [ - [0, "haus_lena", "schlaeft"], - [7, "haus_lena", "morgenroutine"], - [8, "cafe", "essen"], - [10, "haus_lena", "zoegert"], - [12, "haus_lena", "arbeitet"], - [13, "park", "pause"], - [15, "haus_lena", "arbeitet"], - [18, "park", "unterwegs"], - [20, "haus_lena", "aufraeumen"], - [21, "haus_lena", "gruebelt"], - [22, "haus_lena", "abendroutine"], - [23, "haus_lena", "schlaeft"], - ], }, { - "id": "bruno", - "name": "Bruno Keller", - "alter": 54, - "beruf": "Bäcker", "traum": "Wieder Klavier spielen wie mit zwanzig.", "problem": "Das Klavier steht zugestellt im Flur. Nach der Frühschicht ist er zu müde, um auch nur den Deckel zu öffnen.", "tags": ["gewohnheit", "erschoepfung", "ablenkung"], - "mission": true, - "farbe": Color(0.55, 0.45, 0.30), - "routine": [ - [0, "haus_bruno", "schlaeft"], - [3, "baeckerei", "arbeitet"], - [10, "cafe", "pause"], - [12, "haus_bruno", "essen"], - [13, "haus_bruno", "ruht"], - [15, "haus_bruno", "zoegert"], - [16, "park", "unterwegs"], - [18, "haus_bruno", "muede"], - [20, "haus_bruno", "abendroutine"], - [21, "haus_bruno", "schlaeft"], - ], }, { - "id": "mira", - "name": "Mira Sahin", - "alter": 22, - "beruf": "Studentin", - "traum": "Ihre Abschlussarbeit vortragen, ohne zu erstarren.", + "traum": "Die Abschlussarbeit vortragen, ohne zu erstarren.", "problem": "Die Arbeit ist längst fertig. Aber vor jedem Vortrag wird ihr schwarz vor Augen, und sie meldet sich wieder ab.", "tags": ["angst", "ueberforderung", "selbstzweifel"], - "mission": true, - "farbe": Color(0.35, 0.60, 0.85), - "routine": [ - [0, "haus_mira", "schlaeft"], - [8, "haus_mira", "morgenroutine"], - [9, "uni", "arbeitet"], - [13, "cafe", "angespannt"], - [15, "uni", "arbeitet"], - [17, "uni", "wartet"], - [18, "haus_mira", "gruebelt"], - [20, "park", "unterwegs"], - [21, "haus_mira", "ruht"], - [22, "haus_mira", "abendroutine"], - [23, "haus_mira", "schlaeft"], - ], }, { - "id": "tobias", - "name": "Tobias Frank", - "alter": 41, - "beruf": "Sachbearbeiter", - "traum": "Abends wieder Zeit für seine Tochter haben.", + "traum": "Abends wieder Zeit für die Tochter haben.", "problem": "Er sagt zu jeder Bitte ja und sitzt dafür jeden Abend bis acht im Büro. Zu Hause ist er nur noch müde.", "tags": ["stress", "grenzen", "erschoepfung"], - "mission": true, - "farbe": Color(0.40, 0.55, 0.45), - "routine": [ - [0, "haus_tobias", "schlaeft"], - [6, "haus_tobias", "morgenroutine"], - [7, "buero", "arbeitet"], - [12, "cafe", "essen"], - [14, "buero", "arbeitet"], - [19, "buero", "angespannt"], - [20, "haus_tobias", "essen"], - [21, "haus_tobias", "muede"], - [23, "haus_tobias", "abendroutine"], - ], + }, + { + "traum": "Endlich wieder durchschlafen.", + "problem": "Der Feierabend franst aus, das Handy leuchtet bis zwei. Morgens steht er zerschlagen auf und trinkt sich wach.", + "tags": ["erschoepfung", "gesundheit", "gewohnheit"], + }, + { + "traum": "Den alten Freundeskreis zurückholen.", + "problem": "Sie wartet seit Monaten, dass sich jemand meldet, und meldet sich selbst nie. Jede Einladung erscheint ihr aufdringlich.", + "tags": ["einsamkeit", "selbstzweifel"], + }, + { + "traum": "Die Werkstatt in der Garage wieder aufmachen.", + "problem": "Alles liegt bereit, aber der erste Handgriff wirkt jedes Mal wie ein Berg. Am Ende räumt er lieber auf.", + "tags": ["prokrastination", "ueberforderung"], + }, + { + "traum": "Vor Publikum sprechen, ohne dass die Hände zittern.", + "problem": "Schon Tage vorher liegt ihr der Termin im Magen. Kurz davor wird die Luft knapp und der Kopf leer.", + "tags": ["angst", "stress"], + }, + { + "traum": "Nach dem Rückschlag noch einmal anfangen.", + "problem": "Seit die Prüfung schiefging, redet sie mit sich, wie sie mit niemandem reden würde. Jeder neue Versuch fühlt sich sinnlos an.", + "tags": ["rueckschlag", "selbstzweifel"], + }, + { + "traum": "Drei Abende in der Woche für sich behalten.", + "problem": "Der Kalender füllt sich mit fremden Anliegen, weil ein Nein unhöflich wirkt. Am Ende bleibt nichts übrig.", + "tags": ["grenzen", "stress", "ueberforderung"], }, ] +## Tagesabläufe: Platzhalter werden durch Wohnung und Arbeitsplatz ersetzt. +## Tagesabläufe: Platzhalter werden durch Wohnung und Arbeitsplatz ersetzt. +## Die Reihenfolge passt zu den Anliegen — wer aufschiebt, arbeitet zu Hause. +static var ABLAEUFE: Array = [ + [[0, "heim", "schlaeft"], [7, "heim", "zoegert"], [10, "heim", "arbeitet"], + [13, "park", "pause"], [15, "heim", "arbeitet"], [18, "cafe", "wartet"], + [20, "heim", "zoegert"], [22, "heim", "gruebelt"], [23, "heim", "abendroutine"]], + [[0, "heim", "schlaeft"], [4, "arbeit", "arbeitet"], [10, "cafe", "pause"], + [12, "heim", "essen"], [14, "heim", "ruht"], [16, "heim", "zoegert"], + [18, "park", "unterwegs"], [20, "heim", "muede"], [22, "heim", "abendroutine"]], + [[0, "heim", "schlaeft"], [8, "heim", "morgenroutine"], [10, "arbeit", "arbeitet"], + [13, "cafe", "angespannt"], [15, "arbeit", "wartet"], [18, "heim", "gruebelt"], + [20, "park", "unterwegs"], [22, "heim", "abendroutine"]], + [[0, "heim", "schlaeft"], [6, "heim", "morgenroutine"], [8, "arbeit", "arbeitet"], + [12, "cafe", "essen"], [14, "arbeit", "arbeitet"], [19, "arbeit", "angespannt"], + [21, "heim", "essen"], [23, "heim", "muede"]], + [[0, "heim", "schlaeft"], [7, "heim", "morgenroutine"], [9, "arbeit", "arbeitet"], + [12, "cafe", "essen"], [14, "arbeit", "arbeitet"], [17, "park", "unterwegs"], + [19, "heim", "aufraeumen"], [21, "heim", "gruebelt"], [23, "heim", "abendroutine"]], +] + + +static func heim_key(i: int) -> String: + return "heim_%d" % i + + +## Baut Wohnhäuser und Menschen für so viele Bewohner neu auf. +static func erzeuge(menschen: int) -> void: + PLACES = FEST.duplicate(true) + LIST = [] + for i in range(maxi(menschen, 0)): + PLACES[heim_key(i)] = _heim(i) + LIST.append(_mensch(i)) + + +static func _heim(i: int) -> Dictionary: + var ring := i / HAEUSER_JE_RING + var winkel := float(i % HAEUSER_JE_RING) * TAU / float(HAEUSER_JE_RING) + 0.39 + var r := RING_ERSTER + float(ring) * RING_ABSTAND + var x := sin(winkel) * r + var z := cos(winkel) * r + return { + "name": "%s %s" % [_nachname(i), "Weg %d" % (i + 1)], + "pos": [x, z], + "size": [12.0 + float(i % 3) * 2.0, 10.0], + "dir": _tuer_zur_mitte(x, z), + "color": Color.from_hsv(fmod(0.08 + float(i) * 0.113, 1.0), 0.28, 0.78), + } + + +static func _tuer_zur_mitte(x: float, z: float) -> String: + if absf(x) > absf(z): + return "left" if x > 0.0 else "right" + return "forward" if z > 0.0 else "back" + + +static func _mensch(i: int) -> Dictionary: + var anliegen: Dictionary = ANLIEGEN[i % ANLIEGEN.size()] + var arbeit: String = str(ARBEIT[i % ARBEIT.size()]) + var heim := heim_key(i) + var ablauf: Array = [] + for e in ABLAEUFE[i % ABLAEUFE.size()]: + var ort: String = str(e[1]) + if ort == "heim": + ort = heim + elif ort == "arbeit": + ort = arbeit + ablauf.append([e[0], ort, e[2]]) + return { + "id": "mensch_%d" % i, + "name": "%s %s" % [_vorname(i), _nachname(i)], + "alter": 22 + (i * 7) % 38, + "beruf": str(BERUFE[i % BERUFE.size()]), + "traum": str(anliegen["traum"]), + "problem": str(anliegen["problem"]), + "tags": (anliegen["tags"] as Array).duplicate(), + "mission": true, + "heim": heim, + "farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8), + "routine": ablauf, + } + + +static func _vorname(i: int) -> String: + return str(VORNAMEN[i % VORNAMEN.size()]) + + +static func _nachname(i: int) -> String: + return str(NACHNAMEN[(i / VORNAMEN.size() + i) % NACHNAMEN.size()]) + + +## Die Tätigkeiten eines Menschen in der Reihenfolge seines Tages, ohne Dopplung. +static func aktivitaeten(id: String) -> Array: + var out: Array = [] + for e in by_id(id)["routine"]: + if not out.has(e[2]): + out.append(e[2]) + return out + static func by_id(id: String) -> Dictionary: for h in LIST: diff --git a/scripts/main.gd b/scripts/main.gd index f7f1bc3..8e84917 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -31,7 +31,7 @@ func _ready() -> void: Color(0.8, 0.95, 1))) _traumwelt() - ui.intro_panel(func() -> void: pass) + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED func _traumwelt() -> void: @@ -109,6 +109,10 @@ func _hinweis_setzen() -> void: ui.setze_hinweis("[E] Zur Erde reisen") "traum": ui.setze_hinweis("[E] Zurück in die Traumwelt") + "lehre": + ui.setze_hinweis("[E] %s — Techniken lernen" % titel) + "brett": + ui.setze_hinweis("[E] Schwarzes Brett — Mission annehmen") _: ui.setze_hinweis("[E] %s" % titel) else: @@ -148,8 +152,13 @@ func _interagieren() -> void: "erde": if Game.active_mission == "" and Game.completed.size() < HumanDB.LIST.size(): Game.notified.emit( - "Das Tor öffnet sich nur mit einer angenommenen Mission.", Color(1, 0.8, 0.5)) + "Das Tor öffnet sich nur mit einer Mission. Sieh am Schwarzen Brett nach.", + Color(1, 0.8, 0.5)) else: _erde() "traum": _traumwelt() + "lehre": + ui.lehre_panel(str(_fokus.get_meta("arg"))) + "brett": + ui.brett_panel() diff --git a/scripts/ui.gd b/scripts/ui.gd index 25c11ce..b9a2c9f 100644 --- a/scripts/ui.gd +++ b/scripts/ui.gd @@ -133,6 +133,7 @@ func _process(_delta: float) -> void: var wunsch := _content.get_combined_minimum_size().y var platz := get_viewport().get_visible_rect().size.y - 220.0 _scroll.custom_minimum_size.y = clampf(wunsch, 120.0, platz) + _panel.reset_size() # sonst behält der Rahmen die Höhe des Panels davor if _scroll_frames > 0: _scroll.scroll_vertical = _scroll_wunsch _scroll_frames -= 1 @@ -148,13 +149,14 @@ func _hud_aktualisieren() -> void: [Game.einsicht, Game.known.size(), TechDB.LIST.size()]) if Game.active_mission != "": var h := HumanDB.by_id(Game.active_mission) - zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %%" % - [h["name"], int(Game.mission_progress(Game.active_mission))]) + zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %% · %d Regeln" % + [h["name"], int(Game.mission_progress(Game.active_mission)), + Game.regel_anzahl(Game.active_mission)]) zeilen.append("[color=#aaaaaa]%s[/color]" % h["traum"]) elif Game.completed.size() >= HumanDB.LIST.size(): zeilen.append("[color=#88ff99]Alle Träume erfüllt.[/color]") else: - zeilen.append("[color=#aaaaaa]Keine Mission. Journal öffnen mit J.[/color]") + zeilen.append("[color=#aaaaaa]Keine Mission. Schwarzes Brett am Dorfplatz.[/color]") _hud.text = "\n".join(zeilen) @@ -250,8 +252,9 @@ func _hinweis_block(t: Dictionary) -> String: # -------------------------------------------------------- Journal -## Das Journal ist immer dabei. Hier werden Techniken gelernt und vertieft -## und Missionen angenommen. +## Das Journal ist immer dabei und führt Buch: welche Techniken du gelernt hast +## und welche Missionen du angenommen hast. Gelehrt wird im Hörsaal, +## vergeben werden die Missionen am Schwarzen Brett. func journal_panel(seite: String = "") -> void: if seite != "": _journal_seite = seite @@ -271,7 +274,11 @@ func _reiter() -> void: var hb := HBoxContainer.new() hb.add_theme_constant_override("separation", 8) _content.add_child(hb) - for eintrag in [["techniken", "Techniken"], ["missionen", "Missionen"]]: + var seiten := [ + ["techniken", "Techniken (%d)" % Game.known.size()], + ["missionen", "Missionen (%d)" % Game.progress.size()], + ] + for eintrag in seiten: var b := Button.new() b.text = eintrag[1] b.custom_minimum_size = Vector2(0, 36) @@ -293,10 +300,153 @@ func _reiter() -> void: func _journal_techniken() -> void: - _text("Zwölf Wege, die Menschen wirklich weiterbringen. Jede Anwendung " + - "draußen macht eine Technik von allein besser. Vertiefen geht auch hier.", 15) + if Game.known.is_empty(): + _text("[color=#ffcc66]Du hast noch keine Technik gelernt.[/color]\n" + + "Im [b]Hörsaal der Wege[/b] hinter dem Dorfplatz wird der erste Weg gelehrt.", 16) + return + _text("Was du gelernt hast. Jede Anwendung draußen macht eine Technik von allein " + + "besser; vertieft wird im Hörsaal.", 15) _trenner() for id in TechDB.ids(): + if not Game.knows(id): + continue + var t := TechDB.by_id(id) + _text("[b][color=#ffe3a0]%s[/color][/b] · Stufe %d (%s)" % + [t["name"], Game.skill_of(id), Game.xp_to_next(id)], 18) + _text(_hinweis_block(t), 15) + _text("[color=#9fb6ff]Hilft bei:[/color] %s [color=#9fb6ff]Momente:[/color] %s" % + [TechDB.tags_text(t["tags"]), TechDB.momente_text(t["momente"])], 14) + _trenner() + + +func _journal_missionen() -> void: + if Game.progress.is_empty(): + _text("[color=#ffcc66]Du hast noch keine Mission angenommen.[/color]\n" + + "Am [b]Schwarzen Brett[/b] am Dorfplatz hängen die Menschen aus, " + + "die nicht weiterkommen.", 16) + return + _text("Die Menschen, für die du dich entschieden hast.", 15) + _trenner() + for h in HumanDB.LIST: + var id: String = h["id"] + if not Game.angenommen(id): + continue + _text("[b][color=#ffe3a0]%s[/color][/b], %d — %s" % + [h["name"], h["alter"], h["beruf"]], 18) + _text("[b]Traum:[/b] %s" % h["traum"], 16) + _text("[b]Was im Weg steht:[/b] %s" % h["problem"], 15) + _text("[color=#9fb6ff]Themen:[/color] %s" % TechDB.tags_text(h["tags"]), 14) + if Game.completed.has(id): + _text("[color=#88ff99]★ Der Traum ist erfüllt.[/color]", 15) + elif Game.active_mission == id: + _text("[color=#ffdd77]Aktive Mission — %d %%[/color]" % + int(Game.mission_progress(id)), 15) + else: + _text("[color=#aaaaaa]Zurückgestellt — %d %%[/color]" % + int(Game.mission_progress(id)), 15) + if not Game.completed.has(id): + _knopf("Regelwerk — %d Regeln" % Game.regel_anzahl(id), + func() -> void: pipeline_panel(id)) + _trenner() + + +# -------------------------------------------------------- Regelwerk einer Mission + +## Wenn-Dann-Regeln: tut der Mensch etwas Bestimmtes, greift die zugeordnete +## Technik von allein. So automatisiert der Spieler den Fortschritt. +func pipeline_panel(human_id: String) -> void: + var scroll_alt := _scroll.scroll_vertical if offen and _kind == "pipeline" else 0 + var h := HumanDB.by_id(human_id) + _leeren("Regelwerk — %s" % h["name"]) + _text("Jede Tätigkeit im Tag von %s kann eine Technik auslösen. " % h["name"] + + "Wechselt sie hinein, greift die Regel von allein — solange du auf der Erde bist.", 15) + _text("[color=#aaaaaa]Fortschritt: [b]%d %%[/b] · Regeln: [b]%d[/b][/color]" % + [int(Game.mission_progress(human_id)), Game.regel_anzahl(human_id)], 15) + _trenner() + for activity in HumanDB.aktivitaeten(human_id): + var tat := Orte.text(activity) + if not Game.erreichbar(activity): + _knopf("%s → unerreichbar" % tat, func() -> void: pass, false) + continue + var tech: String = Game.regel_fuer(human_id, activity) + var beschriftung := "%s → keine Regel" % tat + if tech != "": + var ev := Game.evaluate(tech, human_id, activity, false) + beschriftung = "%s → %s · %d %%" % [tat, TechDB.by_id(tech)["name"], + int(round(float(ev["chance"]) * 100.0))] + _knopf(beschriftung, func() -> void: regel_panel(human_id, activity)) + _trenner() + _knopf("Zurück zum Journal", func() -> void: journal_panel("missionen")) + _zeigen("pipeline") + _scroll_wunsch = scroll_alt + _scroll_frames = 4 + + +## Welche Technik soll bei dieser Tätigkeit greifen? +func regel_panel(human_id: String, activity: String) -> void: + var h := HumanDB.by_id(human_id) + _leeren("Regel für »%s«" % Orte.text(activity)) + if Game.known.is_empty(): + _text("[color=#ffcc66]Du kennst keine Technik. Im Hörsaal der Wege " + + "werden sie gelehrt.[/color]", 16) + _knopf("Zurück", func() -> void: pipeline_panel(human_id)) + _zeigen("regel") + return + _text("Was soll greifen, wenn %s %s? Die Erfolgschance gilt für genau diesen Moment. " % + [h["name"], Orte.text(activity)] + + "Jede Technik hält nur einen Posten — sie hier einzusetzen nimmt sie anderswo weg.", 15) + _trenner() + for id in TechDB.ids(): + if not Game.knows(id): + continue + var t := TechDB.by_id(id) + var ev := Game.evaluate(id, human_id, activity, false) + var marker: Array = [] + if ev["tag"]: + marker.append("[color=#88ff99]passt zum Thema[/color]") + if ev["moment"]: + marker.append("[color=#88ff99]passender Moment[/color]") + if ev["blocked"]: + marker.append("[color=#ff8877]%s[/color]" % ev["grund"]) + if marker.is_empty(): + marker.append("[color=#aaaaaa]weder Thema noch Moment passen[/color]") + var gewaehlt := "" + var haengt := Game.regel_moment(human_id, id) + if haengt == activity: + gewaehlt = " [color=#ffdd77](hier eingestellt)[/color]" + elif haengt != "": + gewaehlt = " [color=#ffaa66](hängt an »%s«)[/color]" % Orte.text(haengt) + _text("[b]%s[/b] · Stufe %d%s\n[color=#cccccc]%s[/color]\n%s" % + [t["name"], Game.skill_of(id), gewaehlt, t["wann"], " · ".join(marker)], 16) + var beschriftung := "Blockiert" if ev["blocked"] else \ + "Diese Regel — %d %% Erfolgschance" % int(round(float(ev["chance"]) * 100.0)) + _knopf(beschriftung, func() -> void: + Game.regel_setzen(human_id, activity, id) + pipeline_panel(human_id), not ev["blocked"]) + _trenner() + _knopf("Keine Regel für diese Tätigkeit", func() -> void: + Game.regel_setzen(human_id, activity, "") + pipeline_panel(human_id)) + _zeigen("regel") + + +# -------------------------------------------------------- Lehrorte + +## Jeder Lehrort unterrichtet genau einen Weg: der Hörsaal den ersten, jedes +## Haus den, den sein Bewohner mitgebracht hat. +func lehre_panel(ort: String = "hoersaal") -> void: + if not DreamDB.PLACES.has(ort): + return + var scroll_alt := _scroll.scroll_vertical if offen and _kind == "lehre" else 0 + var pl: Dictionary = DreamDB.PLACES[ort] + var ids: Array = DreamDB.techniken_von(ort) + _leeren(str(pl["name"])) + _text("[color=#aaaaaa]Einsicht: [b]%d[/b] · Gelernt: [b]%d / %d[/b] von %d entdeckten[/color]" % + [Game.einsicht, Game.known.size(), TechDB.LIST.size(), Game.offene_techniken()], 15) + _trenner() + if ids.is_empty(): + _text("[color=#ffcc66]Hier wird nichts gelehrt.[/color]", 16) + for id in ids: var t := TechDB.by_id(id) if Game.knows(id): _text("[b][color=#ffe3a0]%s[/color][/b] · Stufe %d (%s)" % @@ -309,16 +459,38 @@ func _journal_techniken() -> void: if Game.knows(id): _knopf("Vertiefen (1 Einsicht)", func() -> void: if Game.deepen(id): - journal_panel(), Game.einsicht >= 1) + lehre_panel(ort), Game.einsicht >= 1) else: _knopf("Lernen (1 Einsicht)", func() -> void: if Game.learn(id): - journal_panel(), Game.einsicht >= 1) + lehre_panel(ort), Game.einsicht >= 1) _trenner() + _naechster_weg() + _zeigen("lehre") + _scroll_wunsch = scroll_alt + _scroll_frames = 4 -func _journal_missionen() -> void: - _text("Menschen, deren Traum feststeckt.", 15) +## Was als Nächstes ins Dorf käme. +func _naechster_weg() -> void: + var offen_zahl := Game.offene_techniken() + if offen_zahl >= TechDB.LIST.size(): + _text("[color=#88ff99]Alle zwölf Wege sind entdeckt. Das Dorf wächst weiter.[/color]", 15) + return + var naechste: Dictionary = TechDB.LIST[offen_zahl] + _text("[color=#aaaaaa]Noch unentdeckt: [b]%s[/b] und %d weitere. Jeder erfüllte Traum " % + [naechste["name"], TechDB.LIST.size() - offen_zahl - 1] + + "bringt einen Bewohner ins Dorf, und er lehrt einen neuen Weg.[/color]", 15) + + +# -------------------------------------------------------- Schwarzes Brett + +## Hier hängen die Menschen aus, deren Traum feststeckt. +func brett_panel() -> void: + var scroll_alt := _scroll.scroll_vertical if offen and _kind == "brett" else 0 + _leeren("Schwarzes Brett") + _text("Menschen, deren Traum feststeckt. Nimm einen an — das Tor zur Erde " + + "öffnet sich nur mit einer Mission.", 15) _trenner() for h in HumanDB.LIST: var id: String = h["id"] @@ -333,11 +505,16 @@ func _journal_missionen() -> void: _text("[color=#ffdd77]Aktive Mission — %d %%[/color]" % int(Game.mission_progress(id)), 15) else: - _text("Fortschritt: %d %%" % int(Game.mission_progress(id)), 15) + if Game.angenommen(id): + _text("[color=#aaaaaa]Schon einmal angenommen — %d %%[/color]" % + int(Game.mission_progress(id)), 15) _knopf("Mission annehmen", func() -> void: Game.accept_mission(id) - journal_panel()) + brett_panel()) _trenner() + _zeigen("brett") + _scroll_wunsch = scroll_alt + _scroll_frames = 4 # -------------------------------------------------------- Anwenden auf der Erde @@ -411,37 +588,6 @@ func _anwenden(id: String) -> void: [t["name"], int(round(float(res["chance"]) * 100.0))], Color(1, 0.6, 0.5)) -# -------------------------------------------------------- Intro - -func intro_panel(on_close: Callable) -> void: - _leeren("Traumwandler") - _text("Du bist ein Wesen aus der Traumwelt. Menschen tragen Träume mit sich herum, " + - "die sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt.") - _trenner() - _text("[b]Dein Journal — Taste J[/b]\n" + - "· Es ist immer dabei, in beiden Welten.\n" + - "· [b]Techniken[/b] — lerne echte Lösungswege. Jede sagt dir, was sie tut, " + - "wann sie passt und warum sie hilft. Anwenden macht sie von allein besser.\n" + - "· [b]Missionen[/b] — nimm einen Menschen an, dem du helfen willst.") - _text("[b]In der Traumwelt[/b]\n" + - "· Ein Dorf auf einer schwebenden Insel. Vier Wesen wohnen hier, jedes in " + - "seinem Haus, jedes mit eigenem Tagesablauf. Die Häuser stehen offen.\n" + - "· [b]Tor zur Erde[/b] — reise hinab, sobald du eine Mission hast.") - _text("[b]Auf der Erde[/b]\n" + - "· Du bist ein unsichtbarer Geist und schwebst durch Wände. Leertaste hoch, Umschalt runter.\n" + - "· Die Menschen leben ihren Tagesablauf: schlafen, arbeiten, grübeln, spazieren.\n" + - "· Geh nah heran und drücke [b]E[/b]. Deine Erfolgschance hängt von deiner Stufe ab — " + - "und davon, ob Technik und Moment zum Menschen passen.\n" + - "· Schlafende erreichst du nicht. Wer abblockt, braucht eine Weile Ruhe.") - _trenner() - _text("[color=#aaaaaa]Du beginnst mit 3 Einsicht. Jede Technik kostet 1. " + - "Ein erfüllter Traum bringt 3 zurück.[/color]", 15) - _knopf("Losgehen", func() -> void: - schliessen() - on_close.call()) - _zeigen("intro") - - func _unhandled_input(event: InputEvent) -> void: if offen and event.is_action_pressed("ui_cancel"): schliessen() diff --git a/tests/shots.gd b/tests/shots.gd index fb10aec..d3d18cd 100644 --- a/tests/shots.gd +++ b/tests/shots.gd @@ -13,66 +13,90 @@ func _process(_d: float) -> void: func _ready() -> void: + seed(20240) DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUT)) main = load("res://scenes/main.tscn").instantiate() add_child(main) await get_tree().process_frame await get_tree().process_frame - await knipse("01_intro") + await knipse("01_start") - main.ui.schliessen() - main.player.global_position = Vector3(0, 4.0, 34.0) + main.player.global_position = Vector3(0, 6.0, 24.0) # vor dem Hörsaal, Blick ins Dorf main.player.yaw = 0.0 - main.player.pitch = -0.14 + main.player.pitch = -0.35 Game.time_h = 10.0 await warte(30) await knipse("02_traumwelt") - main.player.global_position = Vector3(0, 12.0, 30.0) - main.player.pitch = -0.5 + main.player.global_position = Vector3(0, 32.0, 14.0) # über der Lichtung + main.player.yaw = 0.0 + main.player.pitch = -0.78 await warte(20) await knipse("03_traumdorf_ueberblick") - var haus := Orte.center(DreamDB.PLACES, "haus_ailen") - main.player.global_position = haus + Vector3(0, 2.0, 12.0) - main.player.yaw = 0.0 - main.player.pitch = -0.18 + # Der Hörsaal ist zu Beginn das einzige Gebäude auf der Insel + var saal_aussen := Orte.center(DreamDB.PLACES, "hoersaal") + main.player.global_position = saal_aussen + Vector3(0, 2.0, -14.0) + main.player.yaw = PI + main.player.pitch = -0.12 await warte(20) - await knipse("04_haus_aussen") + await knipse("04_hoersaal_aussen") - # ein Wesen in seinem Haus - var ailen: Bewohner = null - for w in main.welt.bewohner: - if w.data["id"] == "ailen": - ailen = w - for i in range(900): - main._process(0.05) - for k in main.welt.get_children(): - if k is Bewohner: - k._process(0.05) - Game.time_h = 10.0 - if ailen.place == "haus_ailen" and ailen._path.is_empty(): - break - main.player.global_position = ailen.global_position + Vector3(3.0, 0.6, 1.2) - main.player.yaw = -PI * 0.32 - main.player.pitch = -0.06 + # Wo später die Häuser stehen, ist noch nichts + main.player.global_position = Vector3(-18.0, 8.0, -6.0) + main.player.yaw = -0.9 + main.player.pitch = -0.30 await warte(20) - await knipse("05_haus_innen") + await knipse("05_insel_ohne_haeuser") + + # Hörsaal der Wege: hier werden die Techniken gelehrt + var saal := Orte.center(DreamDB.PLACES, "hoersaal") + main.player.global_position = saal + Vector3(0, 1.0, -1.0) + main.player.yaw = PI + main.player.pitch = -0.05 + await warte(20) + await knipse("06_hoersaal") + + main.ui.lehre_panel() + await warte(6) + await knipse("07_techniken_lernen") + main.ui.schliessen() + + Game.learn(str(DreamDB.techniken_von("hoersaal")[0])) main.ui.journal_panel("techniken") await warte(6) - await knipse("06_journal_techniken") + await knipse("08_journal_techniken") main.ui.schliessen() - Game.learn("pomodoro") - Game.learn("zerlegen") - Game.learn("box_atmung") + # Schwarzes Brett an der Lichtung + var brett := Orte.center(DreamDB.PLACES, "lichtung") + Vector3(-6.0, 0, 9.0) + main.player.global_position = brett + Vector3(0, 1.0, 4.0) + main.player.yaw = 0.0 + main.player.pitch = -0.02 + await warte(20) + await knipse("09_schwarzes_brett") + + main.ui.brett_panel() + await warte(6) + await knipse("10_missionen") + main.ui.schliessen() + Game.accept_mission("mensch_0") main.ui.journal_panel("missionen") await warte(6) - await knipse("07_journal_missionen") + await knipse("11_journal_missionen") + + # Regelwerk: Wenn-Dann-Ketten, die den Fortschritt automatisieren + Game.regel_setzen("mensch_0", "zoegert", str(DreamDB.techniken_von("hoersaal")[0])) + main.ui.pipeline_panel("mensch_0") + await warte(6) + await knipse("12_regelwerk") + + main.ui.regel_panel("mensch_0", "arbeitet") + await warte(6) + await knipse("13_regel_waehlen") main.ui.schliessen() - Game.accept_mission("lena") # Lichtung, wenn sich die Wesen dort treffen for i in range(1200): @@ -85,14 +109,14 @@ func _ready() -> void: main.player.yaw = 0.0 main.player.pitch = -0.20 await warte(20) - await knipse("08_lichtung") + await knipse("14_dorfplatz_leer") var tor := Orte.center(DreamDB.PLACES, "tor") main.player.global_position = tor + Vector3(0, 2.5, 14.0) main.player.yaw = 0.0 main.player.pitch = -0.12 await warte(20) - await knipse("09_tor_zur_erde") + await knipse("15_tor_zur_erde") main._erde() Game.time_h = 10.5 @@ -101,11 +125,11 @@ func _ready() -> void: main.player.pitch = -0.62 main.player.yaw = 0.0 await warte(20) - await knipse("10_erde_uebersicht") + await knipse("16_erde_uebersicht") var lena: Bewohner = null for m in main.welt.humans: - if m.data["id"] == "lena": + if m.data["id"] == "mensch_0": lena = m for i in range(900): main._process(0.05) @@ -119,14 +143,30 @@ func _ready() -> void: main.player.yaw = PI main.player.pitch = -0.15 await warte(20) - await knipse("11_lena_zoegert") + await knipse("17_lena_zoegert") + + # Das Regelwerk greift ohne Zutun, sobald Lena die Tätigkeit wechselt + var vor := Game.mission_progress("mensch_0") + for i in range(20000): + Game.time_h += 0.004 + main._process(0.02) + for k in main.welt.get_children(): + if k is Bewohner: + k._process(0.05) + if Game.mission_progress("mensch_0") > vor: + break + main.player.global_position = lena.global_position + Vector3(0, 1.6, -5.5) + main.player.yaw = PI + main.player.pitch = -0.10 + await warte(4) + await knipse("18_regel_greift") main.ui.anwenden_panel(lena) await warte(6) - await knipse("12_anwenden") - main.ui._anwenden("pomodoro") + await knipse("19_anwenden") + main.ui._anwenden(str(DreamDB.techniken_von("hoersaal")[0])) await warte(25) - await knipse("13_reaktion") + await knipse("20_reaktion") Game.time_h = 13.5 for i in range(1200): @@ -139,7 +179,7 @@ func _ready() -> void: main.player.yaw = PI main.player.pitch = -0.5 await warte(20) - await knipse("14_cafe_mittag") + await knipse("21_cafe_mittag") Game.time_h = 22.5 await warte(40) @@ -147,7 +187,81 @@ func _ready() -> void: main.player.yaw = 0.0 main.player.pitch = -0.5 await warte(20) - await knipse("15_nacht") + await knipse("22_nacht") + + # Loop 2: ein erfüllter Traum lässt die Insel wachsen + if not Game.completed.has("mensch_0"): + Game.completed.append("mensch_0") + Game.active_mission = "" + Game.einsicht = 3 + Game.welt_erzeugen() + main._traumwelt() + await warte(20) + main.player.global_position = Vector3(-30.0, 26.0, 30.0) + main.player.yaw = 0.7 + main.player.pitch = -0.55 + Game.time_h = 11.0 + await warte(20) + await knipse("23_insel_gewachsen") + + var erstes := Orte.center(DreamDB.PLACES, DreamDB.haus_key(0)) + var tuer := Orte.door(DreamDB.PLACES, DreamDB.haus_key(0)) + var hinein := (erstes - tuer).normalized() + main.player.global_position = erstes - hinein * 2.0 + Vector3(0, 1.0, 0) + main.player.yaw = atan2(-hinein.x, -hinein.z) + main.player.pitch = -0.05 + await warte(20) + await knipse("24_erstes_haus") + + main.ui.lehre_panel(DreamDB.haus_key(0)) + await warte(6) + await knipse("25_neuer_weg") + main.ui.schliessen() + + # Neun erfüllte Träume: das Dorf ist über den ersten Ring hinausgewachsen + while Game.completed.size() < 9: + Game.completed.append("traum_%d" % Game.completed.size()) + Game.welt_erzeugen() + main._traumwelt() + await warte(20) + main.player.global_position = Vector3(0, 110.0, 70.0) + main.player.yaw = 0.0 + main.player.pitch = -0.85 + Game.time_h = 12.0 + await warte(20) + await knipse("26_insel_vollstaendig") + + # Jetzt wohnt jemand hier: die Wesen treffen sich auf der Lichtung + for i in range(1500): + main._process(0.05) + for k in main.welt.get_children(): + if k is Bewohner: + k._process(0.05) + Game.time_h = 19.5 + main.player.global_position = Vector3(0.0, 1.2, 20.0) + main.player.yaw = 0.0 + main.player.pitch = -0.14 + await warte(20) + await knipse("27_wesen_am_dorfplatz") + + var ailen: Bewohner = null + for w in main.welt.bewohner: + if w.data["id"] == "wesen_0": + ailen = w + for i in range(1200): + main._process(0.05) + for k in main.welt.get_children(): + if k is Bewohner: + k._process(0.05) + Game.time_h = 10.0 + if ailen != null and ailen.place == DreamDB.haus_key(0) and ailen._path.is_empty(): + break + if ailen != null: + main.player.global_position = ailen.global_position + Vector3(3.0, 0.6, 1.2) + main.player.yaw = -PI * 0.32 + main.player.pitch = -0.06 + await warte(20) + await knipse("28_wesen_im_haus") print("Screenshots: %d" % n) print("Ordner: %s" % ProjectSettings.globalize_path(OUT)) diff --git a/tests/verify.gd b/tests/verify.gd index a934e19..1d5fa66 100644 --- a/tests/verify.gd +++ b/tests/verify.gd @@ -26,16 +26,20 @@ func _ready() -> void: await test_journal() await test_traumdorf() await test_haus_betreten() + await test_hoersaal() await test_mission_annehmen() await test_reise_zur_erde() await test_tagesablauf() await test_wege_machbar() await test_geist() await test_technik_anwenden() + await test_pipeline() await test_mission_abschliessen() + await test_welt_waechst() await test_techniken_verbessern_sich() await test_journal_ueberall() await test_steuerung() + await test_erster_traum_schaffbar() print("\n=========================================") print("Fehlende Prüfungen: %d" % fehler) @@ -63,6 +67,77 @@ func schritte(n: int, dt: float = 0.05, zeit: float = -1.0) -> void: kind._process(dt) +func _panel_text() -> String: + var out: Array = [] + for c in ui()._content.get_children(): + if c is RichTextLabel: + out.append((c as RichTextLabel).get_parsed_text()) + return "\n".join(out) + + +func _journal_text() -> String: + return _panel_text() + + +## 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 ui()._content.get_children(): + if c is Button: + out.append((c as Button).text) + return "\n".join(out) + + +func _knopf_da(anfang: String) -> bool: + for c in ui()._content.get_children(): + if c is Button and (c as Button).text.begins_with(anfang): + return true + return false + + +func _druecke(anfang: String) -> bool: + for c in ui()._content.get_children(): + 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 @@ -80,16 +155,28 @@ func laufe(von: Vector3, yaw: float, frames: int) -> Vector3: func test_start() -> void: abschnitt("Start") - p(ui().offen, "Intro-Panel ist offen") - ui().schliessen() - p(not ui().offen, "Intro lässt sich schließen") + p(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 inter := get_tree().get_nodes_in_group("interactable") - p(inter.size() == 1 and inter[0].get_meta("kind") == "erde", - "Einzige Interaktion in der Traumwelt ist das Tor zur Erde (%d)" % inter.size()) + 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.einsicht == 3, "Startkapital 3 Einsicht") p(Game.known.is_empty(), "Noch keine Technik gelernt") + 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: @@ -102,45 +189,71 @@ func test_journal() -> void: ui().journal_panel("techniken") p(ui().offen and ui()._kind == "journal", "Journal öffnet sich") - var knoepfe := 0 - for c in ui()._content.get_children(): - if c is Button: - knoepfe += 1 - p(knoepfe >= TechDB.LIST.size(), "Jede der %d Techniken hat einen Knopf (%d)" % - [TechDB.LIST.size(), knoepfe]) + p(_journal_text().contains("Hörsaal"), + "Ohne gelernte Technik verweist das Journal auf den Hörsaal") + p(not _knopf_da("Lernen") and not _knopf_da("Vertiefen"), + "Das Journal lehrt und vertieft nicht selbst") ui().journal_panel("missionen") - p(ui()._journal_seite == "missionen" and ui()._content.get_child_count() > 0, - "Missionsseite baut auf") + p(_journal_text().contains("Schwarzen Brett"), + "Ohne Mission verweist das Journal auf das Schwarze Brett") + p(not _knopf_da("Mission annehmen"), "Das Journal vergibt keine Missionen") ui().schliessen() - p(Game.learn("pomodoro"), "Pomodoro gelernt") - p(Game.learn("zerlegen"), "Aufgabe zerlegen gelernt") - p(Game.learn("umgebung"), "Umgebung gestalten gelernt") - p(Game.einsicht == 0, "Einsicht auf 0 verbraucht") - p(not Game.learn("reframing"), "Ohne Einsicht keine weitere Technik") - p(Game.skill_of("pomodoro") == 1, "Frisch gelernte Technik ist Stufe 1") - Game.add_xp("pomodoro", 4) - p(Game.skill_of("pomodoro") == 2, "4 EP heben auf Stufe 2") - Game.add_xp("pomodoro", 24) - p(Game.skill_of("pomodoro") == 5, "28 EP erreichen Stufe 5") - Game.known["pomodoro"] = 0 + var erster := str(_basiswege()[0]) + p(Game.offene_techniken() == 1, + "Am Anfang ist genau ein Weg entdeckt (%d)" % Game.offene_techniken()) + p(Game.learn(erster), "Der erste Weg ist gelernt: %s" % TechDB.by_id(erster)["name"]) + p(Game.einsicht == 2, "Das kostet 1 von 3 Einsicht (%d)" % Game.einsicht) + p(not Game.learn(str(TechDB.LIST[1]["id"])), + "Der zweite Weg ist trotz Einsicht nirgends zu lernen") + 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 + for id in Game.known: + if not 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 leben ihren Tag") + 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 - p(wesen.size() == 4, "Vier Wesen wohnen in der Traumwelt (%d)" % wesen.size()) var haeuser := 0 for key in DreamDB.PLACES: - var s: Array = DreamDB.PLACES[key]["size"] - if s[0] > 0.0: + if key.begins_with("haus_"): haeuser += 1 - p(haeuser == 4, "Es gibt vier Häuser (%d)" % haeuser) + 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 := "haus_" + str(w.data["id"]) + 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 @@ -190,35 +303,143 @@ func test_traumdorf() -> void: zuhause += 1 p(zuhause == 4, "Um 01:00 sind alle vier an ihrem Nachtplatz (%d)" % zuhause) + # Ausgangslage wiederherstellen: das Dorf ist wieder leer + Game.completed.clear() + Game.welt_erzeugen() + main._traumwelt() + await get_tree().process_frame + func test_haus_betreten() -> void: abschnitt("Orte betreten") - var haus := Orte.center(DreamDB.PLACES, "haus_ailen") - var tuer := Orte.door(DreamDB.PLACES, "haus_ailen") + 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 + Vector3(0, 1.0, 1.5), 0.0, 80) + 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 - var vor_wand := haus + Vector3(-11.0, 1.0, 0) - var gestoppt: Vector3 = await laufe(vor_wand, -PI * 0.5, 80) - p(gestoppt.x < haus.x - 5.5, - "Die Seitenwand hält den Spieler auf (x = %.1f, Wand bei %.1f)" % - [gestoppt.x, haus.x - 6.0]) + # 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 ui()._content.get_children(): + if c is Button and (c as Button).text.begins_with("Lernen"): + 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.einsicht = 1 + Game.known.clear() + ui().lehre_panel() + var vorher := Game.known.size() + p(_druecke("Lernen"), "Ein Lernen-Knopf steht bereit") + p(Game.known.size() == vorher + 1, "Im Hörsaal gelernt (%d -> %d)" % [vorher, Game.known.size()]) + p(Game.einsicht == 0, "Lernen kostet 1 Einsicht") + + Game.einsicht = 1 + ui().lehre_panel() + var xp_summe := 0 + for id in Game.known: + xp_summe += int(Game.known[id]) + p(_druecke("Vertiefen"), "Ein Vertiefen-Knopf steht bereit") + var xp_neu := 0 + for id in Game.known: + xp_neu += int(Game.known[id]) + p(xp_neu == xp_summe + 3, "Vertiefen bringt 3 EP (%d -> %d)" % [xp_summe, xp_neu]) + p(Game.einsicht == 0, "Vertiefen kostet 1 Einsicht") + ui().schliessen() + + # Ausgangslage für die folgenden Prüfungen wiederherstellen + Game.known = {"pomodoro": 0, "zerlegen": 0, "umgebung": 0} + Game.einsicht = 0 func test_mission_annehmen() -> void: - abschnitt("Mission annehmen") - ui().journal_panel("missionen") - p(ui().offen and ui()._journal_seite == "missionen", "Missionsseite des Journals offen") + 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") + var alle_aus := true + for h in HumanDB.LIST: + if not _panel_text().contains(str(h["name"])): + alle_aus = false + p(alle_aus, "Alle %d Menschen hängen aus" % HumanDB.LIST.size()) + + p(Game.active_mission == "", "Vorher keine Mission") + 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(buch.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() - p(Game.accept_mission("lena"), "Mission Lena angenommen") - p(Game.active_mission == "lena", "Mission ist aktiv") func test_reise_zur_erde() -> void: @@ -228,6 +449,28 @@ func test_reise_zur_erde() -> void: 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) @@ -235,39 +478,39 @@ func test_reise_zur_erde() -> void: main._interagieren() await get_tree().process_frame p(main.auf_erde and welt() is EarthWorld, "Spieler ist auf der Erde") - p(welt().humans.size() == 4, "Vier Menschen leben in der Stadt") + p(welt().humans.size() == HumanDB.LIST.size() and welt().humans.size() >= 3, + "%d Menschen leben in der Stadt" % welt().humans.size()) func test_tagesablauf() -> void: abschnitt("Tagesablauf der Menschen") - var lena: Bewohner = null + var mensch: Bewohner = null for m in welt().humans: - if m.data["id"] == "lena": - lena = m - p(lena != null, "Lena existiert auf der Erde") + if m.data["id"] == "mensch_0": + mensch = m + p(mensch != null, "Der erste Mensch lebt auf der Erde: %s" % mensch.data["name"]) schritte(30, 0.05, 2.0) - p(lena.activity == "schlaeft", "Um 02:00 schläft Lena (%s)" % lena.activity) - p(lena.global_position.distance_to(Orte.center(HumanDB.PLACES, "haus_lena")) < 4.0, - "Sie schläft in ihrem Atelier") + p(mensch.activity == "schlaeft", "Um 02:00 schläft er (%s)" % mensch.activity) + p(mensch.global_position.distance_to( + Orte.center(HumanDB.PLACES, str(mensch.data["heim"]))) < 5.0, + "Und zwar zu Hause") - var gewandert := false - for i in range(600): - schritte(1, 0.05, 8.1) - if lena.place == "cafe" and lena._path.is_empty(): - gewandert = true - break - p(gewandert, "Um 08:00 geht Lena ins Café und kommt dort an") - p(lena.activity == "essen", "Im Café isst sie (%s)" % lena.activity) - - var daheim := false - for i in range(600): - schritte(1, 0.05, 10.2) - if lena.place == "haus_lena" and lena._path.is_empty(): - daheim = true - break - p(daheim, "Um 10:00 ist sie zurück im Atelier") - p(lena.activity == "zoegert", "Dort zögert sie vor der Arbeit (%s)" % lena.activity) + # 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: @@ -275,7 +518,8 @@ func test_tagesablauf() -> void: var r: Array = m._routine_at(float(h) + 0.5) if not HumanDB.PLACES.has(r[0]) or not Orte.ACTIVITY_TEXT.has(r[1]): luecken += 1 - p(luecken == 0, "Alle 4 Menschen haben rund um die Uhr Ort und Tätigkeit") + 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 @@ -294,10 +538,10 @@ func test_tagesablauf() -> void: var soll: Dictionary = {} for e in m.data["routine"]: soll[e[1]] = true - var ist: int = besucht[m.data["id"]].size() - if ist < soll.size(): + if besucht[m.data["id"]].size() < soll.size(): alle_ok = false - p(false, "%s erreichte nur %d von %d Stationen" % [m.data["name"], ist, soll.size()]) + 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") @@ -368,14 +612,20 @@ func test_technik_anwenden() -> void: abschnitt("Techniken anwenden — Erfolgschance") var lena: Bewohner = null for m in welt().humans: - if m.data["id"] == "lena": + if m.data["id"] == "mensch_0": lena = m - for i in range(900): - schritte(1, 0.05, 10.2) - if lena.place == "haus_lena" and lena._path.is_empty(): + # Die Stunde suchen, in der dieser Mensch zögert — sie steht in seiner Routine + var zoegert_um := -1.0 + for e in lena.data["routine"]: + if e[2] == "zoegert": + zoegert_um = float(e[0]) + 0.5 + for i in range(1200): + schritte(1, 0.05, zoegert_um) + if lena.activity == "zoegert" and lena._path.is_empty(): break - p(lena.activity == "zoegert", "Lena zögert gerade") + p(lena.activity == "zoegert", "%s zögert um %.0f Uhr (%s)" % + [lena.data["name"], zoegert_um, lena.activity]) main.player.set_mode(Player.Mode.GEIST) main.player.global_position = lena.global_position + Vector3(0, 1.0, 1.5) @@ -386,91 +636,308 @@ func test_technik_anwenden() -> void: ui().schliessen() Game.known["pomodoro"] = 0 - var s1: float = Game.evaluate("pomodoro", "lena", "zoegert", false)["chance"] + var s1: float = Game.evaluate("pomodoro", "mensch_0", "zoegert", false)["chance"] Game.known["pomodoro"] = 28 - var s5: float = Game.evaluate("pomodoro", "lena", "zoegert", false)["chance"] + var s5: float = Game.evaluate("pomodoro", "mensch_0", "zoegert", false)["chance"] p(s5 > s1, "Höherer Skill = höhere Erfolgschance (%.0f%% -> %.0f%%)" % [s1 * 100, s5 * 100]) Game.known["pomodoro"] = 0 - var passend: Dictionary = Game.evaluate("pomodoro", "lena", "zoegert", false) - var unpassend: Dictionary = Game.evaluate("pomodoro", "lena", "essen", false) + var passend: Dictionary = Game.evaluate("pomodoro", "mensch_0", "zoegert", false) + var unpassend: Dictionary = Game.evaluate("pomodoro", "mensch_0", "essen", false) p(passend["moment"] and not unpassend["moment"], "Moment-Erkennung: zögert ja, isst nein") p(passend["chance"] > unpassend["chance"], "Passender Moment erhöht die Chance") p(passend["tag"], "Pomodoro trifft Lenas Thema Aufschieben") - var falsches_thema: Dictionary = Game.evaluate("pomodoro", "mira", "zoegert", false) + var falsches_thema: Dictionary = Game.evaluate("pomodoro", "mensch_2", "zoegert", false) p(not falsches_thema["tag"], "Bei Mira trifft Pomodoro das Thema nicht") p(passend["chance"] > falsches_thema["chance"], "Passendes Thema erhöht die Chance") - p(Game.evaluate("pomodoro", "lena", "schlaeft", false)["blocked"], + p(Game.evaluate("pomodoro", "mensch_0", "schlaeft", false)["blocked"], "Schlafende Menschen sind nicht erreichbar") - p(Game.evaluate("pomodoro", "lena", "zoegert", true)["chance"] < passend["chance"], + p(Game.evaluate("pomodoro", "mensch_0", "zoegert", true)["chance"] < passend["chance"], "Wer abblockt, ist schwerer zu erreichen") - p(Game.evaluate("nein_sagen", "lena", "zoegert", false)["blocked"], + p(Game.evaluate("nein_sagen", "mensch_0", "zoegert", false)["blocked"], "Ungelernte Techniken lassen sich nicht anwenden") - var vorher_p: float = Game.mission_progress("lena") - var res := Game.apply_technique("pomodoro", "lena", "zoegert", false) + var vorher_p: float = Game.mission_progress("mensch_0") + var res := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false) p(res["ok"], "Anwendung wird ausgeführt") if res["success"]: - p(Game.mission_progress("lena") > vorher_p, "Erfolg bringt Missionsfortschritt") + p(Game.mission_progress("mensch_0") > vorher_p, "Erfolg bringt Missionsfortschritt") else: - p(Game.mission_progress("lena") == vorher_p, "Misserfolg bringt keinen Fortschritt") + p(Game.mission_progress("mensch_0") == vorher_p, "Misserfolg bringt keinen Fortschritt") p(Game.known["pomodoro"] > 0, "Jede Anwendung bringt Erfahrung") - Game.progress["lena"] = 0.0 + Game.progress["mensch_0"] = 0.0 Game.used.clear() Game.known["pomodoro"] = 28 var ertraege: Array = [] for i in range(6): - var r := Game.apply_technique("pomodoro", "lena", "zoegert", false) + var r := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false) if r["success"]: ertraege.append(r["gain"]) p(ertraege.size() >= 2 and ertraege[ertraege.size() - 1] < ertraege[0], "Dieselbe Technik immer wieder wirkt schwächer") +func test_pipeline() -> void: + abschnitt("Regelwerk — Missionsfortschritt automatisieren") + Game.active_mission = "mensch_0" + Game.progress["mensch_0"] = 0.0 + Game.completed.clear() + Game.used.clear() + Game.pipeline.clear() + Game.known = {"pomodoro": 28, "zerlegen": 28, "umgebung": 28} + + var taten := HumanDB.aktivitaeten("mensch_0") + p(taten.size() >= 5, "Lenas Tag hat %d verschiedene Tätigkeiten" % taten.size()) + p(taten.has("zoegert") and taten.has("schlaeft"), + "Darunter zögert und schläft: %s" % ", ".join(taten)) + + # Regelwerk über das Journal aufbauen + ui().journal_panel("missionen") + p(_knopf_da("Regelwerk"), "Die angenommene Mission trägt ein Regelwerk") + p(_druecke("Regelwerk"), "Das Regelwerk öffnet sich") + p(ui()._kind == "pipeline", "Panel »pipeline« ist offen") + var zeilen := _knopf_texte() + var fehlt: Array = [] + for a in taten: + if not zeilen.contains(Orte.text(a)): + fehlt.append(a) + p(fehlt.is_empty(), "Jede der %d Tätigkeiten hat eine Zeile (fehlt: %s)" % + [taten.size(), ", ".join(fehlt)]) + p(zeilen.contains("schläft → unerreichbar"), + "Schlaf ist als unerreichbar ausgewiesen, statt eine Regel anzubieten") + var schlaf_aktiv := false + for c in ui()._content.get_children(): + if c is Button and (c as Button).text.begins_with("schläft") and not (c as Button).disabled: + schlaf_aktiv = true + p(not schlaf_aktiv, "Und lässt sich gar nicht erst anklicken") + ui().regel_panel("mensch_0", "zoegert") + p(ui()._kind == "regel", "Die Technikwahl für »zögert« öffnet sich") + p(_panel_text().contains("Pomodoro-Technik"), "Gelernte Techniken stehen zur Wahl") + p(_druecke("Diese Regel"), "Eine Technik lässt sich zuordnen") + p(Game.regel_fuer("mensch_0", "zoegert") != "", + "Regel steht: zögert -> %s" % Game.regel_fuer("mensch_0", "zoegert")) + p(ui()._kind == "pipeline", "Danach ist wieder das Regelwerk offen") + ui().schliessen() + + Game.regel_setzen("mensch_0", "gruebelt", "zerlegen") + Game.regel_setzen("mensch_0", "aufraeumen", "umgebung") + p(Game.regel_anzahl("mensch_0") == 3, "Drei Regeln im Werk (%d)" % Game.regel_anzahl("mensch_0")) + + # Eine Technik hält nur einen Posten + Game.regel_setzen("mensch_0", "arbeitet", "zerlegen") + p(Game.regel_anzahl("mensch_0") == 3, + "Dieselbe Technik anderswo einzusetzen fügt keine Regel hinzu (%d)" % + Game.regel_anzahl("mensch_0")) + p(Game.regel_fuer("mensch_0", "gruebelt") == "" and Game.regel_fuer("mensch_0", "arbeitet") == "zerlegen", + "Sie ist umgezogen statt sich zu verdoppeln") + Game.regel_setzen("mensch_0", "arbeitet", "") + Game.regel_setzen("mensch_0", "gruebelt", "zerlegen") + + # Ohne Zutun einen Tag laufen lassen + var lena: Bewohner = null + for m in welt().humans: + if m.data["id"] == "mensch_0": + lena = m + p(lena != null, "Lena lebt in der Stadt") + var vorher: float = Game.mission_progress("mensch_0") + var vor_xp: int = int(Game.known["pomodoro"]) + int(Game.known["zerlegen"]) + Game.time_h = 0.0 + main.player.global_position = Vector3(0, 30.0, 60.0) # weit weg, kein Zutun + for i in range(9000): + schritte(1) + if Game.time_h >= 23.5: + break + var nachher: float = Game.mission_progress("mensch_0") + p(nachher > vorher, "Der Traum wächst von allein: %d %% -> %d %%" % [int(vorher), int(nachher)]) + p(int(Game.known["pomodoro"]) + int(Game.known["zerlegen"]) > vor_xp, + "Die Regeln sammeln nebenbei Erfahrung") + + # Ohne Regeln passiert nichts + Game.pipeline.clear() + Game.progress["mensch_0"] = 0.0 + var still: float = Game.mission_progress("mensch_0") + Game.time_h = 0.0 + for i in range(9000): + schritte(1) + if Game.time_h >= 23.5: + break + p(is_equal_approx(Game.mission_progress("mensch_0"), still), + "Ohne Regeln bewegt sich nichts (%d %%)" % int(Game.mission_progress("mensch_0"))) + + # Regeln greifen nur für die aktive Mission + Game.regel_setzen("mensch_1", "zoegert", "zerlegen") + Game.progress["mensch_1"] = 0.0 + Game.time_h = 0.0 + for i in range(9000): + schritte(1) + if Game.time_h >= 23.5: + break + p(is_equal_approx(Game.mission_progress("mensch_1"), 0.0), + "Regeln fremder Missionen bleiben still (%d %%)" % int(Game.mission_progress("mensch_1"))) + + # Ausgangslage für die folgenden Prüfungen + Game.pipeline.clear() + Game.progress["mensch_0"] = 0.0 + Game.used.clear() + Game.known = {"pomodoro": 0, "zerlegen": 0, "umgebung": 0} + + func test_mission_abschliessen() -> void: abschnitt("Mission abschließen") - Game.progress["lena"] = 0.0 + Game.progress["mensch_0"] = 0.0 Game.used.clear() var einsicht_vorher := Game.einsicht var runden := 0 - while Game.mission_progress("lena") < Game.MISSION_ZIEL and runden < 400: - Game.apply_technique("pomodoro", "lena", "zoegert", false) - Game.apply_technique("zerlegen", "lena", "gruebelt", false) - Game.apply_technique("umgebung", "lena", "aufraeumen", false) + while Game.mission_progress("mensch_0") < Game.MISSION_ZIEL and runden < 400: + Game.apply_technique("pomodoro", "mensch_0", "zoegert", false) + Game.apply_technique("zerlegen", "mensch_0", "gruebelt", false) + Game.apply_technique("umgebung", "mensch_0", "aufraeumen", false) runden += 1 - p(Game.mission_progress("lena") >= Game.MISSION_ZIEL, + p(Game.mission_progress("mensch_0") >= Game.MISSION_ZIEL, "Lenas Traum erreicht 100 %% (nach %d Runden)" % runden) - p(Game.completed.has("lena"), "Mission gilt als abgeschlossen") + p(Game.completed.has("mensch_0"), "Mission gilt als abgeschlossen") p(Game.active_mission == "", "Aktive Mission wurde freigegeben") p(Game.einsicht == einsicht_vorher + 3, "Abschluss bringt 3 Einsicht") + 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") + 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()) + + Game.einsicht = 3 + var neu_id := str(TechDB.LIST[1]["id"]) + 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("Lernen"), "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.einsicht = 2 + 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() + _wege_pruefen("Traumdorf mit %d Häusern" % stufen, DreamDB.PLACES, DreamDB.LIST) + _wege_pruefen("Stadt mit %d Menschen" % HumanDB.LIST.size(), HumanDB.PLACES, HumanDB.LIST) + Game.completed.assign(merker) + Game.welt_erzeugen() + + func test_techniken_verbessern_sich() -> void: abschnitt("Techniken verbessern sich durch Anwendung") Game.einsicht = 2 - p(Game.learn("reframing"), "Technik im Journal gelernt") - p(Game.skill_of("reframing") == 1, "Start auf Stufe 1") + p(Game.learn("zwei_minuten"), "Technik gelernt") + p(Game.skill_of("zwei_minuten") == 1, "Start auf Stufe 1") var einsicht_vorher := Game.einsicht var anwendungen := 0 - while Game.skill_of("reframing") < 3 and anwendungen < 60: - Game.apply_technique("reframing", "mira", "gruebelt", false) + while Game.skill_of("zwei_minuten") < 3 and anwendungen < 60: + Game.apply_technique("zwei_minuten", "mensch_2", "gruebelt", false) anwendungen += 1 - p(Game.skill_of("reframing") >= 3, - "Nach %d Anwendungen von allein auf Stufe %d" % [anwendungen, Game.skill_of("reframing")]) + p(Game.skill_of("zwei_minuten") >= 3, + "Nach %d Anwendungen von allein auf Stufe %d" % [anwendungen, Game.skill_of("zwei_minuten")]) p(Game.einsicht == einsicht_vorher, "Das kostet keine Einsicht") - var vor_xp: int = Game.known["reframing"] - var res := Game.apply_technique("reframing", "mira", "essen", false) - p(Game.known["reframing"] > vor_xp, + var vor_xp: int = Game.known["zwei_minuten"] + var res := Game.apply_technique("zwei_minuten", "mensch_2", "essen", false) + p(Game.known["zwei_minuten"] > vor_xp, "Auch ein %s bringt Erfahrung" % ("Erfolg" if res["success"] else "Fehlversuch")) - var xp_vor: int = Game.known["reframing"] - p(Game.deepen("reframing"), "Vertiefen im Journal funktioniert") - p(Game.known["reframing"] == xp_vor + 3, "Vertiefen bringt 3 EP") + var xp_vor: int = Game.known["zwei_minuten"] + p(Game.deepen("zwei_minuten"), "Vertiefen funktioniert") + p(Game.known["zwei_minuten"] == xp_vor + 3, "Vertiefen bringt 3 EP") p(Game.einsicht == einsicht_vorher - 1, "Vertiefen kostet 1 Einsicht") @@ -556,3 +1023,71 @@ func test_steuerung() -> void: Input.action_release("sinken") p(main.player.global_position.y < hoch - 1.0, "Umschalt lässt den Geist sinken (y = %.1f)" % main.player.global_position.y) + + +## 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 — +## und zwar zuverlässig, nicht nur bei gutem Würfelglück. +func _erster_traum_tage(saat: int) -> int: + seed(saat) + Game.einsicht = 3 + Game.known.clear() + Game.used.clear() + Game.pipeline.clear() + Game.progress.clear() + Game.completed.clear() + Game.active_mission = "" + Game.welt_erzeugen() + for id in _basiswege(): + Game.learn(id) + Game.accept_mission("mensch_0") + # So ordnet ein Spieler zu: jede Technik an den Moment, wo sie am besten wirkt. + for id in Game.known: + var bester := "" + var beste_chance := 0.0 + for activity in HumanDB.aktivitaeten("mensch_0"): + if not Game.erreichbar(activity) or Game.regel_fuer("mensch_0", activity) != "": + continue + var ev := Game.evaluate(id, "mensch_0", activity, false) + if float(ev["chance"]) > beste_chance: + beste_chance = float(ev["chance"]) + bester = activity + if bester != "": + Game.regel_setzen("mensch_0", bester, id) + + main._erde() + await get_tree().process_frame + main.player.global_position = Vector3(0, 30.0, 60.0) # der Spieler tut nichts + Game.time_h = 7.0 + Game.day = 1 + for i in range(140000): + schritte(1) + if Game.completed.has("mensch_0") or Game.day > 14: + break + return Game.day + + +func test_erster_traum_schaffbar() -> void: + abschnitt("Der erste Traum ist mit dem Startkapital zu schaffen") + 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.regel_anzahl("mensch_0") == 1, + "Der trägt genau eine Regel (%d)" % Game.regel_anzahl("mensch_0")) + + var schlimmster: int = tage.max() + var bester: int = tage.min() + p(schlimmster <= 14, "Der Traum erfüllt sich in allen %d Läufen ohne Zutun (%s)" % + [tage.size(), str(tage)]) + p(bester >= 2, "Nie geschenkt — mindestens %d Tage" % bester) + p(schlimmster <= 8, "Nie zäh — höchstens %d Tage (von Hand geht es schneller)" % + schlimmster) + p(Game.einsicht >= 3, "Der erfüllte Traum bringt 3 Einsicht (%d)" % Game.einsicht) + p(Game.offene_techniken() > 1, + "Und öffnet neue Wege: %d entdeckt" % Game.offene_techniken())