update
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
114
README.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
187
scripts/game.gd
187
scripts/game.gd
@@ -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"],
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
"color": Color.from_hsv(fmod(0.08 + float(i) * 0.113, 1.0), 0.28, 0.78),
|
||||
# ------------------------------------------------------------ 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),
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
## 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 _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()])
|
||||
|
||||
|
||||
@@ -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
141
scripts/netz.gd
Normal 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
1
scripts/netz.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dsaev7wupa8ex
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
144
scripts/ui.gd
144
scripts/ui.gd
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
770
tests/verify.gd
770
tests/verify.gd
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user