This commit is contained in:
Team3
2026-08-30 19:56:47 +02:00
parent a46eba4145
commit f936dd2b2a
13 changed files with 1681 additions and 444 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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]

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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()

View File

@@ -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()

View File

@@ -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))

View File

@@ -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())