This commit is contained in:
team3
2026-08-31 11:07:01 +02:00
parent f936dd2b2a
commit ef2ecc08f9
19 changed files with 2094 additions and 438 deletions

View File

@@ -1,24 +1,27 @@
78098b89c88a74ebe8c20117b8754620
::res://::1787996624
anforderungen.md::TextFile::-1::1788038502::0::1::::<><><>0<>0<><>::
README.md::TextFile::-1::1788038596::0::1::::<><><>0<>0<><>::
::res://::1788113016
anforderungen.md::TextFile::-1::1788165575::0::1::::<><><>0<>0<><>::
README.md::TextFile::-1::1788165599::0::1::::<><><>0<>0<><>::
::res://scenes/::1787989294
main.tscn::PackedScene::-1::1787989294::0::1::::<><><>0<>0<><>::res://scripts/main.gd
::res://scripts/::1788039082
bewohner.gd::GDScript::2125278938812678811::1788039082::0::1::::Bewohner<>Node3D<><>0<>0<><>::
dream_db.gd::GDScript::4217518896355437099::1788039046::0::1::::DreamDB<>RefCounted<><>0<>0<><>::
dream_world.gd::GDScript::8279158278942867258::1788039140::0::1::::DreamWorld<>Node3D<><>0<>0<><>::
earth_world.gd::GDScript::6086818627272428934::1788039160::0::1::::EarthWorld<>Node3D<><>0<>0<><>::
game.gd::GDScript::3600922541500868814::1788038470::0::1::::<>Node<><>0<>0<><>::
human_db.gd::GDScript::412604892032291296::1788039057::0::1::::HumanDB<>RefCounted<><>0<>0<><>::
main.gd::GDScript::1504009331368977934::1788039160::0::1::::<>Node3D<><>0<>0<><>::
orte.gd::GDScript::5536340784294498671::1788039046::0::1::::Orte<>RefCounted<><>0<>0<><>::
player.gd::GDScript::8724108548430371398::1787990898::0::1::::Player<>CharacterBody3D<><>0<>0<><>::
prims.gd::GDScript::322632462553361584::1787990855::0::1::::Prims<>RefCounted<><>0<>0<><>::
technique_db.gd::GDScript::4543286561661639701::1788039160::0::1::::TechDB<>RefCounted<><>0<>0<><>::
ui.gd::GDScript::843471304695966286::1788039160::0::1::::UI<>CanvasLayer<><>0<>0<><>::
::res://tests/::1787990698
shots.gd::GDScript::5999651346126871724::1788038553::0::1::::<>Node<><>0<>0<><>::
::res://scripts/::1788163668
bewohner.gd::GDScript::2125278938812678811::1788165374::0::1::::Bewohner<>Node3D<><>0<>0<><>::
dream_db.gd::GDScript::4217518896355437099::1788164505::0::1::::DreamDB<>RefCounted<><>0<>0<><>::
dream_world.gd::GDScript::8279158278942867258::1788164873::0::1::::DreamWorld<>Node3D<><>0<>0<><>::
earth_world.gd::GDScript::6086818627272428934::1788165412::0::1::::EarthWorld<>Node3D<><>0<>0<><>::
game.gd::GDScript::3600922541500868814::1788165125::0::1::::<>Node<><>0<>0<><>::
human_db.gd::GDScript::412604892032291296::1788165374::0::1::::HumanDB<>RefCounted<><>0<>0<><>::
main.gd::GDScript::1504009331368977934::1788121650::0::1::::<>Node3D<><>0<>0<><>::
netz.gd::GDScript::8422943321868648339::1788163667::0::1::::Netz<>RefCounted<><>0<>0<><>::
orte.gd::GDScript::5536340784294498671::1788163839::0::1::::Orte<>RefCounted<><>0<>0<><>::
player.gd::GDScript::8724108548430371398::1788039718::0::1::::Player<>CharacterBody3D<><>0<>0<><>::
prims.gd::GDScript::322632462553361584::1788039797::0::1::::Prims<>RefCounted<><>0<>0<><>::
technique_db.gd::GDScript::4543286561661639701::1788120975::0::1::::TechDB<>RefCounted<><>0<>0<><>::
ui.gd::GDScript::843471304695966286::1788122559::0::1::::UI<>CanvasLayer<><>0<>0<><>::
::res://tests/::1788164088
probe.gd::GDScript::5788011518062194748::1788165350::0::1::::<>Node<><>0<>0<><>::
probe.tscn::PackedScene::-1::1788163874::0::1::::<><><>0<>0<><>::res://tests/probe.gd
shots.gd::GDScript::5999651346126871724::1788118441::0::1::::<>Node<><>0<>0<><>::
shots.tscn::PackedScene::-1::1787990528::0::1::::<><><>0<>0<><>::res://tests/shots.gd
verify.gd::GDScript::4392606218392155269::1788038533::0::1::::<>Node<><>0<>0<><>::
verify.gd::GDScript::4392606218392155269::1788165669::0::1::::<>Node<><>0<>0<><>::
verify.tscn::PackedScene::-1::1787989983::0::1::::<><><>0<>0<><>::res://tests/verify.gd

View File

@@ -40,6 +40,14 @@ list=[{
"path": "res://scripts/human_db.gd"
}, {
"base": &"RefCounted",
"class": &"Netz",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/netz.gd"
}, {
"base": &"RefCounted",
"class": &"Orte",
"icon": "",
"is_abstract": false,

Binary file not shown.

114
README.md
View File

@@ -11,7 +11,7 @@ sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt.
godot --path .
Steuerung: **WASD** bewegen · **Maus** umsehen · **E** interagieren ·
**J** Journal · **Esc** Maus freigeben.
**J** Journal · **1/2/3** Tempo · **Esc** Maus freigeben.
In der Traumwelt springt **Leertaste**; auf der Erde steigt sie und **Umschalt**
lässt sinken.
@@ -21,7 +21,8 @@ lässt sinken.
Das Journal führt Buch — es lehrt und vergibt nichts.
* **Techniken** — was du gelernt hast, mit Stufe und Erfahrung.
* **Techniken** — was du gelernt hast, mit Stufe und Erfahrung. Besser werden
sie nur draußen.
* **Missionen** — die Menschen, für die du dich entschieden hast, jeweils mit
ihrem **Regelwerk**.
@@ -29,18 +30,29 @@ Das Journal führt Buch — es lehrt und vergibt nichts.
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.
allein, solange du auf der Erde bist.
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.
**Ob eine Regel taugt, sagt dir niemand.** Keine Prozentzahl, kein „passt zum
Thema", keine Liste guter Momente. Du bekommst nur die Technik selbst: was sie
tut, wann sie passt, warum sie hilft. Und den Menschen: seinen Traum und das,
was im Weg steht. Beides zusammenzubringen ist das Spiel.
Eine Technik darf an beliebig vielen Tätigkeiten hängen. Sie überall
hinzuhängen ist trotzdem falsch: im falschen Moment greift sie kaum und wirft
den Traum um 14 % zurück. Nach jedem Einsatz braucht sie außerdem 30 Sekunden
Ruhe — ein Fehlgriff verbrennt also den Platz, den ein guter Moment gehabt
hätte. Mehr Momente sauber abzudecken heißt, 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
Eine Regel greift immer wieder, solange der Moment anhält — im Takt des
Cooldowns. Wie viel ein Tag einbringt, hängt also daran, wie lange ein Mensch
in der passenden Tätigkeit bleibt.
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.
@@ -48,42 +60,74 @@ 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
**Traumwelt** — eine schwebende Insel mit einem Dorf, das wächst
* 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.
gelehrter Weg — und den bringt dein Wesen schon mit. Am Brett hängt also vom
ersten Moment an eine Mission.
* Jeder erfüllte Traum bringt einen Bewohner mit seinem Haus. **In jedem Haus
wird ein Weg gelehrt, den es vorher nirgends gab.**
* Das Haus setzt sich nicht auf einen Ring, sondern **sucht sich seinen Platz**:
gewertet werden Nachbarschaft, Nähe zu einem Weg, Nähe zum Dorfplatz und wie
leer die Himmelsrichtung noch ist. Der beste von 64 Probepunkten gewinnt.
* Der Weg dorthin **dockt an der nächsten Stelle des bestehenden Netzes an** —
nicht an der Dorfmitte. Kreuzt die Gerade ein Haus, sucht ein Raster einen Weg
darum herum. Liegt ein Knoten nah, ist aber im Netz weit weg, kommt eine
Abkürzung dazu. Daraus entstehen Gabeln, Schleifen und krumme Gassen.
* Die Insel dehnt sich mit, Bäume füllen nach Poisson-Regel die freie Fläche.
Vorbild: Emilien et al., *Procedural Generation of Villages* (2012).
* **Schwarzes Brett** am Dorfplatz — hier hängen die Menschen aus, die nicht
weiterkommen. Mission annehmen.
weiterkommen. **Ausgehängt wird nur, wem du mit dem, was du kannst, auch
helfen kannst:** mindestens eine gelernte Technik muss ihr Thema treffen und
in ihrem Tag einen Moment dafür finden. Die anderen bleiben verdeckt, bis du
den passenden Weg gelernt hast.
**Erde** — eine Kleinstadt, ein Tag dauert 6 Minuten
**Tempo** — **1**, **2**, **3**
Drei Stufen wie in Die Sims. Die Stufe ist nicht der Faktor:
Stufe 1 1× ein Tag in 6:00 Minuten
Stufe 2 3× ein Tag in 2:00 Minuten
Stufe 3 7× ein Tag in 0:51 Minuten
Über der Nacht des Menschen, dem du gerade hilfst, springt das Spiel von allein
auf Stufe 3 und fällt beim Aufwachen auf deine zurück — Schlafende erreicht
ohnehin keine Technik. Beschleunigt wird die ganze Simulation samt Cooldown und
Abwehr. Am Ausgang ändert sich dadurch nichts, nur an der Wartezeit.
**Erde** — ein Stadtbezirk nach japanischem Vorbild
* **Arterien** in einem groben, leicht versetzten Raster spannen Blöcke auf.
Am Blockrand stehen die Häuser direkt an der Straße, ohne Vorgarten. Ins
Blockinnere führt eine schmale Sackgasse — *roji* —, an der die hinteren
Parzellen hängen. Der mittlere Block bleibt Park.
* **Hier wächst nichts.** Alle 16 Menschen sind von Anfang an da und leben
ihren Tag: schlafen, arbeiten, grübeln, essen, spazieren. Wer wo wohnt,
arbeitet in der Nähe; das Café liegt in der Mitte.
* Du bist ein unsichtbarer Geist, schwebst und gehst durch Wände.
* Vier Menschen leben ihren Tagesablauf: schlafen, arbeiten, grübeln, essen,
spazieren.
* Nah herangehen, **E** drücken, Technik wählen. Das Panel zeigt für jede
Technik die Erfolgschance in genau dieser Situation.
* Nah herangehen, **E** drücken, Technik wählen. Das Panel nennt für jede
Technik nur, ob Thema und Moment passen — keine Zahl.
* Die Säule im Park bringt dich zurück in die Traumwelt.
## Erfolgschance
8 % + 11 % je Fertigkeitsstufe
+ 25 % wenn die Technik das Thema des Menschen trifft
+ 22 % wenn der Moment passt (z. B. Box-Atmung, während jemand angespannt ist)
5 % + 9 % je Fertigkeitsstufe
+ 20 % wenn die Technik das Thema des Menschen trifft
+ 35 % wenn der Moment passt (z. B. Box-Atmung, während jemand angespannt ist)
− 30 % wenn der Mensch gerade abblockt
begrenzt auf 5 … 95 %
Schlafende sind nicht erreichbar. Erfolg bringt Fortschritt und 2 EP, ein
Fehlversuch 1 EP und eine kurze Sperre. Dieselbe Technik wirkt bei Wiederholung
schwächer — es lohnt sich, mehrere zu lernen. 100 % erfüllen den Traum und
bringen 3 Einsicht zurück.
Fehlversuch 1 EP, −14 % Fortschritt und 18 Sekunden Abwehr. Jeder Einsatz sperrt
die Technik für 30 Sekunden. Dieselbe Technik wirkt bei Wiederholung schwächer —
es lohnt sich, mehrere zu lernen. 100 % erfüllen den Traum.
**Eine Technik wird nur besser, indem du sie anwendest.** Der Lehrort gibt sie
dir, mehr nicht: kein Üben, kein Trainieren, keine Währung, die man dagegen
eintauschen könnte. Wer eine Technik auf Stufe 5 will, muss sie draußen bei
Menschen einsetzen — auch Fehlversuche zählen.
## Aufbau
@@ -91,8 +135,9 @@ bringen 3 Einsicht zurück.
scenes/main.tscn Einstieg
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, erzeugte Wohnhäuser und Menschen
scripts/dream_db.gd Traumdorf: erzeugt Häuser, Bewohner und Lehrorte
scripts/human_db.gd Bezirk: Arterien, Blöcke, roji, Parzellen, Menschen
scripts/dream_db.gd Traumdorf: wachsende Häuser, Wege, Bewohner, Lehrorte
scripts/netz.gd Wegenetz: Knoten, Kanten, Andocken, kürzester Weg
scripts/orte.gd Orte, Türen, Wege, Tätigkeiten (beide Welten)
scripts/bewohner.gd Bewohner mit Tagesablauf, löst Regeln aus
scripts/main.gd Weltenwechsel, Fokus, Interaktion
@@ -108,15 +153,18 @@ bringen 3 Einsicht zurück.
godot --path . tests/shots.tscn # 28 Screenshots nach user://shots/
`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,
Tagesabläufen, gewachsenes Dorf und erzeugter Bezirk,
Häuser betreten, Journal in beiden Welten, im Hörsaal lernen,
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,
Tagesabläufe über einen ganzen Tag, Wirkung der Technikwahl, Cooldown,
verdeckte Erfolgschance, keine Passungshinweise, Mehrfachbelegung von Regeln,
Tempo und Nachtsprung,
nur lösbare Aushänge, Mission abschließen,
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
Mehrere 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

View File

@@ -9,7 +9,15 @@ Loops
Funktionen
- Bewegung: Bewegen, Springen, Geist
- Journal: Gelernte Techniken, Akzetierte Missionen
- Techniken: Lernen, Verbessern
- Missionen: Pipeline
- Techniken: Lernen am Lehrort, Verbessern durch Anwenden
- Missionen: Pipeline, nur lösbare werden angeboten
- Techniken: Cooldown, Fehlschlag kostet Fortschritt
- Erfolgschance und Passung bleiben verdeckt
- Eine Technik darf an mehreren Tätigkeiten hängen
- Menschen: Tagesablauf
- Prozedurale Generierung: Dorf, Erde
- Tempo: Stufe 1, 2, 3 wie in Die Sims (1×, 3×, 7×)
- Im Schlaf des Menschen automatisch Stufe 3
- Prozedurale Generierung
- Dorf: wächst Haus für Haus, verwinkelte Wege, grün und gemütlich
- Erde: Stadtbezirk nach japanischem Vorbild, Arterien und roji
- Auf der Erde sind alle Menschen von Anfang an da

View File

@@ -3,7 +3,7 @@ class_name Bewohner
## Bewohner einer Welt: läuft seinen Tagesablauf ab. Auf der Erde kann er von
## einer Technik erreicht werden -- oder eben nicht.
const GEHTEMPO := 6.0
const GEHTEMPO := 8.0
var data: Dictionary
var idx: int = 0
@@ -11,6 +11,8 @@ var place: String = ""
var activity: String = ""
var resist_left: float = 0.0
var places: Dictionary = {}
var netz: Netz = null
var anker: Dictionary = {}
var mit_mission := false
var _visual: Node3D
@@ -21,14 +23,18 @@ var _aura: MeshInstance3D
var _strahl: MeshInstance3D
var _path: Array = []
var _letzte_aktivitaet: String = ""
var _seit_regel: float = 0.0
var _bob: float = 0.0
var _offset: Vector3
func setup(d: Dictionary, index: int, orte: Dictionary) -> void:
func setup(d: Dictionary, index: int, orte: Dictionary,
wegenetz: Netz = null, wegeanker: Dictionary = {}) -> void:
data = d
idx = index
places = orte
netz = wegenetz
anker = wegeanker
mit_mission = bool(d.get("mission", false))
name = "Bewohner_" + str(d["id"])
var winkel := float(index) * TAU / 4.0
@@ -73,13 +79,14 @@ func _routine_at(h: float) -> Array:
return [cur[1], cur[2]]
func _process(delta: float) -> void:
func _process(roh: float) -> void:
var delta := roh * Game.zeit_faktor()
resist_left = maxf(0.0, resist_left - delta)
var soll := _routine_at(Game.time_h)
if soll[0] != place:
var war_unterwegs := not _path.is_empty()
_path = Orte.route(places, place, soll[0], _offset)
_path = Orte.route(places, place, soll[0], _offset, netz, anker)
if war_unterwegs and _path.size() > 1:
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
place = soll[0]
@@ -110,8 +117,15 @@ 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))
# Solange der Moment anhält, greift die Regel erneut, sobald ihre Technik
# wieder frei ist. Der Cooldown gibt damit den Takt vor.
_seit_regel += delta
if activity != _letzte_aktivitaet:
_letzte_aktivitaet = activity
_seit_regel = 0.0
_regel_pruefen()
elif _seit_regel >= 1.0:
_seit_regel = 0.0
_regel_pruefen()
var erledigt: bool = mit_mission and Game.completed.has(data["id"])

View File

@@ -1,15 +1,17 @@
class_name DreamDB
## 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.
## Fest sind nur Hörsaal, Dorfplatz und Tor zur Erde. Jedes weitere Haus sucht
## sich seinen Platz selbst: es wird dort gebaut, wo es Nachbarn, einen Weg und
## den Dorfplatz in erträglicher Nähe hat. Der Weg dorthin dockt an der
## nächsten Stelle des bestehenden Netzes an. Weil Wiederverwendung gratis ist,
## gabeln sich die Wege und werden verwinkelt, statt sternförmig zu laufen.
## Vorbild: Emilien et al., "Procedural Generation of Villages" (2012).
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]
const PLATZ_FREI := 22.0 # Radius um den Dorfplatz, der leer bleibt
const HAUS_ABSTAND := 4.0 # Mindestluft zwischen zwei Gebäuden
const WEG_ABSTAND := 3.0 # Mindestluft zwischen Haus und Weg
const KANDIDATEN := 64 # Probepunkte je Haus
const SPINE_SCHRITT := 8.0 # Abstand der Knoten auf der Hauptachse
static var FEST: Dictionary = {
"hoersaal": {
@@ -29,6 +31,8 @@ static var FEST: Dictionary = {
## Wird von erzeuge() gefüllt und wächst mit dem Dorf.
static var PLACES: Dictionary = FEST.duplicate(true)
static var LIST: Array = []
static var NETZ: Netz = Netz.new()
static var ANKER: Dictionary = {}
static var FESTE_NAMEN: Array = ["Ailen", "Morr", "Ivo", "Sela"]
static var FESTE_ROLLEN: Array = ["Traumweberin", "Erinnerungssammler", "Bote",
@@ -56,47 +60,346 @@ 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.
## Baut Orte, Wegenetz und Bewohner für ein Dorf mit so vielen Häusern neu auf.
## Deterministisch: Haus i landet immer am selben Fleck.
static func erzeuge(haeuser: int) -> void:
PLACES = FEST.duplicate(true)
LIST = []
NETZ = Netz.new()
ANKER = {}
_hauptachse()
for i in range(maxi(haeuser, 0)):
PLACES[haus_key(i)] = _haus(i)
var haus := _platz_suchen(i)
PLACES[haus_key(i)] = haus
_anschliessen(haus_key(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
## Die feste Achse Hörsaal – Dorfplatz – Tor. Sie ist von Anfang an da und
## trägt genug Knoten, damit Häuser überall andocken können.
static func _hauptachse() -> void:
var punkte: Array[Vector3] = []
var a := Orte.door(PLACES, "hoersaal")
var m := Orte.center(PLACES, "lichtung")
var b := Orte.door(PLACES, "tor")
for paar in [[a, m], [m, b]]:
var von: Vector3 = paar[0]
var nach: Vector3 = paar[1]
var n := maxi(1, int(round(von.distance_to(nach) / SPINE_SCHRITT)))
for k in range(n + 1):
var p := von.lerp(nach, float(k) / float(n))
if punkte.is_empty() or punkte[punkte.size() - 1].distance_to(p) > 0.6:
punkte.append(p)
var vorher := -1
for p in punkte:
var idx := NETZ.fuege_knoten(p)
if vorher >= 0:
NETZ.verbinde(vorher, idx)
vorher = idx
ANKER["hoersaal"] = NETZ.fuege_knoten(a)
ANKER["lichtung"] = NETZ.fuege_knoten(m)
ANKER["tor"] = NETZ.fuege_knoten(b)
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
# ------------------------------------------------------------ Platzsuche
## Wählt unter vielen Probepunkten den mit dem höchsten Interesse.
static func _platz_suchen(i: int) -> Dictionary:
var rng := RandomNumberGenerator.new()
rng.seed = 4711 + i * 7919
var breite := 12.0 + float(i % 2) * 2.0
var tiefe := 10.0 + float(i % 3)
var groesse := Vector2(breite, tiefe)
# Die Suchfläche wächst mit der Zahl der Häuser, nicht mit dem weitesten
# Ausreißer. So bleibt die Dichte gleich, statt zu zerfasern.
var aussen := maxf(_wachstumsradius() + 8.0,
sqrt(PLATZ_FREI * PLATZ_FREI + 240.0 * float(i + 1)))
var bester := Vector3.ZERO
var bestes := -1.0
for k in range(KANDIDATEN):
var winkel := rng.randf() * TAU
var r := PLATZ_FREI + rng.randf() * maxf(6.0, aussen - PLATZ_FREI)
var p := Vector3(cos(winkel) * r, 0.0, sin(winkel) * r)
if not _platz_frei(p, groesse):
continue
var wert := _interesse(p)
if wert > bestes:
bestes = wert
bester = p
if bestes < 0.0:
# Notnagel: irgendwo weit außen, damit das Dorf nie steckenbleibt
bester = _notplatz(i, groesse)
return {
"name": "%s Haus" % _besitz(_name(i)),
"pos": [x, z],
"pos": [bester.x, bester.z],
"size": [breite, tiefe],
"dir": _tuer_zur_mitte(x, z),
"dir": _tuer_zum_netz(bester, groesse),
"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"
## Wie weit reichen die Häuser? Danach richtet sich, wo neu gesucht wird --
## nicht nach Hörsaal und Tor, sonst zerfasert das Dorf sofort.
static func _wachstumsradius() -> float:
var r := PLATZ_FREI + 8.0
for key in PLACES:
if not key.begins_with("haus_"):
continue
var s: Array = PLACES[key]["size"]
var c := Orte.center(PLACES, key)
r = maxf(r, Vector2(absf(c.x) + float(s[0]) * 0.5,
absf(c.z) + float(s[1]) * 0.5).length())
return r
static func _notplatz(i: int, groesse: Vector2) -> Vector3:
var r := aussenradius() + 20.0
for k in range(72):
var a := TAU * float((i * 29 + k) % 72) / 72.0
var p := Vector3(cos(a) * r, 0.0, sin(a) * r)
if _platz_frei(p, groesse):
return p
return Vector3(0, 0, r)
## Harte Bedingungen: nicht auf dem Dorfplatz, nicht in einem anderen Gebäude,
## nicht auf einem Weg.
static func _platz_frei(p: Vector3, groesse: Vector2) -> bool:
if p.length() - maxf(groesse.x, groesse.y) * 0.5 < PLATZ_FREI:
return false
for key in PLACES:
var s: Array = PLACES[key]["size"]
if float(s[0]) <= 0.0:
continue
var c := Orte.center(PLACES, key)
var dx: float = absf(p.x - c.x) - (groesse.x + float(s[0])) * 0.5
var dz: float = absf(p.z - c.z) - (groesse.y + float(s[1])) * 0.5
if dx < HAUS_ABSTAND and dz < HAUS_ABSTAND:
return false
for k in NETZ.kanten:
if Netz.trifft_rechteck(NETZ.knoten[k[0]], NETZ.knoten[k[1]], p, groesse, WEG_ABSTAND):
return false
return true
## Wie gut liegt dieser Punkt? Nachbarschaft, Weganschluss, Nähe zum Platz.
static func _interesse(p: Vector3) -> float:
var sozial := 0.0
var n := 0
for key in PLACES:
if not key.begins_with("haus_"):
continue
var d := Orte.center(PLACES, key).distance_to(p)
sozial += _glocke(d, 16.0, 21.0, 46.0)
n += 1
if n > 0:
sozial = sozial / sqrt(float(n))
var an := NETZ.andocken(p)
var weg := _glocke(float(an["dist"]), 3.0, 9.0, 34.0)
var platz := _glocke(p.length(), PLATZ_FREI - 4.0, PLATZ_FREI + 6.0, 110.0)
return 1.0 * sozial + 1.3 * weg + 0.5 * platz + 0.8 * _freiraum(p)
## Richtungen, in denen schon viel steht, sind weniger interessant. Ohne das
## wächst das Dorf einseitig davon und die halbe Insel bleibt leer.
static func _freiraum(p: Vector3) -> float:
var w := atan2(p.z, p.x)
var nah := 0.0
var alle := 0.0
for key in PLACES:
if not key.begins_with("haus_"):
continue
var c := Orte.center(PLACES, key)
alle += 1.0
if absf(angle_difference(atan2(c.z, c.x), w)) < PI / 3.0:
nah += 1.0
if alle < 1.0:
return 1.0
return clampf(1.0 - 3.0 * nah / alle, 0.0, 1.0)
## Dreiecksglocke: 0 unter lo, 1 bei spitze, 0 über hi.
static func _glocke(d: float, lo: float, spitze: float, hi: float) -> float:
if d <= lo or d >= hi:
return 0.0
if d < spitze:
return (d - lo) / maxf(0.001, spitze - lo)
return (hi - d) / maxf(0.001, hi - spitze)
## Die Tür zeigt auf die Seite, die dem Wegenetz am nächsten liegt.
static func _tuer_zum_netz(p: Vector3, groesse: Vector2) -> String:
var ziel: Vector3 = NETZ.andocken(p)["punkt"]
var d := ziel - p
if absf(d.x) * groesse.y > absf(d.z) * groesse.x:
return "left" if d.x < 0.0 else "right"
return "forward" if d.z < 0.0 else "back"
# ------------------------------------------------------------ Weganschluss
## Verbindet ein neues Haus mit dem Netz. Der Weg geht um Häuser herum, statt
## durch sie hindurch. Weil er am nächsten Punkt des Netzes andockt und nicht
## in der Dorfmitte, gabeln sich die Wege und werden verwinkelt.
static func _anschliessen(key: String) -> void:
var tuer := Orte.door(PLACES, key)
var an := NETZ.andocken(tuer)
var von: Vector3 = an["punkt"]
var linie := _route_um_haeuser(tuer, von, key)
var vorher := -1
for i in range(linie.size()):
var idx := NETZ.fuege_knoten(linie[i])
if vorher >= 0:
NETZ.verbinde(vorher, idx)
vorher = idx
if i == 0:
ANKER[key] = idx
if vorher >= 0:
NETZ.verbinde(vorher, int(an["knoten"]))
if not ANKER.has(key):
ANKER[key] = NETZ.fuege_knoten(tuer)
_schleife(int(ANKER[key]), key)
## Weg von a nach b, der kein Haus schneidet. Frei? Dann die Gerade. Sonst
## Dijkstra über ein grobes Raster, danach auf wenige Knicke geglättet.
static func _route_um_haeuser(a: Vector3, b: Vector3, ausser: String) -> Array:
if _weg_frei(a, b, ausser):
return [a, b]
var rand := 30.0
var min_x := minf(a.x, b.x) - rand
var max_x := maxf(a.x, b.x) + rand
var min_z := minf(a.z, b.z) - rand
var max_z := maxf(a.z, b.z) + rand
var schritt := 5.0
var nx := int((max_x - min_x) / schritt) + 1
var nz := int((max_z - min_z) / schritt) + 1
while nx * nz > 2600:
schritt *= 1.5
nx = int((max_x - min_x) / schritt) + 1
nz = int((max_z - min_z) / schritt) + 1
var start := Vector2i(clampi(int(round((a.x - min_x) / schritt)), 0, nx - 1),
clampi(int(round((a.z - min_z) / schritt)), 0, nz - 1))
var ziel := Vector2i(clampi(int(round((b.x - min_x) / schritt)), 0, nx - 1),
clampi(int(round((b.z - min_z) / schritt)), 0, nz - 1))
var kosten: Dictionary = {start: 0.0}
var her: Dictionary = {}
var offen: Array = [start]
var gefunden := false
while not offen.is_empty():
var best := 0
for i in range(1, offen.size()):
if float(kosten[offen[i]]) < float(kosten[offen[best]]):
best = i
var c: Vector2i = offen[best]
offen.remove_at(best)
if c == ziel:
gefunden = true
break
var pa := Vector3(min_x + float(c.x) * schritt, 0.0, min_z + float(c.y) * schritt)
for dx in [-1, 0, 1]:
for dz in [-1, 0, 1]:
if dx == 0 and dz == 0:
continue
var n := Vector2i(c.x + dx, c.y + dz)
if n.x < 0 or n.y < 0 or n.x >= nx or n.y >= nz:
continue
var pb := Vector3(min_x + float(n.x) * schritt, 0.0,
min_z + float(n.y) * schritt)
if not _weg_frei(pa, pb, ausser):
continue
var neu: float = float(kosten[c]) + pa.distance_to(pb)
if not kosten.has(n) or neu < float(kosten[n]) - 0.001:
kosten[n] = neu
her[n] = c
if not offen.has(n):
offen.append(n)
if not gefunden:
return [a, b]
var roh: Array = []
var cur: Vector2i = ziel
while cur != start:
roh.push_front(Vector3(min_x + float(cur.x) * schritt, 0.0,
min_z + float(cur.y) * schritt))
cur = her[cur]
roh.push_front(a)
roh.append(b)
return _glaetten(roh, ausser)
## Zieht die Rasterspur auf wenige Knicke zusammen: immer so weit springen,
## wie die Sicht frei ist.
static func _glaetten(linie: Array, ausser: String) -> Array:
var out: Array = [linie[0]]
var i := 0
while i < linie.size() - 1:
var j := linie.size() - 1
while j > i + 1 and not _weg_frei(linie[i], linie[j], ausser):
j -= 1
out.append(linie[j])
i = j
return out
## Ein Umweg über das halbe Dorf ist unschön. Liegt ein Knoten nah, ist aber im
## Netz weit weg, wird abgekürzt. Daraus entstehen Schleifen statt Sackgassen.
static func _schleife(neu: int, key: String) -> void:
var p: Vector3 = NETZ.knoten[neu]
var bester := -1
var beste := INF
for i in range(NETZ.knoten.size()):
if i == neu:
continue
var d := NETZ.knoten[i].distance_to(p)
if d > 52.0 or d < 11.0 or d >= beste:
continue
if NETZ.weglaenge(neu, i) < d * 1.9:
continue
if not _weg_frei(p, NETZ.knoten[i], key):
continue
beste = d
bester = i
if bester >= 0:
NETZ.verbinde(neu, bester)
## Kein Weg darf durch ein Gebäude laufen -- auch nicht durch das eigene.
## Am eigenen Haus reicht knappe Luft, sonst käme die Tür nie frei.
static func _weg_frei(a: Vector3, b: Vector3, ausser: String) -> bool:
for key in PLACES:
var s: Array = PLACES[key]["size"]
if float(s[0]) <= 0.0:
continue
var c := Orte.center(PLACES, key)
var rand := 0.2 if key == ausser else 1.0
if Netz.trifft_rechteck(a, b, c, Vector2(float(s[0]), float(s[1])), rand):
return false
return true
# ------------------------------------------------------------ Maße
## Wie weit reicht das Dorf hinaus?
static func aussenradius() -> float:
var r := 0.0
for key in PLACES:
var s: Array = PLACES[key]["size"]
var c := Orte.center(PLACES, key)
var ecke := Vector2(absf(c.x) + float(s[0]) * 0.5, absf(c.z) + float(s[1]) * 0.5)
r = maxf(r, ecke.length())
for k in NETZ.knoten:
r = maxf(r, k.length() + 2.0)
return r
# ------------------------------------------------------------ Bewohner
static func _bewohner(i: int) -> Dictionary:
var haus := haus_key(i)
var ablauf: Array = []
@@ -120,7 +423,7 @@ static func _name(i: int) -> String:
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.
## Genitiv: "Ailens", aber "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"

View File

@@ -7,6 +7,7 @@ var spawn := Vector3(0, 2.0, 20.0)
var bewohner: Array = []
var _deko: Array = []
var _freihalten: Array[Vector3] = []
func _ready() -> void:
@@ -17,6 +18,7 @@ func _ready() -> void:
_schwarzes_brett()
_lichtung()
_tor_zur_erde()
_gruen()
_wesen()
@@ -59,18 +61,14 @@ func _insel() -> void:
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):
var a := TAU * float(i) / 36.0
Prims.box(self, Vector3(cos(a) * 15.0, 0.03, sin(a) * 15.0),
Vector3(2.6, 0.06, 2.6), Color(0.66, 0.58, 0.84), false)
_wege()
# schwebende Deko
var rng := RandomNumberGenerator.new()
rng.seed = 7
for i in range(34):
var a := rng.randf() * TAU
var r := rng.randf_range(18.0, Game.insel_radius() - 4.0)
var r := rng.randf_range(18.0, maxf(24.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)
@@ -79,6 +77,91 @@ func _insel() -> void:
_deko.append(wuerfel)
## Die gewachsenen Wege: je Kante ein flaches Band, an den Knoten ein Fleck,
## damit Ecken nicht ausfransen.
## Begrünung nach Regel, nicht von Hand: Poisson-Scheibe über der freien
## Fläche. Nichts wächst auf einem Weg, in einem Haus oder auf dem Dorfplatz.
func _gruen() -> void:
_freihalten.append(Vector3(spawn.x, 0.0, spawn.z))
var radius := Game.insel_radius() - 4.0
var rng := RandomNumberGenerator.new()
rng.seed = 91
var gesetzt: Array[Vector3] = []
var abstand := 9.0
for versuch in range(1400):
var a := rng.randf() * TAU
var r := sqrt(rng.randf()) * radius
var p := Vector3(cos(a) * r, 0.0, sin(a) * r)
if p.length() < 20.0:
continue
var frei := true
for g in gesetzt:
if g.distance_to(p) < abstand:
frei = false
break
if not frei:
continue
if not _freie_stelle(p, 3.2):
continue
gesetzt.append(p)
_baum(p, rng)
func _freie_stelle(p: Vector3, luft: float) -> bool:
for frei in _freihalten:
if p.distance_to(frei) < 7.0:
return false
for key in DreamDB.PLACES:
var s: Array = DreamDB.PLACES[key]["size"]
if float(s[0]) <= 0.0 or not Game.ort_offen(key):
continue
var c := Orte.center(DreamDB.PLACES, key)
if absf(p.x - c.x) < float(s[0]) * 0.5 + luft \
and absf(p.z - c.z) < float(s[1]) * 0.5 + luft:
return false
for k in DreamDB.NETZ.kanten:
if Netz.trifft_rechteck(DreamDB.NETZ.knoten[k[0]], DreamDB.NETZ.knoten[k[1]],
p, Vector2(luft * 2.0, luft * 2.0), 0.6):
return false
return true
## Traumbäume: schlanker Stamm, zwei versetzte Kronen, leuchtende Spitze.
func _baum(p: Vector3, rng: RandomNumberGenerator) -> void:
var hoehe := rng.randf_range(3.0, 6.0)
# Grün gegen den lila Grund -- das macht die Insel erst gemütlich.
var ton := Color.from_hsv(0.26 + rng.randf() * 0.12, 0.48 + rng.randf() * 0.18,
0.58 + rng.randf() * 0.26)
Prims.cyl(self, p + Vector3(0, hoehe * 0.5, 0), 0.24, hoehe,
Color(0.34, 0.26, 0.30))
var k1 := Prims.sphere(self, p + Vector3(0, hoehe + 0.4, 0),
rng.randf_range(1.1, 2.0), ton)
k1.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
if rng.randf() < 0.6:
var k2 := Prims.sphere(self, p + Vector3(rng.randf_range(-0.8, 0.8),
hoehe - 0.7, rng.randf_range(-0.8, 0.8)), rng.randf_range(0.8, 1.5),
ton.lightened(0.12))
k2.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
if rng.randf() < 0.3:
Prims.sphere(self, p + Vector3(0, hoehe + 1.6, 0), 0.28,
Color(0.9, 0.95, 1.0), 1.6)
func _wege() -> void:
var farbe := Color(0.66, 0.58, 0.84)
for k in DreamDB.NETZ.kanten:
var a: Vector3 = DreamDB.NETZ.knoten[k[0]]
var b: Vector3 = DreamDB.NETZ.knoten[k[1]]
var d := b - a
if d.length() < 0.2:
continue
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, 0.03, 0),
Vector3(2.6, 0.06, d.length()), farbe, false)
band.rotation.y = atan2(d.x, d.z)
for k in DreamDB.NETZ.knoten:
Prims.cyl(self, k + Vector3(0, 0.035, 0), 1.3, 0.06, farbe, false)
func _haeuser() -> void:
for key in DreamDB.PLACES:
var pl: Dictionary = DreamDB.PLACES[key]
@@ -194,8 +277,10 @@ func _schwarzes_brett() -> void:
_marke(pos + Vector3(0, 1.6, 1.2), "brett", "Schwarzes Brett")
## Unsichtbarer Punkt, an dem die Interaktion hängt.
## Unsichtbarer Punkt, an dem die Interaktion hängt. Um jede Marke bleibt ein
## Stück frei, damit später kein Baum davorsteht.
func _marke(pos: Vector3, kind: String, titel: String) -> Node3D:
_freihalten.append(Vector3(pos.x, 0.0, pos.z))
var n := Node3D.new()
n.position = pos
add_child(n)
@@ -243,7 +328,7 @@ func _wesen() -> void:
continue
var w := Bewohner.new()
add_child(w)
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES)
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES, DreamDB.NETZ, DreamDB.ANKER)
bewohner.append(w)

View File

@@ -14,6 +14,7 @@ var _nachtlichter: Array = []
func _ready() -> void:
_environment()
_boden()
_bebauung()
_orte()
_park()
_saeule()
@@ -44,107 +45,142 @@ func _environment() -> void:
func _boden() -> void:
var weite := 240.0
Prims.box(self, Vector3(0, -0.53, 0), Vector3(300, 1, 300), Color(0.42, 0.48, 0.40))
_strassen()
## Arterien breit, roji schmal -- beide als flaches Band mit Belag.
func _strassen() -> void:
for st in HumanDB.STRASSEN:
var a: Vector3 = st["a"]
var b: Vector3 = st["b"]
var d := b - a
if d.length() < 0.2:
continue
var breit := float(st["breite"])
var farbe := Color(0.62, 0.60, 0.58) if breit > 5.0 else Color(0.55, 0.53, 0.52)
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, 0.02, 0),
Vector3(breit, 0.06, d.length() + breit), farbe, false)
band.rotation.y = atan2(d.x, d.z)
## Alles, was nicht Wohnung oder Sonderbau ist, steht als geschlossener Bau da.
## Erst dadurch wird der Bezirk dicht.
func _bebauung() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = 8123
var belegt: Dictionary = {}
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
Prims.box(self, Vector3(cos(a) * 15.0, 0.03, sin(a) * 15.0),
Vector3(2.8, 0.06, 2.8), Color(0.62, 0.58, 0.50), false)
var s: Array = HumanDB.PLACES[key]["size"]
if float(s[0]) > 0.0:
belegt[Orte.center(HumanDB.PLACES, key).snapped(Vector3.ONE * 0.1)] = true
for p in HumanDB.PARZELLEN:
var c: Vector3 = p["pos"]
if belegt.has(c.snapped(Vector3.ONE * 0.1)):
continue
var g: Vector2 = p["size"]
if minf(g.x, g.y) < 3.0:
continue
var hoehe := rng.randf_range(4.0, 9.0)
var ton := Color.from_hsv(fmod(0.07 + rng.randf() * 0.12, 1.0),
0.10 + rng.randf() * 0.14, 0.62 + rng.randf() * 0.26)
Prims.box(self, c + Vector3(0, hoehe * 0.5, 0), Vector3(g.x, hoehe, g.y), ton)
Prims.box(self, c + Vector3(0, hoehe + 0.2, 0),
Vector3(g.x + 0.5, 0.4, g.y + 0.5), ton.darkened(0.25))
if rng.randf() < 0.35:
Prims.box(self, c + Vector3(0, hoehe + 0.9, 0),
Vector3(g.x * 0.4, 1.0, g.y * 0.4), ton.darkened(0.1))
## Wohnungen und Sonderbauten stehen offen, damit der Geist von oben hineinsieht.
func _orte() -> void:
for key in HumanDB.PLACES:
var pl: Dictionary = HumanDB.PLACES[key]
if float(pl["size"][0]) <= 0.0:
var s: Array = pl["size"]
if float(s[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])
var groesse := Vector3(float(s[0]), 2.3, float(s[1]))
var farbe: Color = pl["color"]
var richtung: Vector3 = Orte.DIR_VEC[pl["dir"]]
Prims.room(self, c, groesse, farbe, farbe.darkened(0.35), richtung, 3.4, true)
Prims.label(self, c + Vector3(0, 5.0, 0), pl["name"], 0.6, Color(1, 1, 0.9))
var tuer: float = minf(3.0, minf(groesse.x, groesse.z) - 3.0)
Prims.room(self, c, groesse, farbe, farbe.darkened(0.35),
Orte.DIR_VEC[pl["dir"]], maxf(1.6, tuer), true)
Prims.label(self, c + Vector3(0, 4.4, 0), str(pl["name"]), 0.55, Color(1, 1, 0.9))
_einrichtung(c, groesse, farbe, key)
var tuer := Orte.door(HumanDB.PLACES, key)
var weg := tuer.normalized() * 15.0 - tuer
var b := Prims.box(self, (tuer + tuer.normalized() * 15.0) * 0.5 + Vector3(0, 0.03, 0),
Vector3(2.6, 0.06, weg.length()), Color(0.62, 0.58, 0.50), false)
b.rotation.y = atan2(weg.x, weg.z)
var l := OmniLight3D.new()
l.position = c + Vector3(0, 2.0, 0)
l.omni_range = 20.0
l.light_color = Color(1.0, 0.85, 0.6)
l.light_energy = 0.0
add_child(l)
_nachtlichter.append(l)
var licht := OmniLight3D.new()
licht.position = c + Vector3(0, 2.0, 0)
licht.omni_range = 16.0
licht.light_color = Color(1.0, 0.85, 0.6)
licht.light_energy = 0.0
add_child(licht)
_nachtlichter.append(licht)
## Ein paar Möbel, damit man sieht, wofür ein Raum da ist.
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("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),
farbe.darkened(0.5)) # Schreibtisch
Prims.box(self, c + Vector3(w - 1.0, 1.05, 0), Vector3(0.9, 0.6, 0.1),
Color(0.2, 0.25, 0.3)) # Bildschirm
var w := maxf(0.5, groesse.x * 0.5 - 2.0)
var h := maxf(0.5, groesse.z * 0.5 - 2.0)
if key.begins_with("heim_"):
Prims.box(self, c + Vector3(-w * 0.6, 0.35, -h * 0.5), Vector3(1.9, 0.7, 3.0),
Color(0.85, 0.85, 0.90))
Prims.box(self, c + Vector3(w * 0.6, 0.75, 0), Vector3(1.5, 0.15, 2.2),
farbe.darkened(0.5))
Prims.box(self, c + Vector3(w * 0.6, 1.05, 0), Vector3(0.8, 0.55, 0.1),
Color(0.2, 0.25, 0.3))
elif key == "cafe":
for i in range(3):
Prims.cyl(self, c + Vector3(-3.0 + i * 3.0, 0.4, 0.5), 0.8, 0.8,
Prims.cyl(self, c + Vector3(-w * 0.5 + float(i) * w * 0.5, 0.4, 0.4), 0.7, 0.8,
Color(0.55, 0.40, 0.30))
Prims.box(self, c + Vector3(0, 0.55, -3.2), Vector3(groesse.x - 3.0, 1.1, 0.8),
Prims.box(self, c + Vector3(0, 0.55, -h * 0.7), Vector3(groesse.x - 3.0, 1.1, 0.8),
Color(0.45, 0.32, 0.24))
elif key == "buero":
for i in range(4):
Prims.box(self, c + Vector3(-4.5 + i * 3.0, 0.75, -1.0), Vector3(2.2, 0.15, 1.4),
Color(0.75, 0.75, 0.78))
for i in range(3):
Prims.box(self, c + Vector3(-w * 0.5 + float(i) * w * 0.5, 0.75, 0),
Vector3(1.6, 0.15, 1.2), Color(0.75, 0.75, 0.78))
elif key == "baeckerei":
Prims.box(self, c + Vector3(0, 0.55, 2.0), Vector3(groesse.x - 3.0, 1.1, 1.0),
Prims.box(self, c + Vector3(0, 0.55, h * 0.5), Vector3(groesse.x - 3.0, 1.1, 1.0),
Color(0.70, 0.55, 0.35))
Prims.box(self, c + Vector3(0, 0.85, -3.0), Vector3(3.0, 1.7, 1.5), Color(0.4, 0.4, 0.42))
Prims.box(self, c + Vector3(0, 0.85, -h * 0.6), Vector3(2.6, 1.7, 1.2),
Color(0.4, 0.4, 0.42))
elif key == "uni":
for i in range(3):
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))
Prims.box(self, c + Vector3(0, 0.5, -h * 0.6 + float(i) * h * 0.6),
Vector3(groesse.x - 3.0, 0.2, 0.7), Color(0.66, 0.62, 0.58))
Prims.box(self, c + Vector3(0, 1.1, -h), Vector3(groesse.x * 0.55, 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)
Prims.box(self, c + Vector3(0, 0.6, 0), Vector3(groesse.x - 4.0, 1.2, 1.4),
Color(0.52, 0.48, 0.46))
## Der mittlere Block bleibt frei: Rasen, Teich, Bäume, Bänke. Der Park ist
## der einzige Ort im Bezirk, an dem nicht gebaut wird.
func _park() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = 3
Prims.cyl(self, Vector3(-6, 0.05, 2), 4.0, 0.12, Color(0.30, 0.50, 0.72), false)
for i in range(14):
var a := rng.randf() * TAU
var r := rng.randf_range(5.0, 12.0)
var p := Vector3(cos(a) * r, 0, sin(a) * r + 2.0)
if p.length() < 4.0:
var halb: float = (float(HumanDB.ACHSEN[2]) - float(HumanDB.ACHSEN[1])) * 0.5 \
- HumanDB.ARTERIE * 0.5
Prims.box(self, Vector3(0, 0.01, 0), Vector3(halb * 2.0, 0.05, halb * 2.0),
Color(0.40, 0.58, 0.34), false)
Prims.cyl(self, Vector3(-halb * 0.4, 0.06, halb * 0.25), halb * 0.28, 0.1,
Color(0.30, 0.50, 0.72), false)
for i in range(26):
var p := Vector3(rng.randf_range(-halb, halb), 0, rng.randf_range(-halb, halb))
if p.distance_to(Vector3(-halb * 0.4, 0, halb * 0.25)) < halb * 0.38:
continue
var hoehe := rng.randf_range(2.4, 4.2)
if p.distance_to(Vector3(0, 0, 12.0)) < 6.0:
continue
var hoehe := rng.randf_range(2.6, 5.0)
Prims.cyl(self, p + Vector3(0, hoehe * 0.5, 0), 0.3, hoehe, Color(0.35, 0.26, 0.18))
Prims.sphere(self, p + Vector3(0, hoehe + 0.8, 0), rng.randf_range(1.2, 1.9),
Color(0.25, 0.5 + rng.randf() * 0.2, 0.22))
for i in range(4):
var a := TAU * float(i) / 4.0 + 0.4
var p := Vector3(cos(a) * 8.0, 0.45, sin(a) * 8.0 + 2.0)
Prims.sphere(self, p + Vector3(0, hoehe + 0.9, 0), rng.randf_range(1.3, 2.2),
Color(0.24, 0.48 + rng.randf() * 0.22, 0.22))
for i in range(5):
var a := TAU * float(i) / 5.0 + 0.4
var p := Vector3(cos(a) * halb * 0.62, 0.45, sin(a) * halb * 0.62)
var b := Prims.box(self, p, Vector3(2.4, 0.15, 0.7), Color(0.55, 0.40, 0.28))
b.rotation.y = -a
Prims.label(self, Vector3(0, 5.0, 10.0), "Stadtpark", 0.55, Color(1, 1, 0.9))
Prims.label(self, Vector3(0, 5.0, halb - 3.0), "Stadtpark", 0.6, Color(1, 1, 0.9))
func _saeule() -> void:
@@ -167,7 +203,7 @@ func _menschen() -> void:
for i in range(HumanDB.LIST.size()):
var m := Bewohner.new()
add_child(m)
m.setup(HumanDB.LIST[i], i, HumanDB.PLACES)
m.setup(HumanDB.LIST[i], i, HumanDB.PLACES, HumanDB.NETZ, HumanDB.ANKER)
humans.append(m)

View File

@@ -9,22 +9,40 @@ const DAY_SECONDS := 360.0 # eine Erdenrunde in Echtzeit-Sekunden
const XP_LEVELS := [0, 4, 10, 18, 28]
const MISSION_ZIEL := 100.0
const RESIST_SEKUNDEN := 18.0
## Drei Stufen wie in Die Sims: normal, schnell, sehr schnell. Die Stufe ist
## nicht der Faktor — Stufe 2 läuft dreifach, Stufe 3 siebenfach.
const TEMPO_FAKTOR := [1.0, 3.0, 7.0]
const TECHNIK_COOLDOWN := 30.0 # eine Technik braucht Luft, bevor sie wieder greift
const FEHLVERSUCH_VERLUST := 14.0 # ein Fehlschlag wirft den Traum zurück
# Der Verlust ist mit Absicht so hoch, dass sich nur eine Technik lohnt, die
# Thema UND Moment trifft. Wer eine Regel blind auf jede Tätigkeit hängt,
# verliert unterm Strich — die Wahl muss aus der Technik gelesen werden.
var einsicht: int = 3
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 cooldown: Dictionary = {} # tech_id -> Restsekunden
var pipeline: Dictionary = {} # human_id -> {activity -> tech_id}
var completed: Array = []
var time_h: float = 7.0
var day: int = 1
var teleport_lock: float = 0.0
var tempo: int = 1 # 1, 2 oder 3 -- vom Spieler gewählt
func _ready() -> void:
_setup_input()
welt_erzeugen()
_startweg_lernen()
## Den ersten Weg bringt das Wesen schon mit. Ohne ihn hinge am Schwarzen Brett
## nichts aus, und der erste Gang wäre eine Pflichtübung.
func _startweg_lernen() -> void:
for id in DreamDB.techniken_von("hoersaal"):
if not known.has(str(id)):
known[str(id)] = 0
func _setup_input() -> void:
@@ -37,6 +55,9 @@ func _setup_input() -> void:
"sinken": [KEY_SHIFT],
"interagieren": [KEY_E],
"journal": [KEY_J, KEY_TAB],
"tempo_1": [KEY_1, KEY_KP_1],
"tempo_2": [KEY_2, KEY_KP_2],
"tempo_3": [KEY_3, KEY_KP_3],
}
for action in binds:
if not InputMap.has_action(action):
@@ -47,7 +68,47 @@ func _setup_input() -> void:
InputMap.action_add_event(action, ev)
## Schläft der Mensch, dem du gerade hilfst? Dann ist ohnehin nichts zu holen.
func schlaeft_ziel() -> bool:
if active_mission == "":
return false
var h := HumanDB.by_id(active_mission)
if h.is_empty():
return false
var r: Array = h["routine"]
var cur: Array = r[r.size() - 1]
for e in r:
if time_h >= float(e[0]):
cur = e
return not erreichbar(str(cur[2]))
## Um wie viel schneller läuft die Welt gerade? Über der Nacht des Menschen
## läuft sie immer auf der höchsten Stufe -- warten bringt dort nichts.
func zeit_faktor() -> float:
return faktor_von(3 if schlaeft_ziel() else tempo)
static func faktor_von(stufe: int) -> float:
return float(TEMPO_FAKTOR[clampi(stufe, 1, 3) - 1])
func tempo_setzen(stufe: int) -> void:
var neu := clampi(stufe, 1, 3)
if neu == tempo:
return
tempo = neu
notified.emit("Tempo %d — %d×" % [tempo, int(faktor_von(tempo))], Color(0.8, 0.9, 1))
state_changed.emit()
func advance_time(delta: float) -> void:
for id in cooldown.keys():
var rest: float = float(cooldown[id]) - delta
if rest <= 0.0:
cooldown.erase(id)
else:
cooldown[id] = rest
time_h += delta * 24.0 / DAY_SECONDS
while time_h >= 24.0:
time_h -= 24.0
@@ -75,7 +136,6 @@ func time_string() -> String:
# lehrt eine Technik, die es vorher nirgends gab.
const INSEL_GRUND := 46.0
const OFFENE_MISSIONEN := 3
func haeuser() -> int:
@@ -83,14 +143,13 @@ func haeuser() -> int:
func insel_radius() -> float:
var ring := DreamDB.aeusserster_ring(haeuser())
return maxf(INSEL_GRUND, ring + 14.0)
return maxf(INSEL_GRUND, DreamDB.aussenradius() + 12.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)
HumanDB.erzeuge()
## Das Traumdorf beginnt leer. Mit jedem erfüllten Traum zieht ein Wesen ein.
@@ -117,7 +176,21 @@ func offene_techniken() -> int:
return n
# ------------------------------------------------------------------- Cooldown
# Jede Anwendung sperrt ihre Technik für eine Weile. Wer einen ganzen Tag
# abdecken will, braucht deshalb mehrere Wege — einer allein kommt nicht nach.
func cooldown_rest(tech_id: String) -> float:
return float(cooldown.get(tech_id, 0.0))
func bereit(tech_id: String) -> bool:
return cooldown_rest(tech_id) <= 0.0
# ---------------------------------------------------------------- Fertigkeiten
# Gelernt wird an einem Lehrort, besser wird eine Technik nur durch Anwendung.
# Es gibt keinen Weg, sie am Schreibtisch zu trainieren.
func knows(tech_id: String) -> bool:
return known.has(tech_id)
@@ -149,10 +222,6 @@ func learn(tech_id: String) -> bool:
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
einsicht -= 1
known[tech_id] = 0
var t := TechDB.by_id(tech_id)
notified.emit("Technik gelernt: %s" % t["name"], Color(0.6, 1, 0.7))
@@ -171,18 +240,6 @@ func add_xp(tech_id: String, amount: int) -> void:
state_changed.emit()
func deepen(tech_id: String) -> bool:
if not known.has(tech_id):
return false
if einsicht < 1:
notified.emit("Keine Einsicht übrig.", Color(1, 0.6, 0.4))
return false
einsicht -= 1
add_xp(tech_id, 3)
notified.emit("Vertieft: %s (+3 EP)" % TechDB.by_id(tech_id)["name"], Color(0.7, 0.9, 1))
return true
# ------------------------------------------------------------------ Missionen
func accept_mission(human_id: String) -> bool:
@@ -204,6 +261,40 @@ func angenommen(human_id: String) -> bool:
return progress.has(human_id)
## Eine Mission ist machbar, wenn mindestens eine gelernte Technik das Thema
## des Menschen trifft und in seinem Tag auch einen Moment dafür findet.
## Nur solche werden am Schwarzen Brett überhaupt ausgehängt.
func machbar(human_id: String) -> bool:
var h := HumanDB.by_id(human_id)
if h.is_empty():
return false
var tag_liste: Array = HumanDB.aktivitaeten(human_id)
for id in known:
var t := TechDB.by_id(id)
var thema := false
for tag in t["tags"]:
if (h["tags"] as Array).has(tag):
thema = true
break
if not thema:
continue
for a in t["momente"]:
if tag_liste.has(a) and erreichbar(a):
return true
return false
## Die Menschen, die gerade am Brett hängen können.
func offene_missionen() -> Array:
var out: Array = []
for h in HumanDB.LIST:
var id: String = h["id"]
if completed.has(id) or not machbar(id):
continue
out.append(id)
return out
func mission_progress(human_id: String) -> float:
return progress.get(human_id, 0.0)
@@ -242,17 +333,17 @@ 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:
## An welchen Tätigkeiten hängt diese Technik gerade?
func regel_momente(human_id: String, tech_id: String) -> Array:
var out: Array = []
for activity in regeln(human_id):
if regeln(human_id)[activity] == tech_id:
return activity
return ""
out.append(activity)
return out
## 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.
## Leere tech_id löscht die Regel. Eine Technik darf an beliebig vielen
## Tätigkeiten hängen — ob das klug ist, muss der Spieler aus ihr herauslesen.
func regel_setzen(human_id: String, activity: String, tech_id: String) -> void:
if not pipeline.has(human_id):
pipeline[human_id] = {}
@@ -260,23 +351,20 @@ func regel_setzen(human_id: String, activity: String, tech_id: String) -> void:
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?
## Greift für diese Tätigkeit eine Regel? Wird nicht nur beim Wechsel gefragt,
## sondern immer wieder — solange der Moment anhält und die Technik frei ist.
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 {}
if not bereit(tech_id):
return {} # still: die Technik greift wieder, sobald ihre Sperre fällt
var res := apply_technique(tech_id, human_id, activity, resisting)
if not res["ok"]:
return {}
@@ -287,8 +375,8 @@ func regel_ausloesen(human_id: String, activity: String, resisting: bool) -> Dic
[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))],
notified.emit("Regel läuft ins Leere — %s bei »%s« (−%d %%)" %
[t["name"], Orte.text(activity), int(round(float(res["verlust"])))],
Color(1, 0.7, 0.5))
return res
@@ -324,11 +412,13 @@ func evaluate(tech_id: String, human_id: String, activity: String, resisting: bo
break
res["moment"] = t["momente"].has(activity)
var c := 0.08 + 0.11 * float(skill)
# Der Moment wiegt am schwersten: eine Technik zur falschen Zeit greift kaum,
# egal wie gut sie zum Menschen passt. Genau das steht in ihrem "Wann".
var c := 0.05 + 0.09 * float(skill)
if res["tag"]:
c += 0.25
c += 0.20
if res["moment"]:
c += 0.22
c += 0.35
if resisting:
c -= 0.30
res["grund"] = "Der Mensch macht gerade dicht."
@@ -338,9 +428,13 @@ func evaluate(tech_id: String, human_id: String, activity: String, resisting: bo
## Wendet die Technik wirklich an: würfelt, verteilt EP und Fortschritt.
func apply_technique(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary:
if not bereit(tech_id):
return {"ok": false, "grund": "%s ist noch nicht wieder einsatzbereit." %
TechDB.by_id(tech_id)["name"]}
var ev := evaluate(tech_id, human_id, activity, resisting)
if ev["blocked"]:
return {"ok": false, "grund": ev["grund"]}
cooldown[tech_id] = TECHNIK_COOLDOWN
var roll := randf()
var success: bool = roll < float(ev["chance"])
@@ -349,6 +443,7 @@ func apply_technique(tech_id: String, human_id: String, activity: String, resist
used[key] = wiederholung + 1
var gain := 0.0
var verlust := 0.0
if success:
var base := 8.0 + 2.0 * float(ev["skill"])
if ev["tag"]:
@@ -358,25 +453,25 @@ func apply_technique(tech_id: String, human_id: String, activity: String, resist
gain = base / (1.0 + 0.6 * float(wiederholung))
add_xp(tech_id, 2)
else:
verlust = FEHLVERSUCH_VERLUST
add_xp(tech_id, 1)
var done := false
if human_id == active_mission and gain > 0.0:
var p: float = progress.get(human_id, 0.0) + gain
progress[human_id] = minf(p, MISSION_ZIEL)
if human_id == active_mission:
var p: float = progress.get(human_id, 0.0) + gain - verlust
progress[human_id] = clampf(p, 0.0, MISSION_ZIEL)
if progress[human_id] >= MISSION_ZIEL and not completed.has(human_id):
completed.append(human_id)
active_mission = ""
einsicht += 3
done = true
mission_done.emit(human_id)
notified.emit("%s hat es geschafft. (+3 Einsicht)" % HumanDB.by_id(human_id)["name"],
notified.emit("%s hat es geschafft." % HumanDB.by_id(human_id)["name"],
Color(1, 0.85, 0.4))
_insel_waechst()
state_changed.emit()
return {
"ok": true, "success": success, "chance": ev["chance"], "gain": gain,
"wiederholung": wiederholung, "mission_done": done,
"verlust": verlust, "wiederholung": wiederholung, "mission_done": done,
"tag": ev["tag"], "moment": ev["moment"],
}

View File

@@ -1,40 +1,38 @@
class_name HumanDB
## 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.
## Die Erde: ein Stadtbezirk nach japanischem Vorbild. Weite Arterien in einem
## groben Raster spannen Blöcke auf. Am Blockrand stehen die Häuser direkt an
## der Straße, ohne Vorgarten. Ins Blockinnere führen schmale Sackgassen --
## roji --, an denen die hinteren Parzellen hängen. So entsteht die typische
## Mischung aus klarer Ordnung außen und Gewinkel innen.
##
## Anders als das Traumdorf wächst hier nichts: alle Menschen sind von Anfang
## an da und leben ihren Tag.
const HAEUSER_JE_RING := 8
const RING_ERSTER := 62.0
const RING_ABSTAND := 16.0
const EINWOHNER := 16
const ACHSEN := [-72.0, -24.0, 24.0, 72.0] # Arterien in x wie in z
const ARTERIE := 7.0 # Breite einer Arterie
const ROJI := 3.2 # Breite einer Gasse
const RAND_TIEFE := 11.0 # Tiefe der Parzellen an der Straße
const PARZELLE_MIN := 8.0 # schmalste Parzelle
const PARZELLE_MAX := 12.0
## Gebäude liegen im Ring um den Park. Alle Türen zeigen zur Mitte.
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),
},
"cafe": {
"name": "Café Nadel", "pos": [0.0, -27.0], "size": [12.0, 10.0],
"dir": "back", "color": Color(0.82, 0.62, 0.45),
},
"buero": {
"name": "Büro Ostwind", "pos": [28.0, -27.0], "size": [14.0, 10.0],
"dir": "back", "color": Color(0.62, 0.66, 0.72),
},
"baeckerei": {
"name": "Bäckerei Keller", "pos": [-31.0, 0.0], "size": [10.0, 12.0],
"dir": "right", "color": Color(0.85, 0.72, 0.50),
},
"werkstatt": {
"name": "Werkstatt Loos", "pos": [31.0, 0.0], "size": [10.0, 12.0],
"dir": "left", "color": Color(0.70, 0.58, 0.55),
},
"park": {
"name": "Stadtpark", "pos": [0.0, 4.0], "size": [0.0, 0.0],
"dir": "none", "color": Color(0.45, 0.65, 0.38),
},
}
## Alle Parzellen des Bezirks: {pos, size, dir, strasse}
static var PARZELLEN: Array = []
## Alle Straßenbänder: {a, b, breite}
static var STRASSEN: Array = []
static var NETZ: Netz = Netz.new()
static var ANKER: Dictionary = {}
static var PLACES: Dictionary = FEST.duplicate(true)
## Die fünf Sonderbauten. Sie bekommen eine Parzelle an einer Arterie.
static var SONDER: Array = [
["uni", "Hochschule", Color(0.72, 0.70, 0.66)],
["cafe", "Café Nadel", Color(0.82, 0.62, 0.45)],
["buero", "Büro Ostwind", Color(0.62, 0.66, 0.72)],
["baeckerei", "Bäckerei Keller", Color(0.85, 0.72, 0.50)],
["werkstatt", "Werkstatt Loos", Color(0.70, 0.58, 0.55)],
]
static var PLACES: Dictionary = {}
static var LIST: Array = []
## Arbeitsplätze, die ein erzeugter Mensch bekommen kann.
@@ -125,46 +123,299 @@ static var ABLAEUFE: Array = [
]
## Zu jeder Technik ein Anliegen und ein Tagesablauf, bei dem sie greift:
## [Anliegen, Ablauf]. Mensch i bekommt den Eintrag zu Technik i — und Technik i
## ist genau dann entdeckt, wenn i Träume erfüllt sind. So hängt am Schwarzen
## Brett immer mindestens ein Mensch, dem mit dem Gelernten zu helfen ist.
static var PASSEND: Array = [
[0, 0], # Pomodoro -> aufschieben, zögert
[6, 4], # Zwei-Minuten -> Werkstatt, räumt auf
[4, 3], # Wenn-Dann -> Gewohnheit, Morgenroutine
[1, 1], # Gewohnheiten -> Klavier, isst / Abendroutine
[9, 0], # Zerlegen -> Überforderung, grübelt
[0, 4], # Umgebung -> Ablenkung, räumt auf
[8, 0], # Umdeuten -> Selbstzweifel, grübelt
[7, 2], # Box-Atmung -> Angst, angespannt
[8, 2], # Selbstmitgefühl -> Rückschlag, grübelt
[4, 1], # Schlaffenster -> Erschöpfung, ruht
[5, 2], # Hilfe bitten -> Einsamkeit, wartet
[3, 3], # Nein sagen -> Grenzen, arbeitet
]
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)
## Baut den ganzen Bezirk neu auf: Straßen, Parzellen, Sonderbauten, Wohnhäuser.
## Die Zahl der Menschen liegt fest -- sie sind alle von Anfang an da.
static func erzeuge(_ungenutzt: int = 0) -> void:
_bezirk()
PLACES = {}
LIST = []
for i in range(maxi(menschen, 0)):
PLACES[heim_key(i)] = _heim(i)
_park_eintragen()
_sonderbauten_verteilen()
_wohnhaeuser_verteilen()
_anker_setzen()
for i in range(EINWOHNER):
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),
# ------------------------------------------------------------ Bezirk
## Arterien im Raster, Blöcke dazwischen, roji ins Blockinnere, Parzellen
## an jeder Straße. Danach steht das Straßennetz, auf dem alle laufen.
static func _bezirk() -> void:
PARZELLEN = []
STRASSEN = []
NETZ = Netz.new()
ANKER = {}
var rng := RandomNumberGenerator.new()
rng.seed = 31337
# Arterien: die Achsen leicht versetzt, damit kein reines Schachbrett steht
var xs: Array = []
var zs: Array = []
for i in range(ACHSEN.size()):
xs.append(float(ACHSEN[i]) + rng.randf_range(-3.0, 3.0))
zs.append(float(ACHSEN[i]) + rng.randf_range(-3.0, 3.0))
var von: float = float(ACHSEN[0]) - 6.0
var bis: float = float(ACHSEN[ACHSEN.size() - 1]) + 6.0
for x in xs:
_strasse(Vector3(x, 0, von), Vector3(x, 0, bis), ARTERIE)
for z in zs:
_strasse(Vector3(von, 0, z), Vector3(bis, 0, z), ARTERIE)
# Blöcke zwischen den Arterien
for ix in range(xs.size() - 1):
for iz in range(zs.size() - 1):
var x0: float = float(xs[ix]) + ARTERIE * 0.5
var x1: float = float(xs[ix + 1]) - ARTERIE * 0.5
var z0: float = float(zs[iz]) + ARTERIE * 0.5
var z1: float = float(zs[iz + 1]) - ARTERIE * 0.5
if ix == 1 and iz == 1:
continue # die Mitte bleibt Park
_block(Vector3((x0 + x1) * 0.5, 0, (z0 + z1) * 0.5),
Vector2(x1 - x0, z1 - z0), rng)
_netz_bauen()
static func _strasse(a: Vector3, b: Vector3, breite: float) -> void:
STRASSEN.append({"a": a, "b": b, "breite": breite})
## Aus den Straßenbändern ein zusammenhängendes Netz machen: an jeder Kreuzung
## sitzt ein Knoten, dazwischen alle 16 Meter einer.
static func _netz_bauen() -> void:
for i in range(STRASSEN.size()):
var a: Vector3 = STRASSEN[i]["a"]
var b: Vector3 = STRASSEN[i]["b"]
var laenge := a.distance_to(b)
if laenge < 0.5:
continue
var stellen: Array = [0.0, 1.0]
for j in range(STRASSEN.size()):
if j == i:
continue
var t := _kreuzung(a, b, STRASSEN[j]["a"], STRASSEN[j]["b"])
if t >= 0.0:
stellen.append(t)
stellen.sort()
var fein: Array = [float(stellen[0])]
for k in range(1, stellen.size()):
var von: float = float(fein[fein.size() - 1])
var bis: float = float(stellen[k])
if bis - von < 0.001:
continue
var n := maxi(1, int((bis - von) * laenge / 16.0))
for m in range(1, n + 1):
fein.append(von + (bis - von) * float(m) / float(n))
var vorher := -1
for t2 in fein:
var idx := NETZ.fuege_knoten(a.lerp(b, float(t2)))
if vorher >= 0:
NETZ.verbinde(vorher, idx)
vorher = idx
## Parameter t auf der Strecke a-b, an dem sie c-d schneidet. -1 wenn nicht.
static func _kreuzung(a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> float:
var r := b - a
var s2 := d - c
var nenner := r.x * s2.z - r.z * s2.x
if absf(nenner) < 0.0001:
return -1.0
var t := ((c.x - a.x) * s2.z - (c.z - a.z) * s2.x) / nenner
var u := ((c.x - a.x) * r.z - (c.z - a.z) * r.x) / nenner
if t < 0.0 or t > 1.0 or u < 0.0 or u > 1.0:
return -1.0
return t
## Ein Block: außen ein Kranz von Parzellen an den Arterien, innen eine Gasse
## mit den hinteren Parzellen daran. Die Nord- und Südreihe laufen über die
## volle Breite, die Ost- und Westreihe nur dazwischen -- sonst überlappen die
## Ecken.
static func _block(mitte: Vector3, groesse: Vector2, rng: RandomNumberGenerator) -> void:
var tiefe := minf(RAND_TIEFE, minf(groesse.x, groesse.y) * 0.34)
var seiten := [
["back", Vector3(0, 0, 1), groesse.x, groesse.y],
["forward", Vector3(0, 0, -1), groesse.x, groesse.y],
["right", Vector3(1, 0, 0), groesse.y - tiefe * 2.0, groesse.x],
["left", Vector3(-1, 0, 0), groesse.y - tiefe * 2.0, groesse.x],
]
for eintrag in seiten:
var dir: String = eintrag[0]
var n: Vector3 = eintrag[1]
var laenge: float = eintrag[2]
var quer_mass: float = eintrag[3]
if laenge < PARZELLE_MIN:
continue
var quer := Vector3(absf(n.z), 0, absf(n.x))
var anzahl := maxi(1, int(round(laenge / rng.randf_range(PARZELLE_MIN + 1.0,
PARZELLE_MAX))))
var breite := laenge / float(anzahl)
var rand := mitte + n * (quer_mass * 0.5 - tiefe * 0.5)
for k in range(anzahl):
var versatz := (float(k) + 0.5 - float(anzahl) * 0.5) * breite
var quer_ist := absf(n.z) > 0.0
PARZELLEN.append({
"pos": rand + quer * versatz,
"size": Vector2(breite - 0.8, tiefe - 0.8) if quer_ist
else Vector2(tiefe - 0.8, breite - 0.8),
"dir": dir,
})
# Blockinneres: eine Gasse von der längeren Seite hinein
var innen := groesse - Vector2(tiefe, tiefe) * 2.0
if innen.x < 12.0 or innen.y < 12.0:
return
var laengs := innen.y >= innen.x
var richtung := Vector3(0, 0, -1) if laengs else Vector3(-1, 0, 0)
# Die Gasse setzt an der Arterie an, sonst hinge sie in der Luft
var aussen: float = (groesse.y if laengs else groesse.x) * 0.5 + ARTERIE * 0.5
var start := mitte - richtung * aussen
var ende := mitte + richtung * ((innen.y if laengs else innen.x) * 0.5 - 2.0)
_strasse(start, ende, ROJI)
var flanke := Vector3(1, 0, 0) if laengs else Vector3(0, 0, 1)
var p_breite := (innen.x if laengs else innen.y) * 0.5 - ROJI * 0.5 - 0.8
if p_breite < PARZELLE_MIN * 0.8:
return
# Nur der Teil im Blockinneren trägt Parzellen
var innen_start := mitte - richtung * ((innen.y if laengs else innen.x) * 0.5)
var laenge_g := innen_start.distance_to(ende)
var anzahl_g := maxi(1, int(round(laenge_g / 13.0)))
var tief_g := laenge_g / float(anzahl_g) - 0.8
for seite: float in [1.0, -1.0]:
for k in range(anzahl_g):
var t := (float(k) + 0.5) / float(anzahl_g)
var pos := innen_start.lerp(ende, t) + flanke * (seite * (ROJI * 0.5 + p_breite * 0.5))
PARZELLEN.append({
"pos": pos,
"size": Vector2(p_breite, tief_g) if laengs else Vector2(tief_g, p_breite),
"dir": ("left" if seite > 0.0 else "right") if laengs
else ("forward" if seite > 0.0 else "back"),
})
# ------------------------------------------------------------ Belegung
static func _park_eintragen() -> void:
PLACES["park"] = {
"name": "Stadtpark", "pos": [0.0, 0.0], "size": [0.0, 0.0],
"dir": "none", "color": Color(0.45, 0.65, 0.38),
}
## Die Sonderbauten bekommen Parzellen an einer Arterie, gut verteilt.
static func _sonderbauten_verteilen() -> void:
var kandidaten: Array = []
for i in range(PARZELLEN.size()):
var p: Dictionary = PARZELLEN[i]
if minf(p["size"].x, p["size"].y) >= 10.0:
kandidaten.append(i)
for s in range(SONDER.size()):
if kandidaten.is_empty():
break
# Das Café gehört in die Mitte -- jeder Tagesablauf führt dorthin.
# Die übrigen Sonderbauten verteilen sich rundum.
var wunsch := Vector3.ZERO
if str(SONDER[s][0]) != "cafe":
var w := TAU * float(s) / float(SONDER.size() - 1)
wunsch = Vector3(cos(w), 0, sin(w)) * 42.0
var idx: int = -1
var beste := INF
for k in kandidaten:
var q: Dictionary = PARZELLEN[k]
if q.has("belegt"):
continue
var d: float = (q["pos"] as Vector3).distance_to(wunsch)
if d < beste:
beste = d
idx = int(k)
if idx < 0:
continue
var p: Dictionary = PARZELLEN[idx]
p["belegt"] = true
PLACES[str(SONDER[s][0])] = {
"name": str(SONDER[s][1]),
"pos": [p["pos"].x, p["pos"].z],
"size": [p["size"].x, p["size"].y],
"dir": p["dir"],
"color": SONDER[s][2],
}
## Die Wohnhäuser: gleichmäßig über den Bezirk verteilt, damit die Wege
## nicht alle in derselben Ecke enden.
static func _wohnhaeuser_verteilen() -> void:
var frei: Array = []
for i in range(PARZELLEN.size()):
if not PARZELLEN[i].has("belegt"):
frei.append(i)
if frei.is_empty():
return
for i in range(EINWOHNER):
var idx: int = int(frei[(i * 11 + 5) % frei.size()])
var p: Dictionary = PARZELLEN[idx]
if p.has("belegt"):
for versuch in range(frei.size()):
idx = int(frei[(i * 11 + 5 + versuch) % frei.size()])
p = PARZELLEN[idx]
if not p.has("belegt"):
break
p["belegt"] = true
PLACES[heim_key(i)] = {
"name": "%s %d" % [_nachname(i), i + 1],
"pos": [p["pos"].x, p["pos"].z],
"size": [p["size"].x, p["size"].y],
"dir": p["dir"],
"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 _anker_setzen() -> void:
for key in PLACES:
var s: Array = PLACES[key]["size"]
var tuer := Orte.door(PLACES, key)
if float(s[0]) <= 0.0:
tuer = Orte.center(PLACES, key)
ANKER[key] = int(NETZ.andocken(tuer)["knoten"])
static func _mensch(i: int) -> Dictionary:
var anliegen: Dictionary = ANLIEGEN[i % ANLIEGEN.size()]
var arbeit: String = str(ARBEIT[i % ARBEIT.size()])
# Bis alle Wege entdeckt sind, folgt jeder Mensch dem Weg gleicher Nummer.
# Danach mischt sich das Feld wieder.
var a_nr: int = i % ANLIEGEN.size()
var b_nr: int = i % ABLAEUFE.size()
if i < PASSEND.size():
a_nr = int(PASSEND[i][0])
b_nr = int(PASSEND[i][1])
var anliegen: Dictionary = ANLIEGEN[a_nr]
var heim := heim_key(i)
var arbeit := _naechster_arbeitsplatz(heim, i)
var ablauf: Array = []
for e in ABLAEUFE[i % ABLAEUFE.size()]:
for e in ABLAEUFE[b_nr]:
var ort: String = str(e[1])
if ort == "heim":
ort = heim
@@ -186,6 +437,24 @@ static func _mensch(i: int) -> Dictionary:
}
## Wer wo wohnt, arbeitet in der Nähe. Sonst laufen alle quer durch den Bezirk
## und kommen nie an.
static func _naechster_arbeitsplatz(heim: String, i: int) -> String:
if not PLACES.has(heim):
return str(ARBEIT[i % ARBEIT.size()])
var von := Orte.center(PLACES, heim)
var beste := INF
var ziel := str(ARBEIT[i % ARBEIT.size()])
for a in ARBEIT:
if not PLACES.has(a):
continue
var d := Orte.center(PLACES, str(a)).distance_to(von)
if d < beste:
beste = d
ziel = str(a)
return ziel
static func _vorname(i: int) -> String:
return str(VORNAMEN[i % VORNAMEN.size()])

View File

@@ -65,7 +65,7 @@ func _setze_welt(neu: Node3D) -> void:
func _process(delta: float) -> void:
Game.advance_time(delta)
Game.advance_time(delta * Game.zeit_faktor())
Game.teleport_lock = maxf(0.0, Game.teleport_lock - delta)
if ui.offen:
ui.setze_hinweis("")
@@ -132,6 +132,11 @@ func _unhandled_input(event: InputEvent) -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
player.look_enabled = true
return
for stufe in [1, 2, 3]:
if event.is_action_pressed("tempo_%d" % stufe):
Game.tempo_setzen(stufe)
get_viewport().set_input_as_handled()
return
if event.is_action_pressed("journal"):
ui.journal_panel()
get_viewport().set_input_as_handled()

141
scripts/netz.gd Normal file
View File

@@ -0,0 +1,141 @@
class_name Netz
extends RefCounted
## Ein Wegenetz: Knoten und Kanten in der Ebene. Wege wachsen daran, indem
## neue Punkte an die nächste Stelle des bestehenden Netzes angebunden werden.
## Wiederverwendung ist gratis -- deshalb bündeln sich Wege und gabeln sich,
## statt sternförmig zur Mitte zu laufen.
var knoten: Array[Vector3] = []
var kanten: Array = [] # [a, b] Indexpaare
func fuege_knoten(p: Vector3) -> int:
var q := Vector3(p.x, 0.0, p.z)
for i in range(knoten.size()):
if knoten[i].distance_to(q) < 0.6:
return i
knoten.append(q)
return knoten.size() - 1
func verbinde(a: int, b: int) -> void:
if a == b:
return
for k in kanten:
if (k[0] == a and k[1] == b) or (k[0] == b and k[1] == a):
return
kanten.append([a, b])
func nachbarn(i: int) -> Array:
var out: Array = []
for k in kanten:
if k[0] == i:
out.append(k[1])
elif k[1] == i:
out.append(k[0])
return out
## Nächster Punkt auf einer Kante -- gibt {knoten, dist} zurück und teilt die
## Kante, falls der Fußpunkt zwischen zwei Knoten liegt.
func andocken(p: Vector3) -> Dictionary:
var q := Vector3(p.x, 0.0, p.z)
var beste := INF
var best_i := -1
var best_k := -1
var best_p := Vector3.ZERO
for i in range(knoten.size()):
var d := knoten[i].distance_to(q)
if d < beste:
beste = d
best_i = i
best_k = -1
for ki in range(kanten.size()):
var a: Vector3 = knoten[kanten[ki][0]]
var b: Vector3 = knoten[kanten[ki][1]]
var f := Geometry3D.get_closest_point_to_segment(q, a, b)
var d := f.distance_to(q)
if d < beste - 0.01 and f.distance_to(a) > 1.5 and f.distance_to(b) > 1.5:
beste = d
best_k = ki
best_p = f
if best_k < 0:
return {"knoten": best_i, "dist": beste, "punkt": knoten[best_i]}
# Kante teilen
var alt: Array = kanten[best_k]
kanten.remove_at(best_k)
var neu := fuege_knoten(best_p)
verbinde(alt[0], neu)
verbinde(neu, alt[1])
return {"knoten": neu, "dist": beste, "punkt": knoten[neu]}
## Kürzester Weg als Knotenfolge, leer wenn es keinen gibt.
func pfad(start: int, ziel: int) -> Array:
if start == ziel:
return [start]
var offen: Array = [start]
var kosten: Dictionary = {start: 0.0}
var her: Dictionary = {}
while not offen.is_empty():
var best := 0
for i in range(1, offen.size()):
if float(kosten[offen[i]]) < float(kosten[offen[best]]):
best = i
var k: int = offen[best]
offen.remove_at(best)
if k == ziel:
break
for n in nachbarn(k):
var neu: float = float(kosten[k]) + knoten[k].distance_to(knoten[n])
if not kosten.has(n) or neu < float(kosten[n]) - 0.001:
kosten[n] = neu
her[n] = k
if not offen.has(n):
offen.append(n)
if not kosten.has(ziel):
return []
var weg: Array = [ziel]
var cur: int = ziel
while cur != start:
cur = int(her[cur])
weg.push_front(cur)
return weg
func weglaenge(start: int, ziel: int) -> float:
var w := pfad(start, ziel)
if w.size() < 2:
return 0.0 if w.size() == 1 else INF
var l := 0.0
for i in range(w.size() - 1):
l += knoten[w[i]].distance_to(knoten[w[i + 1]])
return l
## Hängen alle Knoten zusammen?
func zusammenhaengend() -> bool:
if knoten.is_empty():
return true
var gesehen: Dictionary = {0: true}
var stapel: Array = [0]
while not stapel.is_empty():
var k: int = stapel.pop_back()
for n in nachbarn(k):
if not gesehen.has(n):
gesehen[n] = true
stapel.append(n)
return gesehen.size() == knoten.size()
## Schneidet die Strecke a-b ein Rechteck (Mittelpunkt c, Größe s, mit Rand)?
static func trifft_rechteck(a: Vector3, b: Vector3, c: Vector3, s: Vector2, rand: float) -> bool:
var hx := s.x * 0.5 + rand
var hz := s.y * 0.5 + rand
var schritte := int(maxf(2.0, a.distance_to(b) / 1.5))
for i in range(schritte + 1):
var p := a.lerp(b, float(i) / float(schritte))
if absf(p.x - c.x) <= hx and absf(p.z - c.z) <= hz:
return true
return false

1
scripts/netz.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://dsaev7wupa8ex

View File

@@ -58,11 +58,22 @@ static func door(places: Dictionary, key: String) -> Vector3:
return c + d * (half + 2.5)
## Route von Ort a nach Ort b: aus der Tür, über den freien Platz, wieder hinein.
static func route(places: Dictionary, a: String, b: String, offset: Vector3) -> Array:
## Route von Ort a nach Ort b. Gibt es ein Wegenetz, wird darauf gelaufen --
## sonst über den freien Platz in der Mitte.
static func route(places: Dictionary, a: String, b: String, offset: Vector3,
netz: Netz = null, anker: Dictionary = {}) -> Array:
var pts: Array = []
if a == b:
return pts
if netz != null and anker.has(a) and anker.has(b):
var weg := netz.pfad(int(anker[a]), int(anker[b]))
if not weg.is_empty():
for idx in weg:
var k: Vector3 = netz.knoten[idx]
if pts.is_empty() or pts[pts.size() - 1].distance_to(k) > 0.6:
pts.append(k)
pts.append(center(places, b) + offset)
return pts
var da := door(places, a)
var db := door(places, b)
pts.append(da)

View File

@@ -9,6 +9,7 @@ static var LIST: Array = [
"was": "25 Minuten am Stück an einer Sache arbeiten, dann 5 Minuten Pause. Nach vier Runden eine lange Pause.",
"wann": "Wenn eine Aufgabe zu groß wirkt und der Anfang immer wieder verschoben wird.",
"warum": "Ein klar begrenzter Zeitraum senkt die Einstiegshürde. Der Kopf muss nur 25 Minuten durchhalten, nicht den ganzen Tag.",
"woran": "jemandem, der zögert — oder der arbeitet und trotzdem nicht vorankommt.",
"tags": ["prokrastination", "fokus"],
"momente": ["arbeitet", "zoegert"],
"farbe": Color(0.95, 0.45, 0.30),
@@ -19,6 +20,7 @@ static var LIST: Array = [
"was": "Was unter zwei Minuten dauert, sofort erledigen. Große Vorhaben auf einen Zwei-Minuten-Anfang schrumpfen.",
"wann": "Bei einem Berg von Kleinkram oder wenn schon der erste Schritt blockiert.",
"warum": "Der Start kostet die meiste Energie. Zwei Minuten sind zu klein zum Ablehnen und ziehen meist die ganze Aufgabe nach sich.",
"woran": "jemandem, der zögert, lieber aufräumt als anzufangen, oder beim Arbeiten im Kleinkram hängt.",
"tags": ["prokrastination", "ueberforderung"],
"momente": ["zoegert", "aufraeumen", "arbeitet"],
"farbe": Color(0.98, 0.72, 0.25),
@@ -29,6 +31,7 @@ static var LIST: Array = [
"was": "Den Vorsatz an einen konkreten Auslöser binden: \"Wenn ich X tue, dann mache ich Y.\"",
"wann": "Wenn ein Vorhaben im Alltag jedes Mal untergeht.",
"warum": "Der Plan hängt an einer Situation statt an Willenskraft. Der Auslöser erinnert von allein.",
"woran": "der Morgenroutine und der Pause — den festen Punkten, an die sich ein Vorsatz hängen lässt.",
"tags": ["gewohnheit", "prokrastination"],
"momente": ["morgenroutine", "pause"],
"farbe": Color(0.45, 0.80, 0.55),
@@ -39,6 +42,7 @@ static var LIST: Array = [
"was": "Eine neue, kleine Gewohnheit direkt an eine bestehende anhängen.",
"wann": "Wenn eine neue Routine nie einen Platz im Tag findet.",
"warum": "Die alte Gewohnheit ist der Anker. Es muss kein neuer Zeitpunkt erfunden werden.",
"woran": "den Ankern des Tages: Morgenroutine, Essen, Abendroutine.",
"tags": ["gewohnheit"],
"momente": ["morgenroutine", "essen", "abendroutine"],
"farbe": Color(0.40, 0.70, 0.85),
@@ -49,6 +53,7 @@ static var LIST: Array = [
"was": "Ein großes Ziel in benennbare Einzelschritte teilen, bis der nächste Schritt trivial ist.",
"wann": "Wenn jemand vor einem Vorhaben erstarrt und nicht weiß, wo er anfangen soll.",
"warum": "Unklarheit lähmt stärker als Aufwand. Ein sichtbarer nächster Schritt macht Handeln wieder möglich.",
"woran": "jemandem, der grübelt statt anzufangen, oder der arbeitet und sich im Vorhaben verliert.",
"tags": ["ueberforderung", "prokrastination", "ziel"],
"momente": ["gruebelt", "arbeitet"],
"farbe": Color(0.75, 0.55, 0.95),
@@ -59,6 +64,7 @@ static var LIST: Array = [
"was": "Das gewünschte Verhalten leichter machen, das störende schwerer. Handy in einen anderen Raum, Instrument sichtbar aufstellen.",
"wann": "Wenn gute Vorsätze regelmäßig an Ablenkung scheitern.",
"warum": "Verhalten folgt dem geringsten Widerstand. Die Umgebung wirkt zuverlässiger als Disziplin.",
"woran": "jemandem, der aufräumt, arbeitet oder sich ausruht — dann ist der Raum um ihn herum greifbar.",
"tags": ["ablenkung", "gewohnheit"],
"momente": ["aufraeumen", "arbeitet", "ruht"],
"farbe": Color(0.55, 0.85, 0.40),
@@ -69,6 +75,7 @@ static var LIST: Array = [
"was": "Einen belastenden Gedanken prüfen und durch eine ebenso wahre, aber tragfähigere Sicht ersetzen.",
"wann": "Wenn jemand in einem Gedanken feststeckt, der ihn klein hält.",
"warum": "Nicht das Ereignis belastet, sondern seine Deutung. Eine andere Deutung verändert Gefühl und Handlung.",
"woran": "jemandem, der grübelt, angespannt ist oder sich ausruht und den Gedanken doch nicht loswird.",
"tags": ["selbstzweifel", "angst"],
"momente": ["gruebelt", "ruht", "angespannt"],
"farbe": Color(0.95, 0.55, 0.75),
@@ -79,6 +86,7 @@ static var LIST: Array = [
"was": "Vier Sekunden einatmen, vier halten, vier ausatmen, vier halten. Mehrere Runden lang.",
"wann": "Bei akuter Anspannung, kurz vor einem gefürchteten Gespräch oder Auftritt.",
"warum": "Langes Ausatmen bremst den Alarmzustand des Körpers. Der Puls sinkt, der Kopf wird wieder klar.",
"woran": "jemandem, der angespannt ist, wartet oder in der Pause nicht runterkommt.",
"tags": ["angst", "stress"],
"momente": ["angespannt", "wartet", "pause"],
"farbe": Color(0.40, 0.85, 0.95),
@@ -89,6 +97,7 @@ static var LIST: Array = [
"was": "Mit sich selbst so sprechen wie mit einem guten Freund in derselben Lage.",
"wann": "Nach einem Fehler oder Rückschlag, wenn die innere Kritik lauter wird als der Vorsatz.",
"warum": "Harte Selbstkritik erhöht die Vermeidung. Freundlichkeit hält handlungsfähig und senkt die Abbruchquote.",
"woran": "jemandem, der nach einem Fehler grübelt, angespannt ist oder sich ausruht, ohne zur Ruhe zu kommen.",
"tags": ["selbstzweifel", "rueckschlag"],
"momente": ["gruebelt", "ruht", "angespannt"],
"farbe": Color(1.0, 0.80, 0.60),
@@ -99,6 +108,7 @@ static var LIST: Array = [
"was": "Feste Aufsteh- und Schlafenszeit, die letzte Stunde ohne Bildschirm, Zimmer dunkel und kühl.",
"wann": "Bei Dauermüdigkeit, Reizbarkeit und wegbrechender Konzentration.",
"warum": "Der innere Rhythmus braucht feste Anker. Erholter Schlaf ist die Grundlage für jede andere Veränderung.",
"woran": "der Abendroutine, an jemandem, der müde ist oder sich mitten am Tag ausruht.",
"tags": ["erschoepfung", "gesundheit"],
"momente": ["abendroutine", "muede", "ruht"],
"farbe": Color(0.45, 0.50, 0.90),
@@ -109,6 +119,7 @@ static var LIST: Array = [
"was": "Eine klare, kleine Bitte an eine konkrete Person richten, statt allgemein zu hoffen.",
"wann": "Wenn jemand allein an etwas hängen bleibt, das andere leicht lösen könnten.",
"warum": "Vage Bitten werden überhört. Menschen helfen gern, wenn sie genau wissen, was zu tun ist.",
"woran": "jemandem, der wartet, Pause macht oder unterwegs ist — da ist Luft für eine Bitte.",
"tags": ["einsamkeit", "ueberforderung"],
"momente": ["wartet", "pause", "unterwegs"],
"farbe": Color(0.90, 0.90, 0.45),
@@ -119,6 +130,7 @@ static var LIST: Array = [
"was": "Absagen und zugleich benennen, was ginge: \"Heute nicht, am Donnerstag gern.\"",
"wann": "Wenn der eigene Kalender von fremden Anliegen aufgebraucht wird.",
"warum": "Ein nacktes Nein fühlt sich riskant an. Mit Alternative bleibt die Beziehung heil und die Zeit geschützt.",
"woran": "jemandem, der arbeitet, angespannt ist oder unterwegs zwischen Terminen hängt.",
"tags": ["stress", "grenzen"],
"momente": ["arbeitet", "angespannt", "unterwegs"],
"farbe": Color(0.85, 0.35, 0.45),
@@ -126,38 +138,6 @@ static var LIST: Array = [
]
static var TAG_TEXT: Dictionary = {
"prokrastination": "Aufschieben",
"fokus": "Konzentration",
"ueberforderung": "Überforderung",
"gewohnheit": "Gewohnheiten",
"ziel": "Ziele",
"ablenkung": "Ablenkung",
"selbstzweifel": "Selbstzweifel",
"angst": "Angst",
"stress": "Stress",
"rueckschlag": "Rückschläge",
"erschoepfung": "Erschöpfung",
"gesundheit": "Gesundheit",
"einsamkeit": "Einsamkeit",
"grenzen": "Grenzen setzen",
}
static func tags_text(tags: Array) -> String:
var out: Array = []
for t in tags:
out.append(TAG_TEXT.get(t, t))
return ", ".join(out)
static func momente_text(momente: Array) -> String:
var out: Array = []
for m in momente:
out.append(Orte.text(m))
return ", ".join(out)
static func by_id(id: String) -> Dictionary:
for t in LIST:
if t["id"] == id:

View File

@@ -48,7 +48,7 @@ func _baue_hud() -> void:
add_child(_hud)
var steuerung := Label.new()
steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · J Journal · Esc Maus frei"
steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · J Journal · 1/2/3 Tempo · Esc Maus frei"
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
steuerung.position = Vector2(-640, 16)
steuerung.size = Vector2(620, 24)
@@ -144,9 +144,14 @@ func _process(_delta: float) -> void:
func _hud_aktualisieren() -> void:
var zeilen: Array = []
zeilen.append("[b]%s[/b]" % Game.time_string())
zeilen.append("Einsicht: [b]%d[/b] Techniken: [b]%d / %d[/b]" %
[Game.einsicht, Game.known.size(), TechDB.LIST.size()])
var takt := ("[color=#aaaaaa]Tempo %d (%d×)[/color]"
% [Game.tempo, int(Game.faktor_von(Game.tempo))])
if Game.schlaeft_ziel():
takt = ("[color=#9fb6ff]Tempo 3 (%d×) · Nachtsprung[/color]"
% int(Game.faktor_von(3)))
zeilen.append("[b]%s[/b] %s" % [Game.time_string(), takt])
zeilen.append("Techniken: [b]%d[/b] gelernt · [b]%d[/b] entdeckt · %d im Spiel" %
[Game.known.size(), Game.offene_techniken(), TechDB.LIST.size()])
if Game.active_mission != "":
var h := HumanDB.by_id(Game.active_mission)
zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %% · %d Regeln" %
@@ -246,8 +251,9 @@ func schliessen() -> void:
func _hinweis_block(t: Dictionary) -> String:
return ("[b]Was:[/b] %s\n[b]Wann:[/b] %s\n[b]Warum:[/b] %s" %
[t["was"], t["wann"], t["warum"]])
return ("[b]Was:[/b] %s\n[b]Wann:[/b] %s\n[b]Warum:[/b] %s\n" +
"[color=#9fb6ff][b]Zu sehen an:[/b] %s[/color]") % \
[t["was"], t["wann"], t["warum"], t["woran"]]
# -------------------------------------------------------- Journal
@@ -294,18 +300,17 @@ func _reiter() -> void:
if Game.active_mission != "":
mission = "%s (%d %%)" % [HumanDB.by_id(Game.active_mission)["name"],
int(Game.mission_progress(Game.active_mission))]
_text("[color=#aaaaaa]Einsicht: [b]%d[/b] · Mission: %s[/color]" %
[Game.einsicht, mission], 15)
_text("[color=#aaaaaa]Mission: %s[/color]" % mission, 15)
_trenner()
func _journal_techniken() -> void:
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)
"Im [b]Hörsaal der Wege[/b] hinter dem Dorfplatz werden sie gelehrt.", 16)
return
_text("Was du gelernt hast. Jede Anwendung draußen macht eine Technik von allein " +
"besser; vertieft wird im Hörsaal.", 15)
_text("Was du gelernt hast. Besser wird eine Technik nur, indem du sie draußen " +
"anwendest — Üben lässt sie sich nicht.", 15)
_trenner()
for id in TechDB.ids():
if not Game.knows(id):
@@ -314,8 +319,6 @@ func _journal_techniken() -> void:
_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()
@@ -335,7 +338,6 @@ func _journal_missionen() -> void:
[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:
@@ -359,7 +361,8 @@ func pipeline_panel(human_id: String) -> void:
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)
"Wechselt sie hinein, greift die Regel von allein — solange du auf der Erde bist.\n" +
"Nach jedem Einsatz braucht eine Technik Ruhe, bevor sie wieder anschlägt.", 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()
@@ -371,9 +374,9 @@ func pipeline_panel(human_id: String) -> void:
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))]
beschriftung = "%s → %s" % [tat, TechDB.by_id(tech)["name"]]
if not Game.bereit(tech):
beschriftung += " (noch gesperrt)"
_knopf(beschriftung, func() -> void: regel_panel(human_id, activity))
_trenner()
_knopf("Zurück zum Journal", func() -> void: journal_panel("missionen"))
@@ -392,38 +395,32 @@ func regel_panel(human_id: String, activity: String) -> void:
_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. " %
_text("Was soll greifen, wenn %s %s? Ob eine Technik hier taugt, steht in ihr " %
[h["name"], Orte.text(activity)] +
"Jede Technik hält nur einen Posten — sie hier einzusetzen nimmt sie anderswo weg.", 15)
"selbst — nachzulesen im Journal. Ein Fehlgriff wirft den Traum zurück.", 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 wo: Array = Game.regel_momente(human_id, id)
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))
if wo.has(activity):
gewaehlt = " — hier eingestellt"
elif not wo.is_empty():
var namen: Array = []
for a in wo:
namen.append("»%s«" % Orte.text(a))
gewaehlt = " — hängt an %s" % ", ".join(namen)
var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(id)]
if ev["blocked"]:
beschriftung += " — " + str(ev["grund"])
elif gewaehlt != "":
beschriftung += gewaehlt
_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))
@@ -441,8 +438,8 @@ func lehre_panel(ort: String = "hoersaal") -> void:
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)
_text("[color=#aaaaaa]Gelernt: [b]%d[/b] von %d entdeckten Wegen · %d im Spiel[/color]" %
[Game.known.size(), Game.offene_techniken(), TechDB.LIST.size()], 15)
_trenner()
if ids.is_empty():
_text("[color=#ffcc66]Hier wird nichts gelehrt.[/color]", 16)
@@ -454,16 +451,13 @@ func lehre_panel(ort: String = "hoersaal") -> void:
else:
_text("[b][color=#9aa0b8]%s[/color][/b] · noch nicht gelernt" % t["name"], 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)
if Game.knows(id):
_knopf("Vertiefen (1 Einsicht)", func() -> void:
if Game.deepen(id):
lehre_panel(ort), Game.einsicht >= 1)
_text("[color=#88ff99]Gelernt. Stufe %d steigt nur, wenn du diesen Weg " %
Game.skill_of(id) + "draußen anwendest — hier ist nichts mehr zu holen.[/color]", 15)
else:
_knopf("Lernen (1 Einsicht)", func() -> void:
_knopf("Diesen Weg lernen", func() -> void:
if Game.learn(id):
lehre_panel(ort), Game.einsicht >= 1)
lehre_panel(ort))
_trenner()
_naechster_weg()
_zeigen("lehre")
@@ -489,19 +483,24 @@ func _naechster_weg() -> void:
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)
_text("Ausgehängt wird nur, wem du mit dem, was du kannst, auch helfen kannst. " +
"Das Tor zur Erde öffnet sich nur mit einer Mission.", 15)
_trenner()
var gezeigt := 0
var verdeckt := 0
for h in HumanDB.LIST:
var id: String = h["id"]
if Game.completed.has(id):
continue
if Game.active_mission != id and not Game.machbar(id):
verdeckt += 1
continue
gezeigt += 1
_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:
if Game.active_mission == id:
_text("[color=#ffdd77]Aktive Mission — %d %%[/color]" %
int(Game.mission_progress(id)), 15)
else:
@@ -512,6 +511,12 @@ func brett_panel() -> void:
Game.accept_mission(id)
brett_panel())
_trenner()
if gezeigt == 0:
_text("[color=#ffcc66]Gerade hängt nichts aus, das zu deinen Wegen passt.[/color]\n" +
"Lerne im Hörsaal oder in einem Haus einen weiteren Weg.", 16)
if verdeckt > 0:
_text("[color=#aaaaaa]%d weitere Anliegen bleiben verdeckt. Keiner deiner Wege " %
verdeckt + "trifft ihr Thema in einem Moment ihres Tages.[/color]", 15)
_zeigen("brett")
_scroll_wunsch = scroll_alt
_scroll_frames = 4
@@ -538,28 +543,22 @@ func anwenden_panel(mensch: Node3D) -> void:
_text("[color=#ffcc66]Du kennst keine Technik.[/color]")
_zeigen("anwenden")
return
_text("[color=#aaaaaa]Was eine Technik tut, steht im Journal.[/color]", 15)
for id in TechDB.ids():
if not Game.knows(id):
continue
var t := TechDB.by_id(id)
var ev := Game.evaluate(id, h["id"], aktiv, mensch.resist_left > 0.0)
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"])
var gesperrt := not Game.bereit(id)
var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(id)]
if gesperrt:
beschriftung += " — noch %d s gesperrt" % int(ceil(Game.cooldown_rest(id)))
elif ev["blocked"]:
beschriftung += " — " + str(ev["grund"])
elif mensch.resist_left > 0.0:
marker.append("[color=#ff8877]macht gerade dicht[/color]")
if marker.is_empty():
marker.append("[color=#aaaaaa]weder Thema noch Moment passen[/color]")
_text("[b]%s[/b] · Stufe %d\n[color=#cccccc]%s[/color]\n%s" %
[t["name"], Game.skill_of(id), t["wann"], " · ".join(marker)], 16)
var beschriftung := "Blockiert" if ev["blocked"] else "Anwenden — %d %% Erfolgschance" % int(round(float(ev["chance"]) * 100.0))
_knopf(beschriftung, func() -> void: _anwenden(id), not ev["blocked"])
_trenner()
beschriftung += " — macht gerade dicht"
_knopf(beschriftung, func() -> void: _anwenden(id), not ev["blocked"] and not gesperrt)
_zeigen("anwenden")
@@ -583,9 +582,10 @@ func _anwenden(id: String) -> void:
Game.notified.emit("Immer dieselbe Technik wirkt schwächer. Probier eine andere.",
Color(0.85, 0.85, 0.6))
else:
mensch.reagiere(false, "%s prallt ab" % t["name"])
Game.notified.emit("%s dringt gerade nicht durch (%d %% Chance)." %
[t["name"], int(round(float(res["chance"]) * 100.0))], Color(1, 0.6, 0.5))
mensch.reagiere(false, "%s prallt ab\n−%d %%" % [t["name"],
int(round(float(res["verlust"])))])
Game.notified.emit("%s dringt nicht durch. (−%d %%)" %
[t["name"], int(round(float(res["verlust"])))], Color(1, 0.6, 0.5))
func _unhandled_input(event: InputEvent) -> void:

View File

@@ -193,7 +193,6 @@ func _ready() -> void:
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)

View File

@@ -25,14 +25,18 @@ func _ready() -> void:
await test_start()
await test_journal()
await test_traumdorf()
await test_dorf_organisch()
await test_haus_betreten()
await test_hoersaal()
await test_mission_annehmen()
await test_reise_zur_erde()
await test_bezirk()
await test_tagesablauf()
await test_wege_machbar()
await test_geist()
await test_technik_anwenden()
await test_cooldown()
await test_chance_verdeckt()
await test_pipeline()
await test_mission_abschliessen()
await test_welt_waechst()
@@ -40,6 +44,9 @@ func _ready() -> void:
await test_journal_ueberall()
await test_steuerung()
await test_erster_traum_schaffbar()
await test_brett_nie_leer()
await test_lesen_lohnt_sich()
await test_tempo()
print("\n=========================================")
print("Fehlende Prüfungen: %d" % fehler)
@@ -79,6 +86,16 @@ func _journal_text() -> String:
return _panel_text()
func _lehre_text() -> String:
return _panel_text()
## Hebt alle Technik-Sperren auf. Nur für Messschleifen, die viele Anwendungen
## in Folge brauchen — der Cooldown selbst wird eigens geprüft.
func _frei() -> void:
Game.cooldown.clear()
## Welche Gebäude stehen wirklich in der Traumwelt? Erkennbar an ihren Schildern.
## Was der Hörsaal von Anfang an lehrt.
func _basiswege() -> Array:
@@ -163,8 +180,13 @@ func test_start() -> void:
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(Game.known.size() == 1, "Ein Weg ist von Anfang an gelernt (%d)" % Game.known.size())
p(Game.knows(str(_basiswege()[0])),
"Und zwar der des Hörsaals: %s" % TechDB.by_id(str(_basiswege()[0]))["name"])
p(Game.skill_of(str(_basiswege()[0])) == 1, "Auf Stufe 1, wie frisch gelernt")
p(not Game.offene_missionen().is_empty(),
"Damit hängt vom ersten Moment an eine Mission aus (%d)" %
Game.offene_missionen().size())
p((welt() as DreamWorld).bewohner.is_empty(),
"Das Traumdorf steht am Anfang leer (%d Wesen)" %
(welt() as DreamWorld).bewohner.size())
@@ -182,17 +204,49 @@ func test_start() -> void:
func test_journal() -> void:
abschnitt("Journal")
for t in TechDB.LIST:
var vollstaendig: bool = t["was"] != "" and t["wann"] != "" and t["warum"] != ""
var vollstaendig: bool = t["was"] != "" and t["wann"] != "" and t["warum"] != "" \
and t["woran"] != ""
if not vollstaendig:
p(false, "Technik %s hat alle drei Hinweise" % t["id"])
p(true, "Alle %d Techniken tragen Was/Wann/Warum" % TechDB.LIST.size())
p(false, "Technik %s hat alle vier Hinweise" % t["id"])
p(true, "Alle %d Techniken tragen Was/Wann/Warum/Woran" % TechDB.LIST.size())
# Der Woran-Hinweis muss jeden Moment der Technik wirklich benennen —
# sonst ist die Technik im Spiel nicht lesbar.
var stamm := {
"schlaeft": "schläf", "morgenroutine": "morgenroutine", "arbeitet": "arbeit",
"pause": "pause", "essen": "essen", "gruebelt": "grübel", "ruht": "ausruh",
"angespannt": "angespannt", "abendroutine": "abendroutine",
"aufraeumen": "aufräum", "wartet": "wartet", "muede": "müde",
"unterwegs": "unterwegs", "zoegert": "zöger",
}
var luecken: Array = []
for t in TechDB.LIST:
var text: String = str(t["woran"]).to_lower()
for m in t["momente"]:
if not stamm.has(m):
luecken.append("%s: %s unbekannt" % [t["id"], m])
elif not text.contains(str(stamm[m])):
luecken.append("%s nennt »%s« nicht" % [t["id"], m])
p(luecken.is_empty(), "Jeder Woran-Hinweis benennt alle Momente seiner Technik%s" %
("" if luecken.is_empty() else " — " + ", ".join(luecken)))
# Aber er verrät nicht mehr als die Momente
var zuviel: Array = []
for t in TechDB.LIST:
var text: String = str(t["woran"]).to_lower()
for m in stamm:
if (t["momente"] as Array).has(m):
continue
if text.contains(str(stamm[m])):
zuviel.append("%s nennt fremdes »%s«" % [t["id"], m])
p(zuviel.is_empty(), "Und keinen Moment, der nicht dazugehört%s" %
("" if zuviel.is_empty() else " — " + ", ".join(zuviel)))
ui().journal_panel("techniken")
p(ui().offen and ui()._kind == "journal", "Journal öffnet sich")
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")
p(_journal_text().contains(str(TechDB.by_id(str(_basiswege()[0]))["name"])),
"Das Journal führt den mitgebrachten Weg")
p(not _knopf_da("Diesen Weg"), "Das Journal lehrt nicht selbst")
ui().journal_panel("missionen")
p(_journal_text().contains("Schwarzen Brett"),
"Ohne Mission verweist das Journal auf das Schwarze Brett")
@@ -202,10 +256,10 @@ func test_journal() -> void:
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(Game.knows(erster), "Er ist schon gelernt: %s" % TechDB.by_id(erster)["name"])
p(not Game.learn(erster), "Zweimal lernen geht nicht")
p(not Game.learn(str(TechDB.LIST[1]["id"])),
"Der zweite Weg ist trotz Einsicht nirgends zu lernen")
"Der zweite Weg ist nirgends zu lernen — er ist noch nicht entdeckt")
p(Game.skill_of(erster) == 1, "Frisch gelernte Technik ist Stufe 1")
Game.add_xp(erster, 4)
p(Game.skill_of(erster) == 2, "4 EP heben auf Stufe 2")
@@ -369,37 +423,37 @@ func test_hoersaal() -> void:
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"):
if c is Button and (c as Button).text.begins_with("Diesen Weg"):
lernbar += 1
p(lernbar <= 1, "Und höchstens einer davon ist zu lernen (%d)" % lernbar)
p(seite.contains(str(TechDB.LIST[1]["name"])),
"Der Hörsaal nennt den nächsten noch unentdeckten Weg")
p(not Game.learn("box_atmung"), "Eine unentdeckte Technik lässt sich nicht lernen")
Game.einsicht = 1
Game.known.clear()
ui().lehre_panel()
var vorher := Game.known.size()
p(_druecke("Lernen"), "Ein Lernen-Knopf steht bereit")
p(_druecke("Diesen Weg"), "Ein Lern-Knopf steht bereit")
p(Game.known.size() == vorher + 1, "Im Hörsaal gelernt (%d -> %d)" % [vorher, Game.known.size()])
p(Game.einsicht == 0, "Lernen kostet 1 Einsicht")
Game.einsicht = 1
# Gelernt heißt fertig: der Lehrort bietet danach nichts mehr an
ui().lehre_panel()
var xp_summe := 0
for id in Game.known:
xp_summe += int(Game.known[id])
p(_druecke("Vertiefen"), "Ein Vertiefen-Knopf steht bereit")
p(not _knopf_da("Vertiefen") and not _knopf_da("Üben") and not _knopf_da("Trainieren"),
"Kein Knopf trainiert eine gelernte Technik")
p(not _druecke("Diesen Weg"), "Auch der Lern-Knopf ist verschwunden")
var xp_neu := 0
for id in Game.known:
xp_neu += int(Game.known[id])
p(xp_neu == xp_summe + 3, "Vertiefen bringt 3 EP (%d -> %d)" % [xp_summe, xp_neu])
p(Game.einsicht == 0, "Vertiefen kostet 1 Einsicht")
p(xp_neu == xp_summe, "Der Besuch im Hörsaal bringt keine Erfahrung (%d)" % xp_neu)
p(_lehre_text().contains("anwendest"),
"Stattdessen verweist der Hörsaal aufs Anwenden")
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:
@@ -419,11 +473,39 @@ func test_mission_annehmen() -> void:
ui().schliessen()
main._interagieren()
p(ui().offen and ui()._kind == "brett", "Das Brett öffnet sich")
var alle_aus := true
# Nur wer mit dem Gelernten zu schaffen ist, hängt aus.
var alle_wege: Dictionary = Game.known.duplicate()
Game.known = {str(_basiswege()[0]): 0}
ui().brett_panel()
var aushang := _panel_text()
var gezeigt := 0
var fehlend := 0
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())
var id: String = h["id"]
if aushang.contains(str(h["name"])):
gezeigt += 1
if not Game.machbar(id):
p(false, "%s hängt aus, obwohl kein Weg greift" % h["name"])
elif Game.machbar(id):
p(false, "%s wäre lösbar, hängt aber nicht aus" % h["name"])
else:
fehlend += 1
p(gezeigt > 0, "Mit einem Weg hängt mindestens ein Aushang (%d von %d)" %
[gezeigt, HumanDB.LIST.size()])
p(fehlend > 0, "Und mindestens einer bleibt verdeckt (%d)" % fehlend)
p(aushang.contains("verdeckt"), "Das Brett sagt, dass etwas verdeckt bleibt")
p(Game.offene_missionen().size() == gezeigt,
"Brett und offene_missionen() sind sich einig (%d)" % gezeigt)
# Ohne jede Technik bleibt das Brett leer, mit Hinweis auf den Hörsaal
Game.known.clear()
ui().brett_panel()
p(Game.offene_missionen().is_empty(), "Ohne Technik ist nichts zu holen")
p(_panel_text().contains("Hörsaal"), "Und das Brett schickt in den Hörsaal")
Game.known = alle_wege
ui().brett_panel()
p(Game.offene_missionen().size() >= gezeigt,
"Mehr Wege heißt mehr Aushänge (%d)" % Game.offene_missionen().size())
p(Game.active_mission == "", "Vorher keine Mission")
var gedrueckt := _druecke("Mission annehmen")
@@ -547,11 +629,12 @@ func test_tagesablauf() -> void:
func test_wege_machbar() -> void:
abschnitt("Wege passen in die Zeitfenster")
_wege_pruefen("Erde", HumanDB.PLACES, HumanDB.LIST)
_wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST)
_wege_pruefen("Erde", HumanDB.PLACES, HumanDB.LIST, HumanDB.NETZ, HumanDB.ANKER)
_wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST, DreamDB.NETZ, DreamDB.ANKER)
func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array) -> void:
func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array,
netz: Netz = null, anker: Dictionary = {}) -> void:
var sek_pro_stunde := Game.DAY_SECONDS / 24.0
var alles_ok := true
var engste := 999.0
@@ -563,7 +646,7 @@ func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array) -> void:
var nach: String = String(r[i][1])
if von == nach:
continue
var dauer := _weg_laenge(places, von, nach) / Bewohner.GEHTEMPO
var dauer := _weg_laenge(places, von, nach, netz, anker) / Bewohner.GEHTEMPO
var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() \
else (24.0 - float(r[i][0]))
var fenster := slot_h * sek_pro_stunde
@@ -578,8 +661,9 @@ func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array) -> void:
p(alles_ok, "%s — jeder Weg passt, engster Fall: %s" % [welt_name, engste_text])
func _weg_laenge(places: Dictionary, von: String, nach: String) -> float:
var pts: Array = Orte.route(places, von, nach, Vector3.ZERO)
func _weg_laenge(places: Dictionary, von: String, nach: String,
netz: Netz = null, anker: Dictionary = {}) -> float:
var pts: Array = Orte.route(places, von, nach, Vector3.ZERO, netz, anker)
var l := 0.0
var pos := Orte.center(places, von)
for pt in pts:
@@ -657,20 +741,26 @@ func test_technik_anwenden() -> void:
p(Game.evaluate("nein_sagen", "mensch_0", "zoegert", false)["blocked"],
"Ungelernte Techniken lassen sich nicht anwenden")
_frei()
Game.progress["mensch_0"] = 50.0
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("mensch_0") > vorher_p, "Erfolg bringt Missionsfortschritt")
else:
p(Game.mission_progress("mensch_0") == vorher_p, "Misserfolg bringt keinen Fortschritt")
p(Game.mission_progress("mensch_0") < vorher_p,
"Fehlversuch kostet Fortschritt (%.0f -> %.0f)" %
[vorher_p, Game.mission_progress("mensch_0")])
p(Game.known["pomodoro"] > 0, "Jede Anwendung bringt Erfahrung")
_frei()
Game.progress["mensch_0"] = 0.0
Game.used.clear()
Game.known["pomodoro"] = 28
var ertraege: Array = []
for i in range(6):
_frei()
var r := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false)
if r["success"]:
ertraege.append(r["gain"])
@@ -713,8 +803,8 @@ func test_pipeline() -> void:
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(_knopf_texte().contains("Pomodoro-Technik"), "Gelernte Techniken stehen zur Wahl")
p(_druecke("Pomodoro-Technik"), "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")
@@ -724,15 +814,20 @@ func test_pipeline() -> void:
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
# Eine Technik darf an mehreren Tätigkeiten hängen
Game.regel_setzen("mensch_0", "arbeitet", "zerlegen")
p(Game.regel_anzahl("mensch_0") == 3,
"Dieselbe Technik anderswo einzusetzen fügt keine Regel hinzu (%d)" %
p(Game.regel_anzahl("mensch_0") == 4,
"Dieselbe Technik anderswo einzusetzen kommt 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")
p(Game.regel_fuer("mensch_0", "gruebelt") == "zerlegen"
and Game.regel_fuer("mensch_0", "arbeitet") == "zerlegen",
"Sie hängt jetzt an beiden Tätigkeiten")
p(Game.regel_momente("mensch_0", "zerlegen").size() == 2,
"Und das Spiel weiß von beiden Posten (%d)" %
Game.regel_momente("mensch_0", "zerlegen").size())
Game.regel_setzen("mensch_0", "arbeitet", "")
Game.regel_setzen("mensch_0", "gruebelt", "zerlegen")
p(Game.regel_momente("mensch_0", "zerlegen").size() == 1,
"Einzeln löschen lässt den anderen Posten stehen")
# Ohne Zutun einen Tag laufen lassen
var lena: Bewohner = null
@@ -787,18 +882,19 @@ func test_mission_abschliessen() -> void:
abschnitt("Mission abschließen")
Game.progress["mensch_0"] = 0.0
Game.used.clear()
var einsicht_vorher := Game.einsicht
var runden := 0
while Game.mission_progress("mensch_0") < Game.MISSION_ZIEL and runden < 400:
_frei()
Game.apply_technique("pomodoro", "mensch_0", "zoegert", false)
_frei()
Game.apply_technique("zerlegen", "mensch_0", "gruebelt", false)
_frei()
Game.apply_technique("umgebung", "mensch_0", "aufraeumen", false)
runden += 1
p(Game.mission_progress("mensch_0") >= Game.MISSION_ZIEL,
"Lenas Traum erreicht 100 %% (nach %d Runden)" % runden)
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)
@@ -836,13 +932,12 @@ func test_welt_waechst() -> void:
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(_druecke("Diesen Weg"), "Er lässt sich lernen")
p(Game.knows(neu_id), "Gelernt: %s" % TechDB.by_id(neu_id)["name"])
ui().schliessen()
p(not Game.technik_offen(str(TechDB.LIST[2]["id"])),
@@ -853,7 +948,6 @@ func test_welt_waechst() -> void:
# 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
@@ -910,35 +1004,49 @@ func _dorf_platz_pruefen() -> void:
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)
_wege_pruefen("Traumdorf mit %d Häusern" % stufen, DreamDB.PLACES, DreamDB.LIST,
DreamDB.NETZ, DreamDB.ANKER)
_wege_pruefen("Stadt mit %d Menschen" % HumanDB.LIST.size(), HumanDB.PLACES,
HumanDB.LIST, HumanDB.NETZ, HumanDB.ANKER)
Game.completed.assign(merker)
Game.welt_erzeugen()
func test_techniken_verbessern_sich() -> void:
abschnitt("Techniken verbessern sich durch Anwendung")
Game.einsicht = 2
abschnitt("Techniken verbessern sich nur durch Anwendung")
p(Game.learn("zwei_minuten"), "Technik gelernt")
p(Game.skill_of("zwei_minuten") == 1, "Start auf Stufe 1")
var einsicht_vorher := Game.einsicht
p(Game.skill_of("zwei_minuten") == 1, "Frisch gelernt heißt Stufe 1")
var anwendungen := 0
while Game.skill_of("zwei_minuten") < 3 and anwendungen < 60:
_frei()
Game.apply_technique("zwei_minuten", "mensch_2", "gruebelt", false)
anwendungen += 1
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["zwei_minuten"]
_frei()
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["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")
# Es gibt keinen zweiten Weg nach oben
p(not Game.has_method("deepen"), "Kein gezieltes Vertiefen mehr im Spiel")
p(not ("einsicht" in Game), "Und keine Währung, die man dafür ausgeben könnte")
var stand: int = Game.known["zwei_minuten"]
Game.learn("zwei_minuten")
ui().lehre_panel(DreamDB.ort_von_technik("zwei_minuten"))
var geknoepft := _druecke("Diesen Weg") or _druecke("Vertiefen") or _druecke("Üben")
ui().schliessen()
p(not geknoepft and Game.known["zwei_minuten"] == stand,
"Ein zweiter Besuch am Lehrort ändert nichts (%d EP)" % stand)
# Zeitablauf allein bringt auch nichts
var vorher: int = Game.known["zwei_minuten"]
Game.advance_time(Game.DAY_SECONDS)
p(Game.known["zwei_minuten"] == vorher, "Und auch ein ganzer Tag bringt keine Erfahrung")
func test_journal_ueberall() -> void:
@@ -974,13 +1082,13 @@ func test_journal_ueberall() -> void:
func test_steuerung() -> void:
abschnitt("Steuerung")
var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "sinken",
"interagieren", "journal"]
"interagieren", "journal", "tempo_1", "tempo_2", "tempo_3"]
var alle := true
for a in aktionen:
if not InputMap.has_action(a) or InputMap.action_get_events(a).is_empty():
alle = false
p(false, "Aktion '%s' ist belegt" % a)
p(alle, "Alle acht Steuerungsaktionen sind mit Tasten belegt")
p(alle, "Alle %d Steuerungsaktionen sind mit Tasten belegt" % aktionen.size())
ui().schliessen()
main.player.look_enabled = true
@@ -1030,7 +1138,7 @@ func test_steuerung() -> void:
## und zwar zuverlässig, nicht nur bei gutem Würfelglück.
func _erster_traum_tage(saat: int) -> int:
seed(saat)
Game.einsicht = 3
Game.cooldown.clear()
Game.known.clear()
Game.used.clear()
Game.pipeline.clear()
@@ -1088,6 +1196,548 @@ func test_erster_traum_schaffbar() -> void:
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())
## Nach jedem Einsatz braucht eine Technik Ruhe. Ein Weg allein deckt daher
## keinen ganzen Tag ab — das ist der Grund, mehrere zu lernen.
func test_cooldown() -> void:
abschnitt("Techniken haben einen Cooldown")
_frei()
Game.known = {"pomodoro": 28, "zerlegen": 28}
Game.active_mission = "mensch_0"
Game.progress["mensch_0"] = 50.0
p(Game.bereit("pomodoro"), "Frisch ist eine Technik einsatzbereit")
var erste := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false)
p(erste["ok"], "Der erste Einsatz geht durch")
p(not Game.bereit("pomodoro"), "Danach ist sie gesperrt")
p(is_equal_approx(Game.cooldown_rest("pomodoro"), Game.TECHNIK_COOLDOWN),
"Und zwar %.0f Sekunden lang" % Game.TECHNIK_COOLDOWN)
var zweite := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false)
p(not zweite["ok"], "Ein zweiter Einsatz prallt an der Sperre ab")
p(str(zweite["grund"]).contains("einsatzbereit"), "Mit klarer Begründung")
p(Game.bereit("zerlegen"), "Die Sperre gilt nur für diese eine Technik")
var andere := Game.apply_technique("zerlegen", "mensch_0", "gruebelt", false)
p(andere["ok"], "Eine andere Technik greift weiter")
# Eine Regel bleibt still, solange die Technik gesperrt ist
Game.pipeline.clear()
Game.regel_setzen("mensch_0", "zoegert", "pomodoro")
var still := Game.regel_ausloesen("mensch_0", "zoegert", false)
p(still.is_empty(), "Auch eine Regel läuft nicht gegen die Sperre an")
Game.advance_time(Game.TECHNIK_COOLDOWN * 0.5)
p(not Game.bereit("pomodoro"), "Nach der halben Zeit noch immer gesperrt")
Game.advance_time(Game.TECHNIK_COOLDOWN * 0.6)
p(Game.bereit("pomodoro"), "Nach Ablauf wieder frei")
p(Game.cooldown_rest("pomodoro") == 0.0, "Und der Zähler steht auf null")
var wieder := Game.regel_ausloesen("mensch_0", "zoegert", false)
p(not wieder.is_empty(), "Jetzt greift die Regel wieder")
# Im Anwenden-Panel ist die Sperre sichtbar und der Knopf tot
var lena: Bewohner = null
for m in welt().humans:
if m.data["id"] == "mensch_0":
lena = m
ui().anwenden_panel(lena)
var gesperrt_sichtbar := _knopf_texte().contains("gesperrt")
var knopf_tot := true
for c in ui()._content.get_children():
if c is Button and (c as Button).text.contains("gesperrt") \
and not (c as Button).disabled:
knopf_tot = false
p(gesperrt_sichtbar, "Der Anwenden-Knopf weist die Sperre aus")
p(knopf_tot, "Und lässt sich nicht drücken")
ui().schliessen()
Game.pipeline.clear()
_frei()
## Wie gut eine Technik im Moment steht, sagt das Spiel nicht. Es nennt nur,
## was zum Thema und was zum Moment passt — entscheiden muss der Spieler.
func test_chance_verdeckt() -> void:
abschnitt("Die Erfolgschance bleibt verdeckt")
_frei()
Game.known = {"pomodoro": 28, "zerlegen": 28}
Game.active_mission = "mensch_0"
Game.pipeline.clear()
Game.regel_setzen("mensch_0", "zoegert", "pomodoro")
ui().regel_panel("mensch_0", "zoegert")
var knoepfe := _knopf_texte()
p(not knoepfe.contains("%"), "Kein Knopf der Technikwahl nennt eine Prozentzahl")
p(not _panel_text().contains("Erfolgschance"),
"Und das Wort Erfolgschance kommt nicht vor")
var wahl := _panel_text()
p(not wahl.contains("passt zum Thema") and not wahl.contains("passender Moment")
and not wahl.contains("weder Thema"),
"Auch kein Hinweis, ob Thema oder Moment passen")
p(not wahl.contains("Wann:") and not wahl.contains("Zu sehen an:"),
"Die Auswahl zeigt nur die Namen, keine Beschreibung")
p(_knopf_texte().contains("Pomodoro-Technik · Stufe"),
"Jede Wahl steht als Name mit Stufe da")
ui().schliessen()
ui().pipeline_panel("mensch_0")
var zeilen := _knopf_texte()
p(not zeilen.contains("%"), "Auch die Regelzeilen nennen keine Chance")
p(zeilen.contains("Pomodoro-Technik"), "Sie nennen nur die eingehängte Technik")
ui().schliessen()
var lena: Bewohner = null
for m in welt().humans:
if m.data["id"] == "mensch_0":
lena = m
ui().anwenden_panel(lena)
var anwenden := _panel_text()
p(not _knopf_texte().contains("%"), "Und der Anwenden-Knopf nennt keine Chance")
p(_knopf_texte().contains("Pomodoro-Technik · Stufe"),
"Er trägt nur Name und Stufe")
p(not anwenden.contains("passt zum Thema") and not anwenden.contains("passender Moment"),
"Auch beim Anwenden steht nicht da, ob es passt")
p(not anwenden.contains("Hilft bei") and not anwenden.contains("Momente:"),
"Und keine Liste der passenden Themen oder Momente")
p(not anwenden.contains("Wann:") and not anwenden.contains("Zu sehen an:"),
"Auch dort nur Namen — die Beschreibung steht im Journal")
p(anwenden.contains("Journal"), "Und das Panel sagt, wo sie steht")
ui().schliessen()
ui().journal_panel("techniken")
var buch := _panel_text()
p(not buch.contains("Hilft bei") and not buch.contains("Momente:"),
"Auch das Journal verrät die Passung nicht")
p(buch.contains("Wann:") and buch.contains("Zu sehen an:"),
"Das Journal dagegen führt Was, Wann, Warum und Zu sehen an")
ui().schliessen()
ui().brett_panel()
p(not _panel_text().contains("Themen:"),
"Und das Brett listet die Themen des Menschen nicht auf")
ui().schliessen()
Game.pipeline.clear()
## Wenn nur lösbare Missionen aushängen, darf das Brett nie ganz leer werden —
## sonst hinge Loop 2 fest: ohne Traum kein neuer Weg, ohne Weg kein Aushang.
func test_brett_nie_leer() -> void:
abschnitt("Das Brett läuft nie leer")
var merk_c: Array = Game.completed.duplicate()
var merk_k: Dictionary = Game.known.duplicate()
var leer_bei := -1
var kleinste := 99
var stufen := 14
for stufe in range(stufen):
# Ein Spieler, der alles lernt, was das Dorf hergibt
for t in TechDB.LIST:
if Game.technik_offen(str(t["id"])) and not Game.knows(str(t["id"])):
Game.known[str(t["id"])] = 0
var offen: Array = Game.offene_missionen()
kleinste = mini(kleinste, offen.size())
if offen.is_empty():
leer_bei = stufe
break
Game.completed.append(str(offen[0]))
Game.welt_erzeugen()
p(leer_bei < 0, "Über %d Ausbaustufen hängt immer etwas aus%s" %
[stufen, "" if leer_bei < 0 else " — leer bei Stufe %d" % leer_bei])
p(kleinste >= 1, "Im knappsten Fall bleibt %d Aushang übrig" % kleinste)
# Und der jeweils neueste Mensch ist mit dem neuesten Weg zu schaffen
Game.completed.clear()
Game.known.clear()
Game.welt_erzeugen()
var passt := 0
for i in range(TechDB.LIST.size()):
Game.completed.clear()
for k in range(i):
Game.completed.append("stufe_%d" % k)
Game.welt_erzeugen()
Game.known = {str(TechDB.LIST[i]["id"]): 0}
if Game.machbar("mensch_%d" % i):
passt += 1
p(passt == TechDB.LIST.size(),
"Zu jedem der %d Wege gibt es den passenden Menschen (%d)" %
[TechDB.LIST.size(), passt])
Game.completed.assign(merk_c)
Game.known = merk_k
Game.welt_erzeugen()
## Die Probe auf den Kern: wer die Technik liest und sie nur dort einhängt, wo
## ihr "Wann" trifft, kommt schneller ans Ziel als wer sie überall hinhängt.
## Ohne diesen Abstand wäre das Lesen umsonst.
func _traum_tage(saat: int, alles: bool) -> int:
seed(saat)
Game.cooldown.clear()
Game.known.clear()
Game.used.clear()
Game.pipeline.clear()
Game.progress.clear()
Game.completed.clear()
Game.active_mission = ""
Game.welt_erzeugen()
var weg := str(_basiswege()[0])
Game.learn(weg)
Game.accept_mission("mensch_0")
var t := TechDB.by_id(weg)
for a in HumanDB.aktivitaeten("mensch_0"):
if not Game.erreichbar(a):
continue
if alles or (t["momente"] as Array).has(a):
Game.regel_setzen("mensch_0", a, weg)
main._erde()
await get_tree().process_frame
main.player.global_position = Vector3(0, 30.0, 60.0)
Game.time_h = 7.0
Game.day = 1
for i in range(200000):
schritte(1)
if Game.completed.has("mensch_0") or Game.day > 14:
break
return Game.day if Game.completed.has("mensch_0") else 99
func test_lesen_lohnt_sich() -> void:
abschnitt("Die Technik zu lesen zahlt sich aus")
var gezielt: Array = []
var blind: Array = []
for saat in [7, 555, 90210]:
gezielt.append(await _traum_tage(saat, false))
blind.append(await _traum_tage(saat, true))
var g := 0.0
var b := 0.0
for i in range(gezielt.size()):
g += float(gezielt[i])
b += float(blind[i])
g /= float(gezielt.size())
b /= float(blind.size())
p(b >= g * 1.25,
"Gezielt gesetzt geht es deutlich schneller: %.1f statt %.1f Tage (gezielt %s, blind %s)" %
[g, b, str(gezielt), str(blind)])
var schlechter := 0
for i in range(gezielt.size()):
if int(blind[i]) < int(gezielt[i]):
schlechter += 1
p(schlechter == 0,
"Und blind gesetzt ist in keinem der %d Läufe besser (%d)" % [gezielt.size(), schlechter])
# Der Grund: ein falscher Moment ist unterm Strich ein Verlustgeschäft
Game.known = {"pomodoro": 0}
var gut := Game.evaluate("pomodoro", "mensch_0", "zoegert", false)
var falsch := Game.evaluate("pomodoro", "mensch_0", "gruebelt", false)
p(gut["moment"] and not falsch["moment"], "»zögert« ist Pomodoros Moment, »grübelt« nicht")
var ertrag_gut := (8.0 + 2.0) + 5.0 + 5.0
var ertrag_falsch := (8.0 + 2.0) + 5.0
var ev_gut: float = float(gut["chance"]) * ertrag_gut \
- (1.0 - float(gut["chance"])) * Game.FEHLVERSUCH_VERLUST
var ev_falsch: float = float(falsch["chance"]) * ertrag_falsch \
- (1.0 - float(falsch["chance"])) * Game.FEHLVERSUCH_VERLUST
p(ev_gut > 0.0, "Im richtigen Moment trägt sie (%.1f je Einsatz)" % ev_gut)
p(ev_falsch < 0.0, "Im falschen kostet sie (%.1f je Einsatz)" % ev_falsch)
## Warten soll nicht wehtun: 1, 2, 3 stellen das Tempo. Über der Nacht des
## Menschen springt es von allein auf drei — dort ist ohnehin nichts zu holen.
func test_tempo() -> void:
abschnitt("Tempo lässt sich stellen")
Game.completed.clear()
Game.known = {"pomodoro": 0}
Game.welt_erzeugen()
Game.active_mission = ""
Game.tempo = 1
p(is_equal_approx(Game.zeit_faktor(), 1.0), "Ohne Zutun läuft die Welt einfach")
# Wie in Die Sims: die Stufe ist nicht der Faktor
var erwartet := {1: 1.0, 2: 3.0, 3: 7.0}
for stufe in [2, 3, 1]:
Game.tempo_setzen(stufe)
p(Game.tempo == stufe and is_equal_approx(Game.zeit_faktor(), float(erwartet[stufe])),
"Stufe %d läuft %.0f-fach" % [stufe, Game.zeit_faktor()])
Game.tempo_setzen(3)
p(not is_equal_approx(Game.zeit_faktor(), 3.0),
"Stufe 3 heißt gerade nicht dreifach (%.0f×)" % Game.zeit_faktor())
Game.tempo_setzen(1)
Game.tempo_setzen(9)
p(Game.tempo == 3, "Höher als drei geht nicht (%d)" % Game.tempo)
Game.tempo_setzen(0)
p(Game.tempo == 1, "Und tiefer als eins auch nicht (%d)" % Game.tempo)
# Über der Nacht des Menschen springt es von allein
Game.accept_mission("mensch_0")
var wach := -1.0
var nacht := -1.0
for stunde in range(24):
Game.time_h = float(stunde) + 0.5
if Game.schlaeft_ziel():
nacht = Game.zeit_faktor()
else:
wach = Game.zeit_faktor()
p(nacht > 0.0 and wach > 0.0, "Lenas Tag hat wache und schlafende Stunden")
p(is_equal_approx(nacht, Game.faktor_von(3)),
"Im Schlaf läuft die Welt auf der höchsten Stufe (%.0f×)" % nacht)
p(is_equal_approx(wach, 1.0), "Wach wieder auf der gewählten Stufe (%.0f)" % wach)
Game.tempo_setzen(1)
Game.time_h = 2.0
p(Game.schlaeft_ziel(), "Um 02:00 schläft sie")
p(is_equal_approx(Game.zeit_faktor(), Game.faktor_von(3)),
"Die eigene Wahl Stufe 1 gilt dann nicht — die Nacht springt")
# Ohne Mission gibt es nichts zu überspringen
Game.active_mission = ""
p(not Game.schlaeft_ziel(), "Ohne Mission kein Nachtsprung")
p(is_equal_approx(Game.zeit_faktor(), 1.0), "Dann zählt nur die Wahl")
# Der Faktor beschleunigt die Uhr wirklich
Game.tempo_setzen(3)
Game.time_h = 8.0
Game.day = 1
var vorher := Game.time_h
Game.advance_time(1.0 * Game.zeit_faktor())
var schnell := Game.time_h - vorher
Game.tempo_setzen(1)
Game.time_h = 8.0
vorher = Game.time_h
Game.advance_time(1.0 * Game.zeit_faktor())
var langsam := Game.time_h - vorher
p(is_equal_approx(schnell, langsam * Game.faktor_von(3)),
"Auf Stufe 3 vergeht %.0f-mal so viel Zeit je Sekunde (%.4f statt %.4f h)" %
[Game.faktor_von(3), schnell, langsam])
# Das HUD sagt, woran man ist
Game.tempo_setzen(2)
ui().journal_panel()
ui().schliessen()
ui()._hud_aktualisieren()
p(ui()._hud.text.contains("Tempo 2 (3×)"),
"Das HUD nennt Stufe und Faktor getrennt")
Game.accept_mission("mensch_0")
Game.time_h = 2.0
ui()._hud_aktualisieren()
p(ui()._hud.text.contains("Nachtsprung"), "Und weist den Nachtsprung aus")
Game.tempo_setzen(1)
Game.active_mission = ""
## Das Dorf wird nicht gesetzt, sondern wächst. Also muss über viele
## Ausbaustufen gelten: nichts überlappt, kein Weg führt durch ein Haus, alles
## hängt zusammen, und was einmal steht, bleibt stehen.
func test_dorf_organisch() -> void:
abschnitt("Das Dorf wächst organisch")
var merker: Array = Game.completed.duplicate()
var stufen := 24
var ueberlappung := ""
var wegtreffer := ""
var lose := ""
var ohne_anker := ""
var unerreichbar := ""
var sektoren_12 := 0
var dichte := ""
for stufe in range(stufen + 1):
Game.completed.clear()
for i in range(stufe):
Game.completed.append("stufe_%d" % i)
Game.welt_erzeugen()
var netz: Netz = DreamDB.NETZ
var keys: Array = []
for key in DreamDB.PLACES:
if float(DreamDB.PLACES[key]["size"][0]) > 0.0:
keys.append(key)
# nichts steht ineinander
for a in range(keys.size()):
for b in range(a + 1, keys.size()):
var ca := Orte.center(DreamDB.PLACES, keys[a])
var cb := Orte.center(DreamDB.PLACES, keys[b])
var sa: Array = DreamDB.PLACES[keys[a]]["size"]
var sb: Array = DreamDB.PLACES[keys[b]]["size"]
if absf(ca.x - cb.x) < (float(sa[0]) + float(sb[0])) * 0.5 \
and absf(ca.z - cb.z) < (float(sa[1]) + float(sb[1])) * 0.5 \
and ueberlappung == "":
ueberlappung = ("%s und %s bei %d Häusern"
% [keys[a], keys[b], stufe])
# kein Weg führt durch ein Gebäude
for k in netz.kanten:
for key in keys:
var s2: Array = DreamDB.PLACES[key]["size"]
if Netz.trifft_rechteck(netz.knoten[k[0]], netz.knoten[k[1]],
Orte.center(DreamDB.PLACES, key),
Vector2(float(s2[0]), float(s2[1])), 0.0) and wegtreffer == "":
wegtreffer = "%s bei %d Häusern" % [key, stufe]
if not netz.zusammenhaengend() and lose == "":
lose = "bei %d Häusern" % stufe
# jedes Haus hängt am Netz und ist vom Dorfplatz aus zu erreichen
for i in range(stufe):
var key3 := DreamDB.haus_key(i)
if not DreamDB.ANKER.has(key3):
if ohne_anker == "":
ohne_anker = "%s bei %d Häusern" % [key3, stufe]
continue
if netz.weglaenge(int(DreamDB.ANKER["lichtung"]), int(DreamDB.ANKER[key3])) == INF \
and unerreichbar == "":
unerreichbar = "%s bei %d Häusern" % [key3, stufe]
if stufe == 12:
var sekt: Dictionary = {}
for i in range(stufe):
var c := Orte.center(DreamDB.PLACES, DreamDB.haus_key(i))
sekt[int(floor((atan2(c.z, c.x) + PI) / (TAU / 8.0))) % 8] = true
sektoren_12 = sekt.size()
if stufe == stufens_probe():
var flaeche := PI * pow(Game.insel_radius(), 2.0)
if flaeche / float(stufe) > 2200.0:
dichte = "%.0f m² je Haus" % (flaeche / float(stufe))
p(ueberlappung == "", "Über %d Ausbaustufen steht kein Gebäude im anderen%s" %
[stufen, "" if ueberlappung == "" else " — " + ueberlappung])
p(wegtreffer == "", "Und kein Weg führt durch ein Gebäude%s" %
["" if wegtreffer == "" else " — " + wegtreffer])
p(lose == "", "Das Wegenetz hängt immer zusammen%s" %
["" if lose == "" else " — " + lose])
p(ohne_anker == "", "Jedes Haus hängt am Netz%s" %
["" if ohne_anker == "" else " — " + ohne_anker])
p(unerreichbar == "", "Und ist vom Dorfplatz aus erreichbar%s" %
["" if unerreichbar == "" else " — " + unerreichbar])
p(sektoren_12 >= 6,
"Bei 12 Häusern ist das Dorf rundum gewachsen (%d von 8 Richtungen)" % sektoren_12)
p(dichte == "", "Die Insel bleibt dicht besiedelt%s" %
["" if dichte == "" else " — " + dichte])
# Verwinkelt heißt: Gabeln und Schleifen, nicht Speichen zur Mitte
Game.completed.clear()
for i in range(18):
Game.completed.append("stufe_%d" % i)
Game.welt_erzeugen()
var netz2: Netz = DreamDB.NETZ
var gabeln := 0
for i in range(netz2.knoten.size()):
if netz2.nachbarn(i).size() >= 3:
gabeln += 1
var schleifen: int = netz2.kanten.size() - netz2.knoten.size() + 1
p(gabeln >= 6, "Bei 18 Häusern gabelt sich das Netz %d-mal" % gabeln)
p(schleifen >= 2, "Und trägt %d Schleifen statt lauter Sackgassen" % schleifen)
# Was einmal steht, bleibt stehen
var vorher: Array = []
for i in range(18):
vorher.append(Orte.center(DreamDB.PLACES, DreamDB.haus_key(i)))
Game.completed.append("stufe_18")
Game.welt_erzeugen()
var verrutscht := 0
for i in range(18):
if vorher[i].distance_to(Orte.center(DreamDB.PLACES, DreamDB.haus_key(i))) > 0.01:
verrutscht += 1
p(verrutscht == 0, "Das 19. Haus verschiebt keines der 18 alten (%d)" % verrutscht)
Game.completed.assign(merker)
Game.welt_erzeugen()
func stufens_probe() -> int:
return 24
## Die Erde ist ein erzeugter Bezirk. Also muss gelten: Parzellen liegen
## nebeneinander statt ineinander, jede hat Straßenanschluss, das Netz hängt
## zusammen -- und anders als im Dorf wächst hier nichts.
func test_bezirk() -> void:
abschnitt("Die Erde ist ein Bezirk")
var netz: Netz = HumanDB.NETZ
p(HumanDB.PARZELLEN.size() >= 60,
"Der Bezirk ist in %d Parzellen geteilt" % HumanDB.PARZELLEN.size())
var ueber := 0
for i in range(HumanDB.PARZELLEN.size()):
for j in range(i + 1, HumanDB.PARZELLEN.size()):
var a: Dictionary = HumanDB.PARZELLEN[i]
var b: Dictionary = HumanDB.PARZELLEN[j]
if absf(a["pos"].x - b["pos"].x) < (a["size"].x + b["size"].x) * 0.5 - 0.05 \
and absf(a["pos"].z - b["pos"].z) < (a["size"].y + b["size"].y) * 0.5 - 0.05:
ueber += 1
p(ueber == 0, "Keine Parzelle steht in einer anderen (%d)" % ueber)
var ohne := 0
var weiteste := 0.0
for pz in HumanDB.PARZELLEN:
var d := INF
for st in HumanDB.STRASSEN:
var f := Geometry3D.get_closest_point_to_segment(pz["pos"], st["a"], st["b"])
d = minf(d, f.distance_to(pz["pos"]) - maxf(pz["size"].x, pz["size"].y) * 0.5
- float(st["breite"]) * 0.5)
weiteste = maxf(weiteste, d)
if d > 2.0:
ohne += 1
p(ohne == 0, "Jede Parzelle liegt an einer Straße (weiteste Lücke %.1f m)" % weiteste)
p(netz.zusammenhaengend(),
"Das Straßennetz hängt zusammen (%d Knoten, %d Kanten)" %
[netz.knoten.size(), netz.kanten.size()])
var unerreichbar := 0
var keys: Array = HumanDB.PLACES.keys()
for i in range(keys.size()):
for j in range(i + 1, keys.size()):
if netz.weglaenge(int(HumanDB.ANKER[keys[i]]), int(HumanDB.ANKER[keys[j]])) == INF:
unerreichbar += 1
p(unerreichbar == 0, "Jeder Ort ist von jedem anderen aus zu erreichen (%d Lücken)" %
unerreichbar)
# Arterien im Raster, roji schmal und als Sackgasse ins Blockinnere
var arterien := 0
var gassen := 0
for st in HumanDB.STRASSEN:
if float(st["breite"]) >= HumanDB.ARTERIE:
arterien += 1
else:
gassen += 1
p(arterien == HumanDB.ACHSEN.size() * 2,
"%d Arterien spannen das Raster auf" % arterien)
p(gassen >= 4, "Und %d roji führen ins Blockinnere" % gassen)
p(HumanDB.ROJI < HumanDB.ARTERIE * 0.6,
"Eine roji ist mit %.1f m deutlich schmaler als eine Arterie (%.1f m)" %
[HumanDB.ROJI, HumanDB.ARTERIE])
var sackgassen := 0
for st in HumanDB.STRASSEN:
if float(st["breite"]) < HumanDB.ARTERIE:
var ende: Vector3 = st["b"]
if netz.nachbarn(netz.fuege_knoten(ende)).size() <= 1:
sackgassen += 1
p(sackgassen >= gassen / 2, "Die meisten roji enden blind (%d von %d)" %
[sackgassen, gassen])
# Alle Menschen sind da, und sie werden nicht mehr
var vorher := HumanDB.LIST.size()
p(vorher == HumanDB.EINWOHNER, "Alle %d Menschen sind von Anfang an da" % vorher)
var merker: Array = Game.completed.duplicate()
for i in range(6):
Game.completed.append("stufe_%d" % i)
Game.welt_erzeugen()
p(HumanDB.LIST.size() == vorher,
"Sechs erfüllte Träume ändern daran nichts (%d)" % HumanDB.LIST.size())
Game.completed.assign(merker)
Game.welt_erzeugen()
# Jeder wohnt in einer eigenen Wohnung
var heime: Dictionary = {}
for m in HumanDB.LIST:
heime[str(m["heim"])] = true
p(heime.size() == HumanDB.LIST.size(),
"Jeder Mensch hat seine eigene Wohnung (%d)" % heime.size())
# Der mittlere Block bleibt frei
var im_park := 0
var halb: float = (float(HumanDB.ACHSEN[2]) - float(HumanDB.ACHSEN[1])) * 0.5 \
- HumanDB.ARTERIE
for pz in HumanDB.PARZELLEN:
var q: Vector3 = pz["pos"]
if absf(q.x) < halb and absf(q.z) < halb:
im_park += 1
p(im_park == 0, "Im mittleren Block steht kein Haus — er ist der Park (%d)" % im_park)