update
This commit is contained in:
91
README.md
91
README.md
@@ -10,17 +10,21 @@ sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt.
|
|||||||
|
|
||||||
godot --path .
|
godot --path .
|
||||||
|
|
||||||
Steuerung: **WASD** bewegen · **Maus** umsehen · **E** interagieren ·
|
Steuerung: **WASD** bewegen · **Q** Kamera drehen · **Mausrad** Zoom ·
|
||||||
**F** folgen · **R** notieren · **J** Journal · **1/2/3** Tempo ·
|
**Klick** auf einen Menschen folgen · **E** interagieren · **F** folgen ·
|
||||||
**Esc** Maus freigeben.
|
**R** notieren · **J** Journal · **1/2/3** Tempo.
|
||||||
|
|
||||||
|
Die **Kamera** ist fest wie in Diablo: schräg von oben, diagonal über den
|
||||||
|
Bezirk, zwei Zoomstufen, Drehen in 90°-Schritten. Die Maus ist immer frei.
|
||||||
|
Menschen sind klein, Häuser groß — die Stadt soll groß wirken und der Geist
|
||||||
|
entrückt.
|
||||||
|
|
||||||
**Die Oberfläche** ist um den Tag gebaut. Unten über die ganze Breite liegt
|
**Die Oberfläche** ist um den Tag gebaut. Unten über die ganze Breite liegt
|
||||||
die **Zeitleiste**: 24 Felder, eines je Stunde, die laufende steht heraus.
|
die **Zeitleiste**: 24 Felder, eines je Stunde, die laufende steht heraus.
|
||||||
Grau heißt unbekannt, gelb „hier stimmt etwas nicht, noch nicht benannt", rot
|
Grau heißt unbekannt, gelb „hier stimmt etwas nicht, noch nicht benannt", rot
|
||||||
ein benannter Blocker, grün ein gebautes System. Links darüber die Ursachen,
|
ein benannter Blocker, grün ein gebautes System. Links darüber die Ursachen,
|
||||||
rechts die Momente; bricht einer auf, pulsiert die laufende Stunde und rechts
|
rechts die Momente; bricht einer auf, pulsiert die laufende Stunde und rechts
|
||||||
läuft die Uhr. Ein Klick auf eine Stunde (Maus mit **Esc** freigeben) öffnet
|
läuft die Uhr. Ein Klick auf eine Stunde öffnet ihre Karte direkt darüber — was passiert, Auslöser, Belohnung, welche Technik
|
||||||
ihre Karte direkt darüber — was passiert, Auslöser, Belohnung, welche Technik
|
|
||||||
passt.
|
passt.
|
||||||
|
|
||||||
Das **Journal** (J) ist eine Seitenleiste rechts, die Welt bleibt sichtbar:
|
Das **Journal** (J) ist eine Seitenleiste rechts, die Welt bleibt sichtbar:
|
||||||
@@ -30,8 +34,8 @@ hervorgehobene. Fenster in der Mitte gibt es nur an Orten: Brett, Hörsaal,
|
|||||||
Aushang schreiben. **Esc** geht eine Ebene zurück, nicht gleich ganz zu.
|
Aushang schreiben. **Esc** geht eine Ebene zurück, nicht gleich ganz zu.
|
||||||
|
|
||||||
Oben links stehen nur Zeit und Mission. Alles Weitere steht im Journal.
|
Oben links stehen nur Zeit und Mission. Alles Weitere steht im Journal.
|
||||||
In der Traumwelt springt **Leertaste**; auf der Erde steigt sie und **Umschalt**
|
In der Traumwelt springt **Leertaste**; auf der Erde wechselt sie ein
|
||||||
lässt sinken.
|
Stockwerk nach oben, **Umschalt** eines nach unten.
|
||||||
|
|
||||||
## Spielschleife
|
## Spielschleife
|
||||||
|
|
||||||
@@ -133,42 +137,25 @@ Haus wird ein neuer Weg gelehrt. Zu Beginn kennt das Dorf nur einen.
|
|||||||
Nach jedem Einsatz braucht eine Technik 30 Sekunden Ruhe. Ein Einbau gelingt
|
Nach jedem Einsatz braucht eine Technik 30 Sekunden Ruhe. Ein Einbau gelingt
|
||||||
nicht immer — Übung entscheidet.
|
nicht immer — Übung entscheidet.
|
||||||
|
|
||||||
**Traumwelt** — eine schwebende Insel mit einem Dorf, das wächst
|
**Traumwelt** — eine schwebende Insel, die ein Hügel ist
|
||||||
|
|
||||||
* Fest sind nur drei Dinge: der **Hörsaal der Wege**, der **Dorfplatz** und das
|
* Fest sind nur drei Dinge: der **Hörsaal der Wege**, der **Dorfplatz** um den
|
||||||
**Tor zur Erde**. Alles andere entsteht beim Wachsen.
|
leuchtenden **Traumbaum** und das **Tor zur Erde**. Alles andere entsteht
|
||||||
* **Die Insel beginnt kahl.** Ein Gebäude, kein Bewohner, ein einziger
|
beim Wachsen.
|
||||||
gelehrter Weg — und den bringt dein Wesen schon mit. Am Brett hängt also vom
|
* **Die Insel beginnt kahl** und eben. Ab dem Dorfplatz steigt sie an; jeder
|
||||||
ersten Moment an eine Mission.
|
erfüllte Traum bringt einen Bewohner mit seinem Haus, und wer später kommt,
|
||||||
* Jeder erfüllte Traum bringt einen Bewohner mit seinem Haus. **In jedem Haus
|
wohnt weiter außen und damit höher. Das Dorf wächst den Hügel hinauf — vom
|
||||||
wird ein Weg gelehrt, den es vorher nirgends gab.**
|
Platz aus sieht man, wie weit man gekommen ist.
|
||||||
* Das Haus setzt sich nicht auf einen Ring, sondern **sucht sich seinen Platz**:
|
* Jedes Haus steht auf einer **Terrasse** in Höhe seiner Tür, die als
|
||||||
gewertet werden Nachbarschaft, Nähe zu einem Weg, Nähe zum Dorfplatz und wie
|
Böschung in den Hang ausläuft. Dach, Laterne an der Tür, Licht innen. Die
|
||||||
leer die Himmelsrichtung noch ist. Der beste von 64 Probepunkten gewinnt.
|
Wege folgen dem Hang und tragen Laternen. Spieler und Bewohner laufen auf
|
||||||
* Der Weg dorthin **dockt an der nächsten Stelle des bestehenden Netzes an** —
|
dem Boden, den ein Höhenfeld trägt.
|
||||||
nicht an der Dorfmitte. Kreuzt die Gerade ein Haus, sucht ein Raster einen Weg
|
* Häuser suchen sich ihren Platz selbst — mit Nachbarn, Weganschluss und dem
|
||||||
darum herum. Liegt ein Knoten nah, ist aber im Netz weit weg, kommt eine
|
Dorfplatz in erträglicher Nähe — und meiden das Tor. Ein Weg führt nie
|
||||||
Abkürzung dazu. Daraus entstehen Gabeln, Schleifen und krumme Gassen.
|
durch ein Haus; findet die Suche keinen, wird feiner gesucht und notfalls
|
||||||
* Die Insel dehnt sich mit, Bäume füllen nach Poisson-Regel die freie Fläche.
|
woanders angedockt.
|
||||||
Vorbild: Emilien et al., *Procedural Generation of Villages* (2012).
|
|
||||||
* **Schwarzes Brett** am Dorfplatz — hier hängen die Menschen aus, die nicht
|
* **Schwarzes Brett** am Dorfplatz — hier hängen die Menschen aus, die nicht
|
||||||
weiterkommen. **Ausgehängt wird nur, wem du mit dem, was du kannst, auch
|
weiterkommen. Mission annehmen.
|
||||||
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.
|
|
||||||
|
|
||||||
**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
|
**Erde** — ein Stadtbezirk nach japanischem Vorbild
|
||||||
|
|
||||||
@@ -179,7 +166,17 @@ Abwehr. Am Ausgang ändert sich dadurch nichts, nur an der Wartezeit.
|
|||||||
* **Hier wächst nichts.** Alle 16 Menschen sind von Anfang an da und leben
|
* **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,
|
ihren Tag: schlafen, arbeiten, grübeln, essen, spazieren. Wer wo wohnt,
|
||||||
arbeitet in der Nähe; das Café liegt in der Mitte.
|
arbeitet in der Nähe; das Café liegt in der Mitte.
|
||||||
* Du bist ein unsichtbarer Geist, schwebst und gehst durch Wände.
|
* **Die Stadt ist verschachtelt.** Wohnhäuser und Sonderbauten sind
|
||||||
|
geschlossene Häuser mit zwei bis vier Stockwerken, Treppenhaus neben der
|
||||||
|
Tür, Fenstern und Möbeln. Jeder Mensch hat seine Wohnung in einer Etage,
|
||||||
|
im Büro und an der Hochschule sitzt jeder auf seinem Stockwerk. Die
|
||||||
|
Bewohner gehen die Treppe hoch und runter. Alles andere steht als
|
||||||
|
geschlossener Klotz da.
|
||||||
|
* Du bist ein unsichtbarer Geist, schwebst und gehst durch Wände. Bist du
|
||||||
|
in einem Haus, greift der **Etagenschnitt**: Dach, Etagen über dir und
|
||||||
|
die Wände zur Kamera hin verschwinden, wie bei den Sims. Ein Haus, das
|
||||||
|
zwischen Kamera und dir steht, wird durchsichtig. Wer auf einer anderen
|
||||||
|
Etage ist, ist nicht im Fokus — durch die Decke sieht man nichts.
|
||||||
* **F** hängt dich an einen Menschen: du treibst hinter ihm her, durch Türen
|
* **F** hängt dich an einen Menschen: du treibst hinter ihm her, durch Türen
|
||||||
und über den ganzen Tag, ohne ihm nachlaufen zu müssen. **F** noch einmal —
|
und über den ganzen Tag, ohne ihm nachlaufen zu müssen. **F** noch einmal —
|
||||||
oder ein Schritt mit **WASD** — lässt ihn wieder los.
|
oder ein Schritt mit **WASD** — lässt ihn wieder los.
|
||||||
@@ -221,18 +218,18 @@ eintauschen könnte. Ein Erfolg bringt 2 EP, ein Fehlversuch 1 — auch der zäh
|
|||||||
scripts/human_db.gd Bezirk: Arterien, Blöcke, roji, Parzellen, Menschen
|
scripts/human_db.gd Bezirk: Arterien, Blöcke, roji, Parzellen, Menschen
|
||||||
scripts/dream_db.gd Traumdorf: wachsende Häuser, Wege, Bewohner, Lehrorte
|
scripts/dream_db.gd Traumdorf: wachsende Häuser, Wege, Bewohner, Lehrorte
|
||||||
scripts/netz.gd Wegenetz: Knoten, Kanten, Andocken, kürzester Weg
|
scripts/netz.gd Wegenetz: Knoten, Kanten, Andocken, kürzester Weg
|
||||||
scripts/orte.gd Orte, Türen, Wege, Tätigkeiten (beide Welten)
|
scripts/orte.gd Orte, Türen, Wege, Etagen, Treppenhaus (beide Welten)
|
||||||
scripts/bewohner.gd Bewohner mit Tagesablauf und Slots
|
scripts/bewohner.gd Bewohner mit Tagesablauf, Hut nach Beruf, Haltung nach Klemme
|
||||||
scripts/main.gd Weltenwechsel, Fokus, Interaktion
|
scripts/main.gd Weltenwechsel, Fokus, Interaktion
|
||||||
scripts/player.gd Läufer in der Traumwelt, Geist auf der Erde, Verfolgung
|
scripts/player.gd Geist: feste Kamera, Etagenwechsel, Schleier, Funken, Verfolgung
|
||||||
scripts/dream_world.gd Insel mit Dorf
|
scripts/dream_world.gd Insel mit Dorf
|
||||||
scripts/earth_world.gd Stadt mit Tag-Nacht-Wechsel
|
scripts/earth_world.gd Stadt mit Stockwerken, Treppen, Etagenschnitt, Tag-Nacht-Wechsel
|
||||||
scripts/ursachen.gd Ursachen, blockierende Tätigkeiten, Belohnungen
|
scripts/ursachen.gd Ursachen, blockierende Tätigkeiten, Belohnungen
|
||||||
scripts/probleme.gd Psychische Probleme und die Momente, in denen sie aufbrechen
|
scripts/probleme.gd Psychische Probleme und die Momente, in denen sie aufbrechen
|
||||||
scripts/woerter.gd Alltagswörter und was das Spiel daraus schließt
|
scripts/woerter.gd Alltagswörter und was das Spiel daraus schließt
|
||||||
scripts/aushaenge.gd Eigene Aushänge: speichern, laden, prüfen
|
scripts/aushaenge.gd Eigene Aushänge: speichern, laden, prüfen
|
||||||
scripts/ui.gd HUD, Journal, Tagesablauf, Hörsaal, Brett, Panels
|
scripts/ui.gd HUD, Journal, Tagesablauf, Hörsaal, Brett, Panels
|
||||||
scripts/prims.gd Bausteine aus Primitiven
|
scripts/prims.gd Bausteine aus Primitiven, Etagenschnitt
|
||||||
|
|
||||||
## Prüfen
|
## Prüfen
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class_name Bewohner
|
|||||||
## einer Technik erreicht werden -- oder eben nicht.
|
## einer Technik erreicht werden -- oder eben nicht.
|
||||||
|
|
||||||
const GEHTEMPO := 8.0
|
const GEHTEMPO := 8.0
|
||||||
|
const MASSSTAB := 0.8 # Menschen sind klein, die Stadt wirkt groß
|
||||||
|
|
||||||
var data: Dictionary
|
var data: Dictionary
|
||||||
var idx: int = 0
|
var idx: int = 0
|
||||||
@@ -16,6 +17,8 @@ var anker: Dictionary = {}
|
|||||||
var mit_mission := false
|
var mit_mission := false
|
||||||
|
|
||||||
var _visual: Node3D
|
var _visual: Node3D
|
||||||
|
var _ober: Node3D # Oberkörper: neigt sich, duckt sich, zappelt
|
||||||
|
var _arme: Array = []
|
||||||
var _label: Label3D
|
var _label: Label3D
|
||||||
var _koerper: MeshInstance3D
|
var _koerper: MeshInstance3D
|
||||||
var _kopf: MeshInstance3D
|
var _kopf: MeshInstance3D
|
||||||
@@ -23,6 +26,7 @@ var _aura: MeshInstance3D
|
|||||||
var _strahl: MeshInstance3D
|
var _strahl: MeshInstance3D
|
||||||
var _path: Array = []
|
var _path: Array = []
|
||||||
var _letzte_aktivitaet: String = ""
|
var _letzte_aktivitaet: String = ""
|
||||||
|
var boden: Callable = Callable() # Höhe des Bodens (x, z) -> y; leer heißt eben
|
||||||
var slot: int = 0
|
var slot: int = 0
|
||||||
var _bob: float = 0.0
|
var _bob: float = 0.0
|
||||||
var _offset: Vector3
|
var _offset: Vector3
|
||||||
@@ -41,18 +45,30 @@ func setup(d: Dictionary, index: int, orte: Dictionary,
|
|||||||
_offset = Vector3(cos(winkel), 0, sin(winkel)) * 2.2
|
_offset = Vector3(cos(winkel), 0, sin(winkel)) * 2.2
|
||||||
|
|
||||||
_visual = Node3D.new()
|
_visual = Node3D.new()
|
||||||
|
_visual.scale = Vector3.ONE * MASSSTAB
|
||||||
add_child(_visual)
|
add_child(_visual)
|
||||||
var c: Color = d["farbe"]
|
var c: Color = d["farbe"]
|
||||||
var leuchtet := bool(d.get("leuchtet", false))
|
var leuchtet := bool(d.get("leuchtet", false))
|
||||||
var hoehe := 1.05 + float(index % 3) * 0.08
|
var hoehe := 1.05 + float(index % 3) * 0.08
|
||||||
_koerper = Prims.capsule(_visual, Vector3(0, hoehe * 0.62, 0), 0.32, hoehe, c)
|
_ober = Node3D.new()
|
||||||
_kopf = Prims.sphere(_visual, Vector3(0, hoehe * 1.18 + 0.1, 0), 0.24, c.lightened(0.45))
|
_ober.position.y = hoehe * 0.45
|
||||||
Prims.box(_visual, Vector3(0, hoehe * 0.95, 0), Vector3(0.9, 0.14, 0.3), c.darkened(0.25), false)
|
_visual.add_child(_ober)
|
||||||
|
var o := Vector3(0, -hoehe * 0.45, 0)
|
||||||
|
_koerper = Prims.capsule(_ober, o + Vector3(0, hoehe * 0.62, 0), 0.32, hoehe, c)
|
||||||
|
_kopf = Prims.sphere(_ober, o + Vector3(0, hoehe * 1.18 + 0.1, 0), 0.24, c.lightened(0.45))
|
||||||
|
Prims.box(_ober, o + Vector3(0, hoehe * 0.95, 0), Vector3(0.9, 0.14, 0.3), c.darkened(0.25), false)
|
||||||
|
for sx: float in [-1.0, 1.0]:
|
||||||
|
var gelenk := Node3D.new()
|
||||||
|
gelenk.position = o + Vector3(sx * 0.46, hoehe * 0.92, 0)
|
||||||
|
_ober.add_child(gelenk)
|
||||||
|
Prims.capsule(gelenk, Vector3(0, -0.32, 0), 0.09, 0.62, c.darkened(0.1))
|
||||||
|
_arme.append(gelenk)
|
||||||
|
_hut(o + Vector3(0, hoehe * 1.18 + 0.1, 0), c)
|
||||||
Prims.box(_visual, Vector3(0, 0.06, 0), Vector3(0.5, 0.12, 0.7), c.darkened(0.45), false)
|
Prims.box(_visual, Vector3(0, 0.06, 0), Vector3(0.5, 0.12, 0.7), c.darkened(0.45), false)
|
||||||
if leuchtet:
|
if leuchtet:
|
||||||
_koerper.material_override = Prims.mat(c, 0.9)
|
_koerper.material_override = Prims.mat(c, 0.9)
|
||||||
_kopf.material_override = Prims.mat(c.lightened(0.45), 0.8)
|
_kopf.material_override = Prims.mat(c.lightened(0.45), 0.8)
|
||||||
var reif := Prims.cyl(_visual, Vector3(0, hoehe * 0.5, 0), 0.62, 0.05,
|
var reif := Prims.cyl(_ober, o + Vector3(0, hoehe * 0.5, 0), 0.62, 0.05,
|
||||||
c.lightened(0.3), false, 1.6)
|
c.lightened(0.3), false, 1.6)
|
||||||
reif.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
reif.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
|
||||||
@@ -61,13 +77,85 @@ func setup(d: Dictionary, index: int, orte: Dictionary,
|
|||||||
_strahl = Prims.cyl(self, Vector3(0, 11.0, 0), 0.14, 16.0, Color(1, 0.85, 0.35, 0.22), false, 1.2)
|
_strahl = Prims.cyl(self, Vector3(0, 11.0, 0), 0.14, 16.0, Color(1, 0.85, 0.35, 0.22), false, 1.2)
|
||||||
_strahl.visible = false
|
_strahl.visible = false
|
||||||
|
|
||||||
_label = Prims.label(self, Vector3(0, 2.9, 0), d["name"], 0.30)
|
# gestaffelt, damit sich Schilder in einem Raum nicht übereinanderlegen
|
||||||
|
_label = Prims.label(self, Vector3(0, 2.3 + float(index % 5) * 0.35, 0), d["name"], 0.36)
|
||||||
|
|
||||||
var start := _routine_at(Game.time_h)
|
var start := _routine_at(Game.time_h)
|
||||||
place = start[0]
|
place = start[0]
|
||||||
activity = start[1]
|
activity = start[1]
|
||||||
_letzte_aktivitaet = activity
|
_letzte_aktivitaet = activity
|
||||||
global_position = Orte.center(places, place) + _offset
|
global_position = Orte.center(places, place) + _offset + Vector3(0, Orte.hoehe(etage(place)), 0)
|
||||||
|
|
||||||
|
|
||||||
|
## In welcher Etage dieser Mensch an einem Ort ist -- 0, wenn er dort nur unten ist.
|
||||||
|
func etage(ort: String) -> int:
|
||||||
|
return int((data.get("etage", {}) as Dictionary).get(ort, 0))
|
||||||
|
|
||||||
|
|
||||||
|
## Wege führen durchs Treppenhaus: aus der Etage runter zur Tür, und am Ziel
|
||||||
|
## von der Tür hinauf in die Etage.
|
||||||
|
func _mit_etagen(pts: Array, von: String, nach: String, war_unterwegs: bool) -> Array:
|
||||||
|
if pts.is_empty():
|
||||||
|
return pts
|
||||||
|
var e_von := etage(von)
|
||||||
|
if e_von > 0 and not war_unterwegs:
|
||||||
|
var runter: Array = [Orte.treppe(places, von, e_von)]
|
||||||
|
runter.append_array(Orte.treppen_weg(places, von, e_von, 0))
|
||||||
|
runter.append(Orte.innen_tuer(places, von))
|
||||||
|
runter.append_array(pts)
|
||||||
|
pts = runter
|
||||||
|
var e_nach := etage(nach)
|
||||||
|
if e_nach > 0:
|
||||||
|
pts.pop_back()
|
||||||
|
pts.append(Orte.innen_tuer(places, nach))
|
||||||
|
pts.append(Orte.treppe(places, nach, 0))
|
||||||
|
pts.append_array(Orte.treppen_weg(places, nach, 0, e_nach))
|
||||||
|
pts.append(Orte.center(places, nach) + _offset + Vector3(0, Orte.hoehe(e_nach), 0))
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
## Kopfbedeckung nach Tagestyp des Berufs -- so unterscheiden sich die Leute
|
||||||
|
## schon aus der Ferne.
|
||||||
|
func _hut(kopf: Vector3, c: Color) -> void:
|
||||||
|
var typ := str(HumanDB.BERUF_TAGESTYP.get(str(data.get("beruf", "")), ""))
|
||||||
|
var dunkel := c.darkened(0.4)
|
||||||
|
match typ:
|
||||||
|
"frueh": # Mütze
|
||||||
|
Prims.cyl(_ober, kopf + Vector3(0, 0.2, 0), 0.22, 0.14, dunkel, false)
|
||||||
|
"buero": # Hut mit Krempe
|
||||||
|
Prims.cyl(_ober, kopf + Vector3(0, 0.16, 0), 0.36, 0.04, dunkel, false)
|
||||||
|
Prims.cyl(_ober, kopf + Vector3(0, 0.3, 0), 0.18, 0.24, dunkel, false)
|
||||||
|
"studium": # Zipfel
|
||||||
|
var z := Prims.cyl(_ober, kopf + Vector3(0, 0.36, 0), 0.2, 0.4, dunkel, false)
|
||||||
|
(z.mesh as CylinderMesh).top_radius = 0.0
|
||||||
|
"werkstatt": # Kappe mit Schirm
|
||||||
|
Prims.cyl(_ober, kopf + Vector3(0, 0.18, 0), 0.25, 0.1, dunkel, false)
|
||||||
|
Prims.box(_ober, kopf + Vector3(0, 0.14, -0.3), Vector3(0.3, 0.04, 0.2), dunkel, false)
|
||||||
|
_:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
## Angst duckt, Erschöpfung beugt, Unruhe zappelt -- solange die Klemme nicht
|
||||||
|
## überwunden ist. So sieht man Probleme beim Beobachten am Körper.
|
||||||
|
func _haltung(delta: float, geht: bool) -> void:
|
||||||
|
var id := str(data["id"])
|
||||||
|
var offen: Dictionary = {}
|
||||||
|
for k in data.get("probleme", []):
|
||||||
|
if not Game.problem_ueberwunden(id, str(k)):
|
||||||
|
offen[str(k)] = true
|
||||||
|
var neigung := 0.38 if offen.has("erschoepfung") else 0.0
|
||||||
|
var groesse := 0.86 if offen.has("angst") else 1.0
|
||||||
|
var zappeln := 0.0
|
||||||
|
if offen.has("unruhe") and not geht:
|
||||||
|
zappeln = sin(Time.get_ticks_msec() * 0.011) * 0.06
|
||||||
|
_ober.rotation.x = lerpf(_ober.rotation.x, -neigung, 1.0 - pow(0.02, delta))
|
||||||
|
_ober.rotation.z = lerpf(_ober.rotation.z, zappeln, 1.0 - pow(0.001, delta))
|
||||||
|
_ober.scale = _ober.scale.lerp(Vector3.ONE * groesse, 1.0 - pow(0.02, delta))
|
||||||
|
var schwung := sin(_bob) * 0.7 if geht else 0.0
|
||||||
|
for i in range(_arme.size()):
|
||||||
|
var a: Node3D = _arme[i]
|
||||||
|
a.rotation.x = lerpf(a.rotation.x, schwung * (1.0 if i == 0 else -1.0),
|
||||||
|
1.0 - pow(0.001, delta))
|
||||||
|
|
||||||
|
|
||||||
## [Ort, Tätigkeit, Slotnummer] zur Stunde h. Steht in dem Slot ein System,
|
## [Ort, Tätigkeit, Slotnummer] zur Stunde h. Steht in dem Slot ein System,
|
||||||
@@ -95,6 +183,7 @@ func _process(roh: float) -> void:
|
|||||||
_path = Orte.route(places, place, soll[0], _offset, netz, anker)
|
_path = Orte.route(places, place, soll[0], _offset, netz, anker)
|
||||||
if war_unterwegs and _path.size() > 1:
|
if war_unterwegs and _path.size() > 1:
|
||||||
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
|
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
|
||||||
|
_path = _mit_etagen(_path, place, soll[0], war_unterwegs)
|
||||||
place = soll[0]
|
place = soll[0]
|
||||||
activity = "unterwegs"
|
activity = "unterwegs"
|
||||||
elif _path.is_empty() and activity != soll[1]:
|
elif _path.is_empty() and activity != soll[1]:
|
||||||
@@ -102,9 +191,9 @@ func _process(roh: float) -> void:
|
|||||||
|
|
||||||
if not _path.is_empty():
|
if not _path.is_empty():
|
||||||
var ziel: Vector3 = _path[0]
|
var ziel: Vector3 = _path[0]
|
||||||
ziel.y = 0.0
|
|
||||||
var hier := global_position
|
var hier := global_position
|
||||||
hier.y = 0.0
|
if boden.is_valid():
|
||||||
|
ziel.y = hier.y # das Gelände bestimmt die Höhe, nicht der Wegpunkt
|
||||||
var d := ziel - hier
|
var d := ziel - hier
|
||||||
if d.length() < 0.4:
|
if d.length() < 0.4:
|
||||||
_path.remove_at(0)
|
_path.remove_at(0)
|
||||||
@@ -121,6 +210,9 @@ func _process(roh: float) -> void:
|
|||||||
|
|
||||||
if _path.is_empty():
|
if _path.is_empty():
|
||||||
_visual.position.y = lerpf(_visual.position.y, 0.0, 1.0 - pow(0.01, delta))
|
_visual.position.y = lerpf(_visual.position.y, 0.0, 1.0 - pow(0.01, delta))
|
||||||
|
_haltung(delta, not _path.is_empty())
|
||||||
|
if boden.is_valid():
|
||||||
|
global_position.y = float(boden.call(global_position.x, global_position.z))
|
||||||
|
|
||||||
_visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0,
|
_visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0,
|
||||||
1.0 - pow(0.01, delta))
|
1.0 - pow(0.01, delta))
|
||||||
@@ -161,7 +253,7 @@ func beschriftung() -> String:
|
|||||||
|
|
||||||
## Sichtbare Reaktion auf eine angewandte Technik.
|
## Sichtbare Reaktion auf eine angewandte Technik.
|
||||||
func reagiere(success: bool, text: String) -> void:
|
func reagiere(success: bool, text: String) -> void:
|
||||||
var l := Prims.label(self, Vector3(0, 3.5, 0), text, 0.34,
|
var l := Prims.label(self, Vector3(0, 3.0, 0), text, 0.4,
|
||||||
Color(0.55, 1.0, 0.6) if success else Color(1, 0.55, 0.45))
|
Color(0.55, 1.0, 0.6) if success else Color(1, 0.55, 0.45))
|
||||||
var tw := l.create_tween()
|
var tw := l.create_tween()
|
||||||
tw.set_parallel(true)
|
tw.set_parallel(true)
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ const WEG_ABSTAND := 3.0 # Mindestluft zwischen Haus und Weg
|
|||||||
const KANDIDATEN := 64 # Probepunkte je Haus
|
const KANDIDATEN := 64 # Probepunkte je Haus
|
||||||
const SPINE_SCHRITT := 8.0 # Abstand der Knoten auf der Hauptachse
|
const SPINE_SCHRITT := 8.0 # Abstand der Knoten auf der Hauptachse
|
||||||
|
|
||||||
|
## Die Insel ist ein Hügel. Innen eben -- Dorfplatz, Hörsaal, Tor --, ab FLACH
|
||||||
|
## steigt sie an. Wer später einzieht, wohnt weiter außen und damit höher:
|
||||||
|
## das Dorf wächst den Hügel hinauf, und vom Platz aus sieht man, wie weit.
|
||||||
|
const FLACH := 24.0
|
||||||
|
const STEIGUNG := 0.30
|
||||||
|
const GIPFEL := 36.0
|
||||||
|
|
||||||
static var FEST: Dictionary = {
|
static var FEST: Dictionary = {
|
||||||
"hoersaal": {
|
"hoersaal": {
|
||||||
"name": "Hörsaal der Wege", "pos": [0.0, 34.0], "size": [16.0, 12.0],
|
"name": "Hörsaal der Wege", "pos": [0.0, 34.0], "size": [16.0, 12.0],
|
||||||
@@ -56,6 +63,72 @@ static var ABLAEUFE: Array = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
## Höhe des nackten Hangs bei diesem Abstand von der Mitte.
|
||||||
|
static func hang(r: float) -> float:
|
||||||
|
return clampf((r - FLACH) * STEIGUNG, 0.0, GIPFEL)
|
||||||
|
|
||||||
|
|
||||||
|
## Jedes Gebäude steht auf einer ebenen Terrasse in Höhe seiner Tür -- so ist
|
||||||
|
## die Türseite ebenerdig, die Bergseite eingegraben. Das Tor hat keine Tür;
|
||||||
|
## seine Terrasse liegt auf Höhe der Dorfseite.
|
||||||
|
const TOR_FLAECHE := 12.0
|
||||||
|
const TERRASSE_RAND := 2.6 # bis zur Türschwelle davor
|
||||||
|
const BOESCHUNG := 5.0 # so weit läuft die Terrasse in den Hang aus
|
||||||
|
|
||||||
|
static func terrasse(key: String) -> float:
|
||||||
|
if not PLACES.has(key):
|
||||||
|
return 0.0
|
||||||
|
if key == "tor":
|
||||||
|
var c := Orte.center(PLACES, key)
|
||||||
|
return hang(Vector2(c.x, c.z).length() - TOR_FLAECHE * 0.5)
|
||||||
|
var t := Orte.door(PLACES, key)
|
||||||
|
return hang(Vector2(t.x, t.z).length())
|
||||||
|
|
||||||
|
|
||||||
|
## Höhe des Bodens an dieser Stelle: Terrasse in einem Gebäude, sonst Hang.
|
||||||
|
## Wo sich Terrassenränder überlappen, gilt das Gebäude, in dem der Punkt
|
||||||
|
## wirklich liegt -- erst danach der Rand.
|
||||||
|
static func hoehe_bei(x: float, z: float) -> float:
|
||||||
|
var h := hang(Vector2(x, z).length())
|
||||||
|
var rand_treffer := ""
|
||||||
|
var boeschung_key := ""
|
||||||
|
var boeschung_t := 1.0
|
||||||
|
var reihenfolge: Array = PLACES.keys()
|
||||||
|
reihenfolge.erase("tor")
|
||||||
|
reihenfolge.append("tor")
|
||||||
|
for key in reihenfolge:
|
||||||
|
var s: Array = PLACES[key]["size"]
|
||||||
|
var bx: float = float(s[0])
|
||||||
|
var bz: float = float(s[1])
|
||||||
|
if key == "tor":
|
||||||
|
bx = TOR_FLAECHE
|
||||||
|
bz = TOR_FLAECHE
|
||||||
|
if bx <= 0.0 or not Game.ort_offen(key):
|
||||||
|
continue
|
||||||
|
var c := Orte.center(PLACES, key)
|
||||||
|
var dx := absf(x - c.x) - bx * 0.5
|
||||||
|
var dz := absf(z - c.z) - bz * 0.5
|
||||||
|
if dx <= 0.0 and dz <= 0.0:
|
||||||
|
return terrasse(key)
|
||||||
|
if dx <= TERRASSE_RAND and dz <= TERRASSE_RAND:
|
||||||
|
if rand_treffer == "":
|
||||||
|
rand_treffer = key
|
||||||
|
continue
|
||||||
|
# außerhalb des Rands: die Terrasse läuft als Böschung in den Hang aus
|
||||||
|
var d := maxf(dx, dz) - TERRASSE_RAND
|
||||||
|
if d < BOESCHUNG:
|
||||||
|
var t := d / BOESCHUNG
|
||||||
|
if t < boeschung_t:
|
||||||
|
boeschung_t = t
|
||||||
|
boeschung_key = key
|
||||||
|
if rand_treffer != "":
|
||||||
|
return terrasse(rand_treffer)
|
||||||
|
if boeschung_key != "":
|
||||||
|
var tt := boeschung_t * boeschung_t * (3.0 - 2.0 * boeschung_t) # weich
|
||||||
|
return lerpf(terrasse(boeschung_key), h, tt)
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
static func haus_key(i: int) -> String:
|
static func haus_key(i: int) -> String:
|
||||||
return "haus_%d" % i
|
return "haus_%d" % i
|
||||||
|
|
||||||
@@ -171,11 +244,16 @@ static func _platz_frei(p: Vector3, groesse: Vector2) -> bool:
|
|||||||
return false
|
return false
|
||||||
for key in PLACES:
|
for key in PLACES:
|
||||||
var s: Array = PLACES[key]["size"]
|
var s: Array = PLACES[key]["size"]
|
||||||
if float(s[0]) <= 0.0:
|
var bx: float = float(s[0])
|
||||||
|
var bz: float = float(s[1])
|
||||||
|
if key == "tor":
|
||||||
|
bx = TOR_FLAECHE
|
||||||
|
bz = TOR_FLAECHE
|
||||||
|
if bx <= 0.0:
|
||||||
continue
|
continue
|
||||||
var c := Orte.center(PLACES, key)
|
var c := Orte.center(PLACES, key)
|
||||||
var dx: float = absf(p.x - c.x) - (groesse.x + float(s[0])) * 0.5
|
var dx: float = absf(p.x - c.x) - (groesse.x + bx) * 0.5
|
||||||
var dz: float = absf(p.z - c.z) - (groesse.y + float(s[1])) * 0.5
|
var dz: float = absf(p.z - c.z) - (groesse.y + bz) * 0.5
|
||||||
if dx < HAUS_ABSTAND and dz < HAUS_ABSTAND:
|
if dx < HAUS_ABSTAND and dz < HAUS_ABSTAND:
|
||||||
return false
|
return false
|
||||||
for k in NETZ.kanten:
|
for k in NETZ.kanten:
|
||||||
@@ -249,6 +327,18 @@ static func _anschliessen(key: String) -> void:
|
|||||||
var an := NETZ.andocken(tuer)
|
var an := NETZ.andocken(tuer)
|
||||||
var von: Vector3 = an["punkt"]
|
var von: Vector3 = an["punkt"]
|
||||||
var linie := _route_um_haeuser(tuer, von, key)
|
var linie := _route_um_haeuser(tuer, von, key)
|
||||||
|
if not _linie_frei(linie, key):
|
||||||
|
# Der nächste Punkt des Netzes liegt eingeklemmt. Dann den nächsten
|
||||||
|
# Knoten nehmen, zu dem ein sauberer Weg führt -- nie durch ein Haus.
|
||||||
|
var reihe: Array = range(NETZ.knoten.size())
|
||||||
|
reihe.sort_custom(func(i, j) -> bool:
|
||||||
|
return NETZ.knoten[i].distance_to(tuer) < NETZ.knoten[j].distance_to(tuer))
|
||||||
|
for idx in reihe.slice(0, 16):
|
||||||
|
var l2 := _route_um_haeuser(tuer, NETZ.knoten[idx], key)
|
||||||
|
if _linie_frei(l2, key):
|
||||||
|
linie = l2
|
||||||
|
an = {"punkt": NETZ.knoten[idx], "knoten": idx}
|
||||||
|
break
|
||||||
var vorher := -1
|
var vorher := -1
|
||||||
for i in range(linie.size()):
|
for i in range(linie.size()):
|
||||||
var idx := NETZ.fuege_knoten(linie[i])
|
var idx := NETZ.fuege_knoten(linie[i])
|
||||||
@@ -264,17 +354,33 @@ static func _anschliessen(key: String) -> void:
|
|||||||
_schleife(int(ANKER[key]), key)
|
_schleife(int(ANKER[key]), key)
|
||||||
|
|
||||||
|
|
||||||
|
## Schneidet diese Linie irgendwo ein Gebäude?
|
||||||
|
static func _linie_frei(linie: Array, ausser: String) -> bool:
|
||||||
|
for i in range(linie.size() - 1):
|
||||||
|
if not _weg_frei(linie[i], linie[i + 1], ausser):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
## Weg von a nach b, der kein Haus schneidet. Frei? Dann die Gerade. Sonst
|
## 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.
|
## Dijkstra über ein Raster -- erst grob, dann fein --, danach geglättet.
|
||||||
static func _route_um_haeuser(a: Vector3, b: Vector3, ausser: String) -> Array:
|
static func _route_um_haeuser(a: Vector3, b: Vector3, ausser: String) -> Array:
|
||||||
if _weg_frei(a, b, ausser):
|
if _weg_frei(a, b, ausser):
|
||||||
return [a, b]
|
return [a, b]
|
||||||
var rand := 30.0
|
for schritt_start in [5.0, 2.5]:
|
||||||
|
var weg := _route_raster(a, b, ausser, schritt_start)
|
||||||
|
if not weg.is_empty():
|
||||||
|
return weg
|
||||||
|
return [a, b]
|
||||||
|
|
||||||
|
|
||||||
|
static func _route_raster(a: Vector3, b: Vector3, ausser: String, schritt_start: float) -> Array:
|
||||||
|
var rand := 34.0
|
||||||
var min_x := minf(a.x, b.x) - rand
|
var min_x := minf(a.x, b.x) - rand
|
||||||
var max_x := maxf(a.x, b.x) + rand
|
var max_x := maxf(a.x, b.x) + rand
|
||||||
var min_z := minf(a.z, b.z) - rand
|
var min_z := minf(a.z, b.z) - rand
|
||||||
var max_z := maxf(a.z, b.z) + rand
|
var max_z := maxf(a.z, b.z) + rand
|
||||||
var schritt := 5.0
|
var schritt := schritt_start
|
||||||
var nx := int((max_x - min_x) / schritt) + 1
|
var nx := int((max_x - min_x) / schritt) + 1
|
||||||
var nz := int((max_z - min_z) / schritt) + 1
|
var nz := int((max_z - min_z) / schritt) + 1
|
||||||
while nx * nz > 2600:
|
while nx * nz > 2600:
|
||||||
@@ -320,7 +426,7 @@ static func _route_um_haeuser(a: Vector3, b: Vector3, ausser: String) -> Array:
|
|||||||
if not offen.has(n):
|
if not offen.has(n):
|
||||||
offen.append(n)
|
offen.append(n)
|
||||||
if not gefunden:
|
if not gefunden:
|
||||||
return [a, b]
|
return []
|
||||||
|
|
||||||
var roh: Array = []
|
var roh: Array = []
|
||||||
var cur: Vector2i = ziel
|
var cur: Vector2i = ziel
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
class_name DreamWorld
|
class_name DreamWorld
|
||||||
## Die Traumwelt: eine schwebende Insel mit einem Dorf. Vier Wesen wohnen hier,
|
## Die Traumwelt: eine schwebende Insel, die ein Hügel ist. Unten der Dorfplatz
|
||||||
## jedes in seinem Haus, jedes mit eigenem Tagesablauf.
|
## um den Traumbaum, der Hörsaal und das Tor; hangaufwärts wächst das Dorf,
|
||||||
|
## ein Haus je erfülltem Traum, jedes eine Terrasse höher.
|
||||||
|
|
||||||
var spawn := Vector3(0, 2.0, 20.0)
|
var spawn := Vector3(0, 2.0, 20.0)
|
||||||
var bewohner: Array = []
|
var bewohner: Array = []
|
||||||
|
var spieler: Node3D = null # setzt main, für den Etagenschnitt
|
||||||
|
var _bauten: Array = []
|
||||||
|
|
||||||
var _deko: Array = []
|
var _lichter: Array = []
|
||||||
var _freihalten: Array[Vector3] = []
|
var _freihalten: Array[Vector3] = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_environment()
|
_environment()
|
||||||
_insel()
|
_insel()
|
||||||
|
_wege()
|
||||||
_haeuser()
|
_haeuser()
|
||||||
_lehrpulte()
|
_lehrpulte()
|
||||||
_schwarzes_brett()
|
_schwarzes_brett()
|
||||||
@@ -22,146 +26,178 @@ func _ready() -> void:
|
|||||||
_wesen()
|
_wesen()
|
||||||
|
|
||||||
|
|
||||||
|
## Dämmerung: tiefes Blau oben, warmes Lavendel am Horizont, Nebel im Tal.
|
||||||
func _environment() -> void:
|
func _environment() -> void:
|
||||||
var we := WorldEnvironment.new()
|
var we := WorldEnvironment.new()
|
||||||
var env := Environment.new()
|
var env := Environment.new()
|
||||||
env.background_mode = Environment.BG_SKY
|
env.background_mode = Environment.BG_SKY
|
||||||
var sky := Sky.new()
|
var sky := Sky.new()
|
||||||
var mat := ProceduralSkyMaterial.new()
|
var mat := ProceduralSkyMaterial.new()
|
||||||
mat.sky_top_color = Color(0.10, 0.05, 0.24)
|
mat.sky_top_color = Color(0.05, 0.05, 0.19)
|
||||||
mat.sky_horizon_color = Color(0.42, 0.24, 0.55)
|
mat.sky_horizon_color = Color(0.58, 0.34, 0.56)
|
||||||
mat.ground_bottom_color = Color(0.06, 0.03, 0.14)
|
mat.ground_bottom_color = Color(0.04, 0.03, 0.11)
|
||||||
mat.ground_horizon_color = Color(0.32, 0.18, 0.45)
|
mat.ground_horizon_color = Color(0.30, 0.19, 0.42)
|
||||||
mat.sun_angle_max = 40.0
|
mat.sun_angle_max = 30.0
|
||||||
sky.sky_material = mat
|
sky.sky_material = mat
|
||||||
env.sky = sky
|
env.sky = sky
|
||||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||||
env.ambient_light_energy = 2.1
|
env.ambient_light_color = Color(0.58, 0.50, 0.78)
|
||||||
|
env.ambient_light_energy = 1.5
|
||||||
env.glow_enabled = true
|
env.glow_enabled = true
|
||||||
env.glow_intensity = 0.32
|
env.glow_intensity = 0.42
|
||||||
env.glow_bloom = 0.05
|
env.glow_bloom = 0.08
|
||||||
env.fog_enabled = true
|
env.fog_enabled = true
|
||||||
env.fog_light_color = Color(0.30, 0.18, 0.45)
|
env.fog_light_color = Color(0.46, 0.30, 0.56)
|
||||||
env.fog_density = 0.0015
|
env.fog_density = 0.0011
|
||||||
we.environment = env
|
we.environment = env
|
||||||
add_child(we)
|
add_child(we)
|
||||||
|
|
||||||
var sun := DirectionalLight3D.new()
|
var sun := DirectionalLight3D.new()
|
||||||
sun.rotation_degrees = Vector3(-46, 25, 0)
|
sun.rotation_degrees = Vector3(-34, 40, 0)
|
||||||
sun.light_energy = 1.5
|
sun.light_energy = 1.3
|
||||||
sun.light_color = Color(0.85, 0.80, 1.0)
|
sun.light_color = Color(1.0, 0.84, 0.74)
|
||||||
sun.shadow_enabled = true
|
sun.shadow_enabled = true
|
||||||
sun.shadow_blur = 2.0
|
sun.shadow_blur = 2.5
|
||||||
add_child(sun)
|
add_child(sun)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Insel
|
||||||
|
|
||||||
|
## Der Hügel als Höhenfeld: ein Gitter, das die Höhe von DreamDB abfragt --
|
||||||
|
## innen eben, außen ansteigend, in jedem Gebäude eine Terrasse. Darauf läuft
|
||||||
|
## der Spieler (Kollision) und die Bewohner (Bodenhöhe je Bild).
|
||||||
func _insel() -> void:
|
func _insel() -> void:
|
||||||
var radius := Game.insel_radius()
|
var radius := Game.insel_radius()
|
||||||
Prims.cyl(self, Vector3(0, -1.03, 0), radius, 2.0, Color(0.48, 0.40, 0.64))
|
var schritt := 1.5
|
||||||
Prims.cyl(self, Vector3(0, -4.0, 0), radius * 0.77, 4.0, Color(0.34, 0.26, 0.50), false)
|
var n := int(ceil(radius * 2.0 / schritt)) + 7
|
||||||
Prims.cyl(self, Vector3(0, -9.0, 0), radius * 0.42, 6.0, Color(0.24, 0.17, 0.38), false)
|
var halb := float(n - 1) * schritt * 0.5
|
||||||
|
var daten := PackedFloat32Array()
|
||||||
|
daten.resize(n * n)
|
||||||
|
var hoehen: Array = []
|
||||||
|
for iz in range(n):
|
||||||
|
var zeile: PackedFloat32Array = PackedFloat32Array()
|
||||||
|
zeile.resize(n)
|
||||||
|
for ix in range(n):
|
||||||
|
var x := -halb + float(ix) * schritt
|
||||||
|
var z := -halb + float(iz) * schritt
|
||||||
|
var r := Vector2(x, z).length()
|
||||||
|
var h: float
|
||||||
|
if r <= radius - 4.0:
|
||||||
|
h = DreamDB.hoehe_bei(x, z)
|
||||||
|
elif r <= radius:
|
||||||
|
# der Rand rundet sich ab, statt abzubrechen
|
||||||
|
var t := (r - (radius - 4.0)) / 4.0
|
||||||
|
h = lerpf(DreamDB.hoehe_bei(x, z), -1.5, t * t)
|
||||||
|
else:
|
||||||
|
# die Kante fällt ab -- die Insel schwebt. Begrenzt, sonst hängt
|
||||||
|
# das quadratische Gitter in den Ecken als Vorhang herab.
|
||||||
|
h = maxf(-1.5 - (r - radius) * 2.4, -12.0)
|
||||||
|
zeile[ix] = h
|
||||||
|
daten[iz * n + ix] = h
|
||||||
|
hoehen.append(zeile)
|
||||||
|
|
||||||
_wege()
|
var st := SurfaceTool.new()
|
||||||
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
var tief := Color(0.40, 0.40, 0.62)
|
||||||
|
var hoch := Color(0.70, 0.60, 0.88)
|
||||||
|
var kante := Color(0.16, 0.12, 0.27)
|
||||||
|
var eck := func(ix: int, iz: int) -> void:
|
||||||
|
var x := -halb + float(ix) * schritt
|
||||||
|
var z := -halb + float(iz) * schritt
|
||||||
|
var h: float = hoehen[iz][ix]
|
||||||
|
var hx0: float = hoehen[iz][maxi(ix - 1, 0)]
|
||||||
|
var hx1: float = hoehen[iz][mini(ix + 1, n - 1)]
|
||||||
|
var hz0: float = hoehen[maxi(iz - 1, 0)][ix]
|
||||||
|
var hz1: float = hoehen[mini(iz + 1, n - 1)][ix]
|
||||||
|
var nrm := Vector3(-(hx1 - hx0) / (2.0 * schritt), 1.0, -(hz1 - hz0) / (2.0 * schritt)).normalized()
|
||||||
|
var c: Color = kante if h < 0.0 else tief.lerp(hoch, clampf(h / DreamDB.GIPFEL, 0.0, 1.0))
|
||||||
|
st.set_color(c)
|
||||||
|
st.set_normal(nrm)
|
||||||
|
st.add_vertex(Vector3(x, h, z))
|
||||||
|
for iz in range(n - 1):
|
||||||
|
for ix in range(n - 1):
|
||||||
|
var mx := -halb + (float(ix) + 0.5) * schritt
|
||||||
|
var mz := -halb + (float(iz) + 0.5) * schritt
|
||||||
|
if Vector2(mx, mz).length() > radius + 5.0:
|
||||||
|
continue # außerhalb der Kante nichts zeichnen
|
||||||
|
eck.call(ix, iz)
|
||||||
|
eck.call(ix, iz + 1)
|
||||||
|
eck.call(ix + 1, iz)
|
||||||
|
eck.call(ix + 1, iz)
|
||||||
|
eck.call(ix, iz + 1)
|
||||||
|
eck.call(ix + 1, iz + 1)
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.mesh = st.commit()
|
||||||
|
var m := StandardMaterial3D.new()
|
||||||
|
m.vertex_color_use_as_albedo = true
|
||||||
|
m.roughness = 0.92
|
||||||
|
m.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
|
mi.material_override = m
|
||||||
|
add_child(mi)
|
||||||
|
|
||||||
# schwebende Deko
|
var body := StaticBody3D.new()
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
var shape := HeightMapShape3D.new()
|
||||||
|
shape.map_width = n
|
||||||
|
shape.map_depth = n
|
||||||
|
shape.map_data = daten
|
||||||
|
cs.shape = shape
|
||||||
|
cs.scale = Vector3(schritt, 1.0, schritt)
|
||||||
|
body.add_child(cs)
|
||||||
|
add_child(body)
|
||||||
|
|
||||||
|
# der Fels darunter -- klein, die Insel soll schweben, nicht stehen
|
||||||
|
Prims.cyl(self, Vector3(0, -12.0, 0), radius * 0.52, 5.0, Color(0.17, 0.13, 0.28), false)
|
||||||
|
Prims.cyl(self, Vector3(0, -17.0, 0), radius * 0.22, 6.0, Color(0.12, 0.09, 0.21), false)
|
||||||
|
|
||||||
|
# treibende Lichter statt schwebender Würfel
|
||||||
var rng := RandomNumberGenerator.new()
|
var rng := RandomNumberGenerator.new()
|
||||||
rng.seed = 7
|
rng.seed = 7
|
||||||
for i in range(34):
|
for i in range(44):
|
||||||
var a := rng.randf() * TAU
|
var a := rng.randf() * TAU
|
||||||
var r := rng.randf_range(18.0, maxf(24.0, Game.insel_radius() - 4.0))
|
var r := rng.randf_range(10.0, maxf(24.0, radius - 4.0))
|
||||||
var p := Vector3(cos(a) * r, rng.randf_range(5.0, 24.0), sin(a) * r)
|
var p := Vector3(cos(a) * r, 0, sin(a) * r)
|
||||||
var s := rng.randf_range(0.6, 2.0)
|
p.y = DreamDB.hoehe_bei(p.x, p.z) + rng.randf_range(2.5, 9.0)
|
||||||
var c := Color(0.6 + rng.randf() * 0.3, 0.5 + rng.randf() * 0.4, 1.0)
|
var c := Color(0.62 + rng.randf() * 0.3, 0.7 + rng.randf() * 0.25, 1.0)
|
||||||
var wuerfel := Prims.box(self, p, Vector3(s, s, s), c, false, 1.1)
|
var licht := Prims.sphere(self, p, rng.randf_range(0.14, 0.34), c, 2.4)
|
||||||
wuerfel.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
licht.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
_deko.append(wuerfel)
|
_lichter.append(licht)
|
||||||
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
||||||
|
|
||||||
|
## Die gewachsenen Wege folgen dem Hang: jedes Band von Knoten zu Knoten ist
|
||||||
|
## geneigt. An jedem zweiten Knoten eine Laterne.
|
||||||
func _wege() -> void:
|
func _wege() -> void:
|
||||||
var farbe := Color(0.66, 0.58, 0.84)
|
var farbe := Color(0.70, 0.62, 0.86)
|
||||||
for k in DreamDB.NETZ.kanten:
|
for k in DreamDB.NETZ.kanten:
|
||||||
var a: Vector3 = DreamDB.NETZ.knoten[k[0]]
|
var a: Vector3 = DreamDB.NETZ.knoten[k[0]]
|
||||||
var b: Vector3 = DreamDB.NETZ.knoten[k[1]]
|
var b: Vector3 = DreamDB.NETZ.knoten[k[1]]
|
||||||
|
a.y = DreamDB.hoehe_bei(a.x, a.z) + 0.04
|
||||||
|
b.y = DreamDB.hoehe_bei(b.x, b.z) + 0.04
|
||||||
var d := b - a
|
var d := b - a
|
||||||
if d.length() < 0.2:
|
if d.length() < 0.2:
|
||||||
continue
|
continue
|
||||||
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, 0.03, 0),
|
var band := Prims.box(self, (a + b) * 0.5, Vector3(2.6, 0.06, d.length()), farbe, false)
|
||||||
Vector3(2.6, 0.06, d.length()), farbe, false)
|
band.basis = Basis.looking_at(d.normalized(), Vector3.UP)
|
||||||
band.rotation.y = atan2(d.x, d.z)
|
var nr := 0
|
||||||
for k in DreamDB.NETZ.knoten:
|
for k in DreamDB.NETZ.knoten:
|
||||||
Prims.cyl(self, k + Vector3(0, 0.035, 0), 1.3, 0.06, farbe, false)
|
var p: Vector3 = k
|
||||||
|
p.y = DreamDB.hoehe_bei(p.x, p.z)
|
||||||
|
Prims.cyl(self, p + Vector3(0, 0.04, 0), 1.3, 0.06, farbe, false)
|
||||||
|
if nr % 2 == 0 and Vector2(p.x, p.z).length() > 12.0:
|
||||||
|
_laterne(p + Vector3(1.6, 0, 0))
|
||||||
|
nr += 1
|
||||||
|
|
||||||
|
|
||||||
|
## Eine Laterne: Pfahl und leuchtende Kugel. Kein eigenes Licht -- der Glow
|
||||||
|
## trägt sie, und so dürfen es viele sein.
|
||||||
|
func _laterne(p: Vector3) -> void:
|
||||||
|
Prims.cyl(self, p + Vector3(0, 1.1, 0), 0.09, 2.2, Color(0.36, 0.30, 0.48), false)
|
||||||
|
var k := Prims.sphere(self, p + Vector3(0, 2.35, 0), 0.24, Color(1.0, 0.86, 0.62), 2.6)
|
||||||
|
k.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
|
||||||
|
|
||||||
|
## Jedes Gebäude steht auf seiner Terrasse: Wände, Dach, Laterne an der Tür,
|
||||||
|
## Licht innen. Das Schild darüber.
|
||||||
func _haeuser() -> void:
|
func _haeuser() -> void:
|
||||||
for key in DreamDB.PLACES:
|
for key in DreamDB.PLACES:
|
||||||
var pl: Dictionary = DreamDB.PLACES[key]
|
var pl: Dictionary = DreamDB.PLACES[key]
|
||||||
@@ -169,26 +205,29 @@ func _haeuser() -> void:
|
|||||||
if s[0] <= 0.0 or not Game.ort_offen(key):
|
if s[0] <= 0.0 or not Game.ort_offen(key):
|
||||||
continue
|
continue
|
||||||
var c := Orte.center(DreamDB.PLACES, key)
|
var c := Orte.center(DreamDB.PLACES, key)
|
||||||
|
c.y = DreamDB.terrasse(key)
|
||||||
var groesse := Vector3(s[0], 3.2, s[1])
|
var groesse := Vector3(s[0], 3.2, s[1])
|
||||||
var farbe: Color = pl["color"]
|
var farbe: Color = pl["color"]
|
||||||
Prims.room(self, c, groesse, farbe, farbe.darkened(0.35),
|
var waende: Array = Prims.room(self, c, groesse, farbe, farbe.darkened(0.35),
|
||||||
Orte.DIR_VEC[pl["dir"]], 3.4, true)
|
Orte.DIR_VEC[pl["dir"]], 3.4, true)
|
||||||
Prims.label(self, c + Vector3(0, 5.0, 0), pl["name"], 0.6, Color(1, 0.95, 0.85))
|
var dach := Prims.dach(self, c, groesse, farbe)
|
||||||
|
_bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": groesse.y + 3.0,
|
||||||
|
"etagen": [], "dach": [dach], "waende": [waende]})
|
||||||
|
Prims.label(self, c + Vector3(0, groesse.y + 4.2, 0), pl["name"], 0.6, Color(1, 0.95, 0.85))
|
||||||
_einrichtung(c, groesse, farbe, key)
|
_einrichtung(c, groesse, farbe, key)
|
||||||
|
|
||||||
var licht := OmniLight3D.new()
|
var licht := OmniLight3D.new()
|
||||||
licht.position = c + Vector3(0, 2.4, 0)
|
licht.position = c + Vector3(0, 2.4, 0)
|
||||||
licht.omni_range = 18.0
|
licht.omni_range = 16.0
|
||||||
licht.light_energy = 2.2
|
licht.light_energy = 1.8
|
||||||
licht.light_color = farbe.lightened(0.5)
|
licht.light_color = farbe.lightened(0.5)
|
||||||
add_child(licht)
|
add_child(licht)
|
||||||
|
|
||||||
# Weg von der Tür zum Ring
|
|
||||||
var tuer := Orte.door(DreamDB.PLACES, key)
|
var tuer := Orte.door(DreamDB.PLACES, key)
|
||||||
var weg := tuer.normalized() * 15.0 - tuer
|
tuer.y = c.y
|
||||||
var b := Prims.box(self, (tuer + tuer.normalized() * 15.0) * 0.5 + Vector3(0, 0.03, 0),
|
var seite: Vector3 = Orte.DIR_VEC[pl["dir"]]
|
||||||
Vector3(2.4, 0.06, weg.length()), Color(0.66, 0.58, 0.84), false)
|
var quer := Vector3(-seite.z, 0, seite.x)
|
||||||
b.rotation.y = atan2(weg.x, weg.z)
|
_laterne(tuer - seite * 1.2 + quer * 2.4)
|
||||||
|
|
||||||
|
|
||||||
func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
|
func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
|
||||||
@@ -250,6 +289,7 @@ func _lehrpulte() -> void:
|
|||||||
var tief: float = float(pl["size"][1])
|
var tief: float = float(pl["size"][1])
|
||||||
var innen: Vector3 = Orte.DIR_VEC[pl["dir"]] * -1.0
|
var innen: Vector3 = Orte.DIR_VEC[pl["dir"]] * -1.0
|
||||||
var c := Orte.center(DreamDB.PLACES, key)
|
var c := Orte.center(DreamDB.PLACES, key)
|
||||||
|
c.y = DreamDB.terrasse(key)
|
||||||
var pos := c + innen * (tief * 0.5 - 2.6)
|
var pos := c + innen * (tief * 0.5 - 2.6)
|
||||||
var farbe: Color = pl["color"]
|
var farbe: Color = pl["color"]
|
||||||
Prims.box(self, pos + Vector3(0, 0.55, 0), Vector3(2.4, 1.1, 0.9), farbe.darkened(0.35))
|
Prims.box(self, pos + Vector3(0, 0.55, 0), Vector3(2.4, 1.1, 0.9), farbe.darkened(0.35))
|
||||||
@@ -291,23 +331,48 @@ func _marke(pos: Vector3, kind: String, titel: String) -> Node3D:
|
|||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
## Der Dorfplatz: der Traumbaum in der Mitte, Bänke darum, Laternen am Rand.
|
||||||
func _lichtung() -> void:
|
func _lichtung() -> void:
|
||||||
var c := Orte.center(DreamDB.PLACES, "lichtung")
|
var c := Orte.center(DreamDB.PLACES, "lichtung")
|
||||||
Prims.cyl(self, c + Vector3(0, 0.04, 0), 11.0, 0.12, Color(0.60, 0.52, 0.78), false)
|
Prims.cyl(self, c + Vector3(0, 0.04, 0), 11.0, 0.12, Color(0.60, 0.52, 0.78), false)
|
||||||
Prims.cyl(self, c + Vector3(0, 0.14, 0), 3.2, 0.2, Color(0.55, 0.80, 1.0, 0.5), false, 1.2)
|
Prims.cyl(self, c + Vector3(0, 0.14, 0), 3.4, 0.2, Color(0.30, 0.24, 0.40), false)
|
||||||
for i in range(8):
|
|
||||||
var a := TAU * float(i) / 8.0
|
# der Traumbaum
|
||||||
var p := c + Vector3(cos(a) * 8.5, 0, sin(a) * 8.5)
|
Prims.cyl(self, c + Vector3(0, 4.5, 0), 1.1, 9.0, Color(0.36, 0.26, 0.40))
|
||||||
Prims.cyl(self, p + Vector3(0, 1.6, 0), 0.28, 3.2, Color(0.72, 0.64, 0.88))
|
var kronen := [
|
||||||
Prims.sphere(self, p + Vector3(0, 3.6, 0), 0.42, Color(0.75, 0.85, 1.0), 2.0)
|
[Vector3(0, 9.6, 0), 3.6, Color(0.62, 0.55, 0.96)],
|
||||||
Prims.label(self, c + Vector3(0, 5.2, 0), str(DreamDB.PLACES["lichtung"]["name"]), 0.6,
|
[Vector3(1.6, 11.4, -0.8), 2.7, Color(0.70, 0.62, 1.0)],
|
||||||
|
[Vector3(-1.4, 12.2, 1.0), 2.4, Color(0.55, 0.78, 1.0)],
|
||||||
|
[Vector3(0.3, 14.0, 0.2), 1.7, Color(0.80, 0.90, 1.0)],
|
||||||
|
]
|
||||||
|
for k in kronen:
|
||||||
|
var s := Prims.sphere(self, c + k[0], k[1], k[2], 1.3)
|
||||||
|
s.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
var licht := OmniLight3D.new()
|
||||||
|
licht.position = c + Vector3(0, 11.0, 0)
|
||||||
|
licht.omni_range = 46.0
|
||||||
|
licht.light_energy = 3.2
|
||||||
|
licht.light_color = Color(0.72, 0.78, 1.0)
|
||||||
|
add_child(licht)
|
||||||
|
|
||||||
|
for i in range(4):
|
||||||
|
var a := TAU * float(i) / 4.0 + 0.4
|
||||||
|
var p := c + Vector3(cos(a) * 5.6, 0.42, sin(a) * 5.6)
|
||||||
|
var bank := Prims.box(self, p, Vector3(2.6, 0.16, 0.7), Color(0.46, 0.38, 0.58))
|
||||||
|
bank.rotation.y = -a
|
||||||
|
Prims.box(self, p + Vector3(0, -0.2, 0), Vector3(0.4, 0.3, 0.6), Color(0.36, 0.30, 0.48), false)
|
||||||
|
for i in range(4):
|
||||||
|
var a := TAU * float(i) / 4.0
|
||||||
|
_laterne(c + Vector3(cos(a) * 8.8, 0, sin(a) * 8.8))
|
||||||
|
Prims.label(self, c + Vector3(0, 16.0, 0), str(DreamDB.PLACES["lichtung"]["name"]), 0.6,
|
||||||
Color(1, 0.95, 0.85))
|
Color(1, 0.95, 0.85))
|
||||||
|
|
||||||
|
|
||||||
func _tor_zur_erde() -> void:
|
func _tor_zur_erde() -> void:
|
||||||
var pos := Orte.center(DreamDB.PLACES, "tor")
|
var pos := Orte.center(DreamDB.PLACES, "tor")
|
||||||
# flach und ohne Kollision wie die Lichtung — eine 0,6 hohe Stufe hätte den
|
pos.y = DreamDB.hoehe_bei(pos.x, pos.z)
|
||||||
# Spieler vor dem Tor stehen lassen
|
# flach und ohne Kollision wie der Platz — eine Stufe hätte den Spieler
|
||||||
|
# vor dem Tor stehen lassen
|
||||||
Prims.cyl(self, pos + Vector3(0, 0.05, 0), 5.5, 0.1, Color(0.35, 0.30, 0.50), false)
|
Prims.cyl(self, pos + Vector3(0, 0.05, 0), 5.5, 0.1, Color(0.35, 0.30, 0.50), false)
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
var a := PI * float(i) / 9.0
|
var a := PI * float(i) / 9.0
|
||||||
@@ -317,24 +382,96 @@ func _tor_zur_erde() -> void:
|
|||||||
Color(0.45, 0.75, 1.0, 0.28), false, 1.0)
|
Color(0.45, 0.75, 1.0, 0.28), false, 1.0)
|
||||||
scheibe.rotation.x = PI * 0.5
|
scheibe.rotation.x = PI * 0.5
|
||||||
Prims.label(self, pos + Vector3(0, 6.4, 0), "Tor zur Erde", 0.8, Color(0.7, 0.95, 1))
|
Prims.label(self, pos + Vector3(0, 6.4, 0), "Tor zur Erde", 0.8, Color(0.7, 0.95, 1))
|
||||||
|
|
||||||
_marke(pos + Vector3(0, 1.6, 0), "erde", "Tor zur Erde")
|
_marke(pos + Vector3(0, 1.6, 0), "erde", "Tor zur Erde")
|
||||||
|
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
p.y = DreamDB.hoehe_bei(p.x, p.z)
|
||||||
|
_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)
|
||||||
|
|
||||||
|
|
||||||
## Am Anfang ist das Dorf leer; mit jedem erfüllten Traum zieht ein Wesen ein.
|
## Am Anfang ist das Dorf leer; mit jedem erfüllten Traum zieht ein Wesen ein.
|
||||||
|
## Jedes läuft auf dem Hügel -- der Boden sagt ihm, wie hoch.
|
||||||
func _wesen() -> void:
|
func _wesen() -> void:
|
||||||
for i in range(DreamDB.LIST.size()):
|
for i in range(DreamDB.LIST.size()):
|
||||||
if not Game.wesen_eingezogen(i):
|
if not Game.wesen_eingezogen(i):
|
||||||
continue
|
continue
|
||||||
var w := Bewohner.new()
|
var w := Bewohner.new()
|
||||||
add_child(w)
|
add_child(w)
|
||||||
|
w.boden = Callable(DreamDB, "hoehe_bei")
|
||||||
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES, DreamDB.NETZ, DreamDB.ANKER)
|
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES, DreamDB.NETZ, DreamDB.ANKER)
|
||||||
bewohner.append(w)
|
bewohner.append(w)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
for i in range(_deko.size()):
|
if spieler != null:
|
||||||
var n: Node3D = _deko[i]
|
Prims.schnitt(_bauten, spieler.global_position, spieler.cam.global_position)
|
||||||
n.rotation.y += delta * (0.2 + 0.05 * (i % 5))
|
var t := Time.get_ticks_msec() * 0.001
|
||||||
n.rotation.x += delta * 0.13
|
for i in range(_lichter.size()):
|
||||||
n.position.y += sin(Time.get_ticks_msec() * 0.0007 + i) * delta * 0.6
|
var n: Node3D = _lichter[i]
|
||||||
|
n.position += Vector3(sin(t * 0.5 + i) * 0.35, cos(t * 0.7 + i * 0.3) * 0.25,
|
||||||
|
cos(t * 0.4 + i * 0.5) * 0.35) * delta
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ extends Node3D
|
|||||||
class_name EarthWorld
|
class_name EarthWorld
|
||||||
## Die Erde: eine Kleinstadt mit vier Menschen, die ihren Tag leben.
|
## Die Erde: eine Kleinstadt mit vier Menschen, die ihren Tag leben.
|
||||||
|
|
||||||
var spawn := Vector3(0, 3.0, 12.0)
|
var spawn := Vector3(0, 1.4, 12.0)
|
||||||
var humans: Array = []
|
var humans: Array = []
|
||||||
|
var spieler: Node3D = null # setzt main, für den Etagenschnitt
|
||||||
|
var _bauten: Array = [] # alles, was der Etagenschnitt aufschneidet
|
||||||
|
|
||||||
var _sun: DirectionalLight3D
|
var _sun: DirectionalLight3D
|
||||||
var _sky: ProceduralSkyMaterial
|
var _sky: ProceduralSkyMaterial
|
||||||
@@ -64,8 +66,8 @@ func _strassen() -> void:
|
|||||||
band.rotation.y = atan2(d.x, d.z)
|
band.rotation.y = atan2(d.x, d.z)
|
||||||
|
|
||||||
|
|
||||||
## Alles, was nicht Wohnung oder Sonderbau ist, steht als geschlossener Bau da.
|
## Alles, was nicht Wohnung oder Sonderbau ist, steht als geschlossener Klotz
|
||||||
## Erst dadurch wird der Bezirk dicht.
|
## mit ein bis vier Stockwerken da. Erst dadurch wird der Bezirk dicht.
|
||||||
func _bebauung() -> void:
|
func _bebauung() -> void:
|
||||||
var rng := RandomNumberGenerator.new()
|
var rng := RandomNumberGenerator.new()
|
||||||
rng.seed = 8123
|
rng.seed = 8123
|
||||||
@@ -81,18 +83,26 @@ func _bebauung() -> void:
|
|||||||
var g: Vector2 = p["size"]
|
var g: Vector2 = p["size"]
|
||||||
if minf(g.x, g.y) < 3.0:
|
if minf(g.x, g.y) < 3.0:
|
||||||
continue
|
continue
|
||||||
var hoehe := rng.randf_range(4.0, 9.0)
|
var n := 1 + rng.randi_range(0, 3)
|
||||||
|
var hoehe := Orte.hoehe(n)
|
||||||
var ton := Color.from_hsv(fmod(0.07 + rng.randf() * 0.12, 1.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)
|
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)
|
var bau := Node3D.new()
|
||||||
Prims.box(self, c + Vector3(0, hoehe + 0.2, 0),
|
add_child(bau)
|
||||||
|
Prims.box(bau, c + Vector3(0, hoehe * 0.5, 0), Vector3(g.x, hoehe, g.y), ton)
|
||||||
|
for f in range(n):
|
||||||
|
_fenster(bau, c + Vector3(0, Orte.hoehe(f), 0), g, ton)
|
||||||
|
Prims.box(bau, c + Vector3(0, hoehe + 0.2, 0),
|
||||||
Vector3(g.x + 0.5, 0.4, g.y + 0.5), ton.darkened(0.25))
|
Vector3(g.x + 0.5, 0.4, g.y + 0.5), ton.darkened(0.25))
|
||||||
if rng.randf() < 0.35:
|
if rng.randf() < 0.35:
|
||||||
Prims.box(self, c + Vector3(0, hoehe + 0.9, 0),
|
Prims.box(bau, c + Vector3(0, hoehe + 0.9, 0),
|
||||||
Vector3(g.x * 0.4, 1.0, g.y * 0.4), ton.darkened(0.1))
|
Vector3(g.x * 0.4, 1.0, g.y * 0.4), ton.darkened(0.1))
|
||||||
|
_bauten.append({"c": c, "size": g, "hoch": hoehe, "etagen": [bau], "dach": [],
|
||||||
|
"waende": [], "voll": true})
|
||||||
|
|
||||||
|
|
||||||
## Wohnungen und Sonderbauten stehen offen, damit der Geist von oben hineinsieht.
|
## Wohnungen und Sonderbauten: geschlossene Häuser mit Stockwerken, Treppenhaus
|
||||||
|
## und Möbeln. Der Geist gleitet hinein, der Etagenschnitt öffnet sie ihm.
|
||||||
func _orte() -> void:
|
func _orte() -> void:
|
||||||
for key in HumanDB.PLACES:
|
for key in HumanDB.PLACES:
|
||||||
var pl: Dictionary = HumanDB.PLACES[key]
|
var pl: Dictionary = HumanDB.PLACES[key]
|
||||||
@@ -100,57 +110,100 @@ func _orte() -> void:
|
|||||||
if float(s[0]) <= 0.0:
|
if float(s[0]) <= 0.0:
|
||||||
continue
|
continue
|
||||||
var c := Orte.center(HumanDB.PLACES, key)
|
var c := Orte.center(HumanDB.PLACES, key)
|
||||||
var groesse := Vector3(float(s[0]), 2.3, float(s[1]))
|
var groesse := Vector3(float(s[0]), Orte.ETAGE, float(s[1]))
|
||||||
var farbe: Color = pl["color"]
|
var farbe: Color = pl["color"]
|
||||||
var tuer: float = minf(3.0, minf(groesse.x, groesse.z) - 3.0)
|
var tuer: float = maxf(1.6, minf(3.0, minf(groesse.x, groesse.z) - 3.0))
|
||||||
Prims.room(self, c, groesse, farbe, farbe.darkened(0.35),
|
var n := Orte.etagen(HumanDB.PLACES, key)
|
||||||
Orte.DIR_VEC[pl["dir"]], maxf(1.6, tuer), true)
|
var etagen: Array = []
|
||||||
Prims.label(self, c + Vector3(0, 4.4, 0), str(pl["name"]), 0.55, Color(1, 1, 0.9))
|
var waende: Array = []
|
||||||
_einrichtung(c, groesse, farbe, key)
|
for f in range(n):
|
||||||
|
var eb := Node3D.new()
|
||||||
|
add_child(eb)
|
||||||
|
var cf := c + Vector3(0, Orte.hoehe(f), 0)
|
||||||
|
var ton := farbe.lightened(0.06 * float(f))
|
||||||
|
waende.append(Prims.room(eb, cf, groesse, ton, farbe.darkened(0.35),
|
||||||
|
Orte.DIR_VEC[pl["dir"]] if f == 0 else Vector3.ZERO, tuer, true))
|
||||||
|
_fenster(eb, cf, Vector2(groesse.x, groesse.z), ton)
|
||||||
|
_einrichtung(eb, cf, groesse, farbe, key if f == 0 or HumanDB.ETAGEN_ARBEIT.has(key)
|
||||||
|
else "heim_")
|
||||||
|
if f + 1 < n:
|
||||||
|
_treppe(eb, key, f)
|
||||||
var licht := OmniLight3D.new()
|
var licht := OmniLight3D.new()
|
||||||
licht.position = c + Vector3(0, 2.0, 0)
|
licht.position = cf + Vector3(0, 2.2, 0)
|
||||||
licht.omni_range = 16.0
|
licht.omni_range = 12.0
|
||||||
licht.light_color = Color(1.0, 0.85, 0.6)
|
licht.light_color = Color(1.0, 0.85, 0.6)
|
||||||
licht.light_energy = 0.0
|
licht.light_energy = 0.0
|
||||||
add_child(licht)
|
eb.add_child(licht)
|
||||||
_nachtlichter.append(licht)
|
_nachtlichter.append(licht)
|
||||||
|
etagen.append(eb)
|
||||||
|
var dach := Node3D.new()
|
||||||
|
add_child(dach)
|
||||||
|
Prims.box(dach, c + Vector3(0, Orte.hoehe(n) + 0.1, 0),
|
||||||
|
Vector3(groesse.x + 0.4, 0.2, groesse.z + 0.4), farbe.darkened(0.3))
|
||||||
|
Prims.label(self, c + Vector3(0, Orte.hoehe(n) + 1.6, 0), str(pl["name"]), 0.55,
|
||||||
|
Color(1, 1, 0.9))
|
||||||
|
_bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": Orte.hoehe(n),
|
||||||
|
"etagen": etagen, "dach": [dach], "waende": waende})
|
||||||
|
|
||||||
|
|
||||||
|
## Ein Fensterband je Wand, damit man die Stockwerke von außen zählt.
|
||||||
|
func _fenster(parent: Node, cf: Vector3, g: Vector2, ton: Color) -> void:
|
||||||
|
var glas := ton.darkened(0.55).lerp(Color(0.35, 0.45, 0.6), 0.5)
|
||||||
|
for d: Vector3 in [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]:
|
||||||
|
var quer := absf(d.z) > 0.0
|
||||||
|
var laenge: float = (g.x if quer else g.y) - 2.4
|
||||||
|
if laenge < 1.0:
|
||||||
|
continue
|
||||||
|
var off := Vector3(d.x * (g.x * 0.5 + 0.05), 1.75, d.z * (g.y * 0.5 + 0.05))
|
||||||
|
Prims.box(parent, cf + off, Vector3(laenge, 0.9, 0.08) if quer else Vector3(0.08, 0.9, laenge),
|
||||||
|
glas, false)
|
||||||
|
|
||||||
|
|
||||||
|
## Eine Rampe von Podest zu Podest -- die Treppe, die die Bewohner hochgehen.
|
||||||
|
func _treppe(parent: Node, key: String, f: int) -> void:
|
||||||
|
var a := Orte.treppe(HumanDB.PLACES, key, f)
|
||||||
|
var b := Orte.treppe(HumanDB.PLACES, key, f + 1)
|
||||||
|
var d := b - a
|
||||||
|
var stufe := Prims.box(parent, (a + b) * 0.5, Vector3(1.2, 0.18, d.length()),
|
||||||
|
Color(0.55, 0.50, 0.46), false)
|
||||||
|
stufe.basis = Basis.looking_at(d.normalized(), Vector3.UP)
|
||||||
|
Prims.box(parent, b + Vector3(0, -0.09, 0), Vector3(1.4, 0.18, 1.4), Color(0.55, 0.50, 0.46), false)
|
||||||
|
|
||||||
|
|
||||||
## Ein paar Möbel, damit man sieht, wofür ein Raum da ist.
|
## Ein paar Möbel, damit man sieht, wofür ein Raum da ist.
|
||||||
func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
|
func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
|
||||||
var w := maxf(0.5, groesse.x * 0.5 - 2.0)
|
var w := maxf(0.5, groesse.x * 0.5 - 2.0)
|
||||||
var h := maxf(0.5, groesse.z * 0.5 - 2.0)
|
var h := maxf(0.5, groesse.z * 0.5 - 2.0)
|
||||||
if key.begins_with("heim_"):
|
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),
|
Prims.box(parent, c + Vector3(-w * 0.6, 0.35, -h * 0.5), Vector3(1.9, 0.7, 3.0),
|
||||||
Color(0.85, 0.85, 0.90))
|
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),
|
Prims.box(parent, c + Vector3(w * 0.6, 0.75, 0), Vector3(1.5, 0.15, 2.2),
|
||||||
farbe.darkened(0.5))
|
farbe.darkened(0.5))
|
||||||
Prims.box(self, c + Vector3(w * 0.6, 1.05, 0), Vector3(0.8, 0.55, 0.1),
|
Prims.box(parent, c + Vector3(w * 0.6, 1.05, 0), Vector3(0.8, 0.55, 0.1),
|
||||||
Color(0.2, 0.25, 0.3))
|
Color(0.2, 0.25, 0.3))
|
||||||
elif key == "cafe":
|
elif key == "cafe":
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
Prims.cyl(self, c + Vector3(-w * 0.5 + float(i) * w * 0.5, 0.4, 0.4), 0.7, 0.8,
|
Prims.cyl(parent, 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))
|
Color(0.55, 0.40, 0.30))
|
||||||
Prims.box(self, c + Vector3(0, 0.55, -h * 0.7), Vector3(groesse.x - 3.0, 1.1, 0.8),
|
Prims.box(parent, c + Vector3(0, 0.55, -h * 0.7), Vector3(groesse.x - 3.0, 1.1, 0.8),
|
||||||
Color(0.45, 0.32, 0.24))
|
Color(0.45, 0.32, 0.24))
|
||||||
elif key == "buero":
|
elif key == "buero":
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
Prims.box(self, c + Vector3(-w * 0.5 + float(i) * w * 0.5, 0.75, 0),
|
Prims.box(parent, 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))
|
Vector3(1.6, 0.15, 1.2), Color(0.75, 0.75, 0.78))
|
||||||
elif key == "baeckerei":
|
elif key == "baeckerei":
|
||||||
Prims.box(self, c + Vector3(0, 0.55, h * 0.5), Vector3(groesse.x - 3.0, 1.1, 1.0),
|
Prims.box(parent, c + Vector3(0, 0.55, h * 0.5), Vector3(groesse.x - 3.0, 1.1, 1.0),
|
||||||
Color(0.70, 0.55, 0.35))
|
Color(0.70, 0.55, 0.35))
|
||||||
Prims.box(self, c + Vector3(0, 0.85, -h * 0.6), Vector3(2.6, 1.7, 1.2),
|
Prims.box(parent, c + Vector3(0, 0.85, -h * 0.6), Vector3(2.6, 1.7, 1.2),
|
||||||
Color(0.4, 0.4, 0.42))
|
Color(0.4, 0.4, 0.42))
|
||||||
elif key == "uni":
|
elif key == "uni":
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
Prims.box(self, c + Vector3(0, 0.5, -h * 0.6 + float(i) * h * 0.6),
|
Prims.box(parent, 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))
|
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),
|
Prims.box(parent, c + Vector3(0, 1.1, -h), Vector3(groesse.x * 0.55, 1.6, 0.15),
|
||||||
Color(0.15, 0.25, 0.20))
|
Color(0.15, 0.25, 0.20))
|
||||||
elif key == "werkstatt":
|
elif key == "werkstatt":
|
||||||
Prims.box(self, c + Vector3(0, 0.6, 0), Vector3(groesse.x - 4.0, 1.2, 1.4),
|
Prims.box(parent, c + Vector3(0, 0.6, 0), Vector3(groesse.x - 4.0, 1.2, 1.4),
|
||||||
Color(0.52, 0.48, 0.46))
|
Color(0.52, 0.48, 0.46))
|
||||||
|
|
||||||
|
|
||||||
@@ -209,6 +262,8 @@ func _menschen() -> void:
|
|||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
_himmel_aktualisieren()
|
_himmel_aktualisieren()
|
||||||
|
if spieler != null:
|
||||||
|
Prims.schnitt(_bauten, spieler.global_position, spieler.cam.global_position)
|
||||||
|
|
||||||
|
|
||||||
func _himmel_aktualisieren() -> void:
|
func _himmel_aktualisieren() -> void:
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func _setup_input() -> void:
|
|||||||
"folgen": [KEY_F],
|
"folgen": [KEY_F],
|
||||||
"notieren": [KEY_R],
|
"notieren": [KEY_R],
|
||||||
"journal": [KEY_J, KEY_TAB],
|
"journal": [KEY_J, KEY_TAB],
|
||||||
|
"drehen": [KEY_Q],
|
||||||
"tempo_1": [KEY_1, KEY_KP_1],
|
"tempo_1": [KEY_1, KEY_KP_1],
|
||||||
"tempo_2": [KEY_2, KEY_KP_2],
|
"tempo_2": [KEY_2, KEY_KP_2],
|
||||||
"tempo_3": [KEY_3, KEY_KP_3],
|
"tempo_3": [KEY_3, KEY_KP_3],
|
||||||
|
|||||||
@@ -26,12 +26,15 @@ static var ANKER: Dictionary = {}
|
|||||||
|
|
||||||
## Die fünf Sonderbauten. Sie bekommen eine Parzelle an einer Arterie.
|
## Die fünf Sonderbauten. Sie bekommen eine Parzelle an einer Arterie.
|
||||||
static var SONDER: Array = [
|
static var SONDER: Array = [
|
||||||
["uni", "Hochschule", Color(0.72, 0.70, 0.66)],
|
["uni", "Hochschule", Color(0.72, 0.70, 0.66), 3],
|
||||||
["cafe", "Café Nadel", Color(0.82, 0.62, 0.45)],
|
["cafe", "Café Nadel", Color(0.82, 0.62, 0.45), 2],
|
||||||
["buero", "Büro Ostwind", Color(0.62, 0.66, 0.72)],
|
["buero", "Büro Ostwind", Color(0.62, 0.66, 0.72), 4],
|
||||||
["baeckerei", "Bäckerei Keller", Color(0.85, 0.72, 0.50)],
|
["baeckerei", "Bäckerei Keller", Color(0.85, 0.72, 0.50), 2],
|
||||||
["werkstatt", "Werkstatt Loos", Color(0.70, 0.58, 0.55)],
|
["werkstatt", "Werkstatt Loos", Color(0.70, 0.58, 0.55), 1],
|
||||||
]
|
]
|
||||||
|
## Arbeitsorte, an denen die Leute auf verschiedenen Etagen sitzen. Café,
|
||||||
|
## Bäckerei und Werkstatt spielen sich unten ab.
|
||||||
|
static var ETAGEN_ARBEIT: Array = ["buero", "uni"]
|
||||||
|
|
||||||
static var PLACES: Dictionary = {}
|
static var PLACES: Dictionary = {}
|
||||||
static var LIST: Array = []
|
static var LIST: Array = []
|
||||||
@@ -408,6 +411,7 @@ static func _sonderbauten_verteilen() -> void:
|
|||||||
"size": [p["size"].x, p["size"].y],
|
"size": [p["size"].x, p["size"].y],
|
||||||
"dir": p["dir"],
|
"dir": p["dir"],
|
||||||
"color": SONDER[s][2],
|
"color": SONDER[s][2],
|
||||||
|
"etagen": int(SONDER[s][3]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -436,9 +440,21 @@ static func _wohnhaeuser_verteilen() -> void:
|
|||||||
"size": [p["size"].x, p["size"].y],
|
"size": [p["size"].x, p["size"].y],
|
||||||
"dir": p["dir"],
|
"dir": p["dir"],
|
||||||
"color": Color.from_hsv(fmod(0.08 + float(i) * 0.113, 1.0), 0.28, 0.78),
|
"color": Color.from_hsv(fmod(0.08 + float(i) * 0.113, 1.0), 0.28, 0.78),
|
||||||
|
"etagen": 2 + (i * 5 + 1) % 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## In welcher Etage jemand wohnt und arbeitet. Wohnhäuser sind Mehrfamilien-
|
||||||
|
## häuser: die eigene Wohnung liegt irgendwo im Haus.
|
||||||
|
static func etagen_fuer(i: int, heim: String, arbeit: String) -> Dictionary:
|
||||||
|
var out: Dictionary = {}
|
||||||
|
if PLACES.has(heim):
|
||||||
|
out[heim] = (i * 7 + 3) % Orte.etagen(PLACES, heim)
|
||||||
|
if PLACES.has(arbeit) and ETAGEN_ARBEIT.has(arbeit):
|
||||||
|
out[arbeit] = i % Orte.etagen(PLACES, arbeit)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
static func _anker_setzen() -> void:
|
static func _anker_setzen() -> void:
|
||||||
for key in PLACES:
|
for key in PLACES:
|
||||||
var s: Array = PLACES[key]["size"]
|
var s: Array = PLACES[key]["size"]
|
||||||
@@ -572,6 +588,7 @@ static func mensch_aus_profil(pr: Dictionary, i: int) -> Dictionary:
|
|||||||
"eigen": true,
|
"eigen": true,
|
||||||
"mission": true,
|
"mission": true,
|
||||||
"heim": heim,
|
"heim": heim,
|
||||||
|
"etage": etagen_fuer(i, heim, arbeit),
|
||||||
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
|
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
|
||||||
"routine": _tag_belegen(TAGESTYPEN[typ], heim, arbeit, bild["blocker"],
|
"routine": _tag_belegen(TAGESTYPEN[typ], heim, arbeit, bild["blocker"],
|
||||||
bild["ursachen"], i),
|
bild["ursachen"], i),
|
||||||
@@ -614,6 +631,7 @@ static func _mensch(i: int) -> Dictionary:
|
|||||||
"verhalten": {},
|
"verhalten": {},
|
||||||
"mission": true,
|
"mission": true,
|
||||||
"heim": heim,
|
"heim": heim,
|
||||||
|
"etage": etagen_fuer(i, heim, arbeit),
|
||||||
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
|
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
|
||||||
"routine": _tagesablauf(i, heim, arbeit),
|
"routine": _tagesablauf(i, heim, arbeit),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ func _ready() -> void:
|
|||||||
ui = UI.new()
|
ui = UI.new()
|
||||||
add_child(ui)
|
add_child(ui)
|
||||||
ui.panel_opened.connect(func() -> void:
|
ui.panel_opened.connect(func() -> void:
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
|
||||||
player.look_enabled = false)
|
player.look_enabled = false)
|
||||||
ui.panel_closed.connect(func() -> void:
|
ui.panel_closed.connect(func() -> void:
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
|
||||||
player.look_enabled = true)
|
player.look_enabled = true)
|
||||||
|
|
||||||
player = Player.new()
|
player = Player.new()
|
||||||
@@ -31,7 +29,7 @@ func _ready() -> void:
|
|||||||
Color(0.8, 0.95, 1)))
|
Color(0.8, 0.95, 1)))
|
||||||
|
|
||||||
_traumwelt()
|
_traumwelt()
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
|
||||||
func _traumwelt() -> void:
|
func _traumwelt() -> void:
|
||||||
@@ -39,8 +37,7 @@ func _traumwelt() -> void:
|
|||||||
_setze_welt(DreamWorld.new())
|
_setze_welt(DreamWorld.new())
|
||||||
player.set_mode(Player.Mode.TRAUM)
|
player.set_mode(Player.Mode.TRAUM)
|
||||||
player.global_position = (welt as DreamWorld).spawn
|
player.global_position = (welt as DreamWorld).spawn
|
||||||
player.yaw = 0.0
|
player.yaw = Player.CAM_YAW
|
||||||
player.pitch = -0.18
|
|
||||||
|
|
||||||
|
|
||||||
func _erde() -> void:
|
func _erde() -> void:
|
||||||
@@ -48,8 +45,7 @@ func _erde() -> void:
|
|||||||
_setze_welt(EarthWorld.new())
|
_setze_welt(EarthWorld.new())
|
||||||
player.set_mode(Player.Mode.GEIST)
|
player.set_mode(Player.Mode.GEIST)
|
||||||
player.global_position = (welt as EarthWorld).spawn
|
player.global_position = (welt as EarthWorld).spawn
|
||||||
player.yaw = 0.0
|
player.yaw = Player.CAM_YAW
|
||||||
player.pitch = -0.42
|
|
||||||
|
|
||||||
|
|
||||||
func _setze_welt(neu: Node3D) -> void:
|
func _setze_welt(neu: Node3D) -> void:
|
||||||
@@ -59,6 +55,7 @@ func _setze_welt(neu: Node3D) -> void:
|
|||||||
welt = neu
|
welt = neu
|
||||||
add_child(welt)
|
add_child(welt)
|
||||||
move_child(welt, 0)
|
move_child(welt, 0)
|
||||||
|
welt.set("spieler", player) # für den Etagenschnitt
|
||||||
Game.teleport_lock = 1.0
|
Game.teleport_lock = 1.0
|
||||||
_fokus = null
|
_fokus = null
|
||||||
_fokus_mensch = null
|
_fokus_mensch = null
|
||||||
@@ -92,7 +89,10 @@ func _fokus_suchen() -> void:
|
|||||||
if auf_erde:
|
if auf_erde:
|
||||||
var bestm := REICHWEITE_MENSCH
|
var bestm := REICHWEITE_MENSCH
|
||||||
for m in (welt as EarthWorld).humans:
|
for m in (welt as EarthWorld).humans:
|
||||||
var d: float = p.distance_to((m as Node3D).global_position + Vector3(0, 1.0, 0))
|
var mp: Vector3 = (m as Node3D).global_position + Vector3(0, 1.0, 0)
|
||||||
|
if absf(mp.y - p.y) > 2.0:
|
||||||
|
continue # andere Etage: durch die Decke sieht man nichts
|
||||||
|
var d: float = p.distance_to(mp)
|
||||||
if d < bestm:
|
if d < bestm:
|
||||||
bestm = d
|
bestm = d
|
||||||
_fokus_mensch = m
|
_fokus_mensch = m
|
||||||
@@ -143,17 +143,11 @@ func _notiz_hinweis(mensch: Bewohner) -> String:
|
|||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event.is_action_pressed("ui_cancel") and not ui.offen:
|
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if \
|
|
||||||
Input.mouse_mode == Input.MOUSE_MODE_CAPTURED else Input.MOUSE_MODE_CAPTURED
|
|
||||||
player.look_enabled = Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if ui.offen:
|
if ui.offen:
|
||||||
return
|
return
|
||||||
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_VISIBLE:
|
if event is InputEventMouseButton and event.pressed \
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
player.look_enabled = true
|
_klick(event.position)
|
||||||
return
|
return
|
||||||
for stufe in [1, 2, 3]:
|
for stufe in [1, 2, 3]:
|
||||||
if event.is_action_pressed("tempo_%d" % stufe):
|
if event.is_action_pressed("tempo_%d" % stufe):
|
||||||
@@ -213,6 +207,30 @@ func _notieren() -> void:
|
|||||||
Game.notieren(id, int(ziel.slot))
|
Game.notieren(id, int(ziel.slot))
|
||||||
|
|
||||||
|
|
||||||
|
## Klick auf einen Menschen: an ihn hängen, noch ein Klick löst wieder.
|
||||||
|
func _klick(pos: Vector2) -> void:
|
||||||
|
if not auf_erde:
|
||||||
|
return
|
||||||
|
var bester: Bewohner = null
|
||||||
|
var beste := 48.0
|
||||||
|
for m in (welt as EarthWorld).humans:
|
||||||
|
var w: Vector3 = (m as Node3D).global_position + Vector3(0, 0.8, 0)
|
||||||
|
if player.cam.is_position_behind(w):
|
||||||
|
continue
|
||||||
|
var d := player.cam.unproject_position(w).distance_to(pos)
|
||||||
|
if d < beste:
|
||||||
|
beste = d
|
||||||
|
bester = m
|
||||||
|
if bester == null:
|
||||||
|
return
|
||||||
|
if player.folgt == bester:
|
||||||
|
player.folgt = null
|
||||||
|
Game.notified.emit("Du lässt %s wieder los." % bester.data["name"], Color(0.8, 0.85, 1))
|
||||||
|
else:
|
||||||
|
player.folgt = bester
|
||||||
|
Game.notified.emit("Du hängst dich an %s." % bester.data["name"], Color(0.7, 0.9, 1))
|
||||||
|
|
||||||
|
|
||||||
## Als Geist an einen Menschen hängen und ihn durch seinen Tag begleiten.
|
## Als Geist an einen Menschen hängen und ihn durch seinen Tag begleiten.
|
||||||
func _verfolgen() -> void:
|
func _verfolgen() -> void:
|
||||||
if not auf_erde:
|
if not auf_erde:
|
||||||
|
|||||||
@@ -48,6 +48,54 @@ static func kennt(a: String) -> bool:
|
|||||||
return ACTIVITY_TEXT.has(a) or Ursachen.ist_blocker(a)
|
return ACTIVITY_TEXT.has(a) or Ursachen.ist_blocker(a)
|
||||||
|
|
||||||
|
|
||||||
|
const ETAGE := 3.0 # Höhe eines Stockwerks
|
||||||
|
|
||||||
|
|
||||||
|
static func etagen(places: Dictionary, key: String) -> int:
|
||||||
|
return maxi(1, int(places[key].get("etagen", 1)))
|
||||||
|
|
||||||
|
|
||||||
|
static func hoehe(e: int) -> float:
|
||||||
|
return float(e) * ETAGE
|
||||||
|
|
||||||
|
|
||||||
|
## Treppenhaus: die Ecke neben der Tür. Die Podeste wechseln je Etage die
|
||||||
|
## Seite, so läuft die Treppe im Zickzack an der Türwand hoch.
|
||||||
|
static func treppe(places: Dictionary, key: String, e: int) -> Vector3:
|
||||||
|
var pl: Dictionary = places[key]
|
||||||
|
var c := center(places, key)
|
||||||
|
var d: Vector3 = DIR_VEC[pl["dir"]]
|
||||||
|
var size: Array = pl["size"]
|
||||||
|
var quer_ist := absf(d.z) > 0.0
|
||||||
|
var halb_d: float = (float(size[1]) if quer_ist else float(size[0])) * 0.5
|
||||||
|
var halb_q: float = (float(size[0]) if quer_ist else float(size[1])) * 0.5
|
||||||
|
var quer := Vector3(-d.z, 0, d.x)
|
||||||
|
var p := c + d * (halb_d - 1.6) + quer * (halb_q - 1.4) - quer * (3.0 if e % 2 == 1 else 0.0)
|
||||||
|
p.y = hoehe(e)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
## Punkt gleich hinter der Tür, im Haus.
|
||||||
|
static func innen_tuer(places: Dictionary, key: String) -> Vector3:
|
||||||
|
var pl: Dictionary = places[key]
|
||||||
|
var c := center(places, key)
|
||||||
|
var d: Vector3 = DIR_VEC[pl["dir"]]
|
||||||
|
var size: Array = pl["size"]
|
||||||
|
var halb: float = (float(size[1]) if absf(d.z) > 0.0 else float(size[0])) * 0.5
|
||||||
|
return c + d * (halb - 1.2)
|
||||||
|
|
||||||
|
|
||||||
|
## Wegpunkte durchs Treppenhaus von Etage a nach Etage b, ohne a selbst.
|
||||||
|
static func treppen_weg(places: Dictionary, key: String, a: int, b: int) -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
var schritt := 1 if b > a else -1
|
||||||
|
var e := a
|
||||||
|
while e != b:
|
||||||
|
e += schritt
|
||||||
|
out.append(treppe(places, key, e))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
static func center(places: Dictionary, key: String) -> Vector3:
|
static func center(places: Dictionary, key: String) -> Vector3:
|
||||||
var p: Array = places[key]["pos"]
|
var p: Array = places[key]["pos"]
|
||||||
return Vector3(p[0], 0.0, p[1])
|
return Vector3(p[0], 0.0, p[1])
|
||||||
|
|||||||
@@ -9,22 +9,43 @@ const SPEED_TRAUM := 8.0
|
|||||||
const SPEED_GEIST := 11.0
|
const SPEED_GEIST := 11.0
|
||||||
const GRAVITY := 24.0
|
const GRAVITY := 24.0
|
||||||
const JUMP := 9.0
|
const JUMP := 9.0
|
||||||
const MAUS := 0.0026
|
const FOLGE_ABSTAND := 2.2
|
||||||
const FOLGE_ABSTAND := 3.0
|
|
||||||
const FOLGE_SPIEL := 1.5
|
const FOLGE_SPIEL := 1.5
|
||||||
|
const PITCH := -0.96 # 55° von oben, fest wie in Diablo
|
||||||
|
const CAM_YAW := PI * 0.25 # Blick diagonal über den Bezirk
|
||||||
|
const ZOOM := [26.0, 40.0] # zwei Stufen, Mausrad
|
||||||
|
const FOV := 35.0
|
||||||
|
const DREHSCHRITT := PI * 0.5
|
||||||
|
|
||||||
var mode: int = Mode.TRAUM
|
var mode: int = Mode.TRAUM
|
||||||
var yaw := 0.0
|
var yaw := CAM_YAW # Kamera- und damit Steuerrichtung
|
||||||
var pitch := -0.25
|
var look_enabled := true # Steuerung an (aus, solange ein Panel offen ist)
|
||||||
var look_enabled := true
|
|
||||||
var folgt: Node3D = null # Mensch, an den sich der Geist gehängt hat
|
var folgt: Node3D = null # Mensch, an den sich der Geist gehängt hat
|
||||||
|
|
||||||
var pivot: Node3D
|
var pivot: Node3D
|
||||||
var arm: SpringArm3D
|
var arm: Node3D
|
||||||
var cam: Camera3D
|
var cam: Camera3D
|
||||||
|
var _dreh_rest := 0.0 # noch zu drehender Winkel (Q)
|
||||||
|
var _zoom_stufe := 0
|
||||||
|
var _schwebe_ziel := -1.0 # Etage, zu der der Geist gerade gleitet
|
||||||
var body: Node3D
|
var body: Node3D
|
||||||
|
var _schleier: MeshInstance3D
|
||||||
var _kern: MeshInstance3D
|
var _kern: MeshInstance3D
|
||||||
var _ringe: Array = []
|
var _kopf: MeshInstance3D
|
||||||
|
var _augen: Array = []
|
||||||
|
var _funken: Array = [] # kreisende Lichter, eins je gelernter Technik
|
||||||
|
var _schleppe: Array = [] # Lichtschweif hinter dem Geist
|
||||||
|
var _schleppe_pos: Array = []
|
||||||
|
var _farbe := Color(0.55, 0.85, 1.0)
|
||||||
|
var _schleier_mat: StandardMaterial3D
|
||||||
|
var _kern_mat: StandardMaterial3D
|
||||||
|
var _kopf_mat: StandardMaterial3D
|
||||||
|
var _kreis := 0.0
|
||||||
|
|
||||||
|
const FARBE_TRAUM := Color(0.55, 0.85, 1.0)
|
||||||
|
const FARBE_GEIST := Color(0.50, 0.92, 0.90) # kühl: beobachten
|
||||||
|
const FARBE_FOLGE := Color(1.0, 0.72, 0.38) # warm: verfolgen
|
||||||
|
const FUNKEN_MAX := 6
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -38,32 +59,47 @@ func _ready() -> void:
|
|||||||
|
|
||||||
body = Node3D.new()
|
body = Node3D.new()
|
||||||
add_child(body)
|
add_child(body)
|
||||||
_kern = Prims.capsule(body, Vector3(0, 1.0, 0), 0.42, 1.6, Color(0.55, 0.85, 1.0))
|
# Schleier: oben schmal, unten weit, halb durchsichtig
|
||||||
_kern.material_override = Prims.mat(Color(0.55, 0.85, 1.0), 0.9)
|
_schleier = Prims.cyl(body, Vector3(0, 1.05, 0), 0.6, 1.7, _farbe, false)
|
||||||
var kopf := Prims.sphere(body, Vector3(0, 2.0, 0), 0.28, Color(0.80, 0.92, 1.0), 0.6)
|
(_schleier.mesh as CylinderMesh).top_radius = 0.24
|
||||||
kopf.name = "Kopf"
|
_schleier_mat = Prims.mat(Color(_farbe, 0.55), 0.35)
|
||||||
for i in range(2):
|
_schleier.material_override = _schleier_mat
|
||||||
var r := Prims.cyl(body, Vector3(0, 0.5 + i * 0.9, 0), 0.75 + i * 0.15, 0.06,
|
_schleier.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
Color(0.6, 0.9, 1.0), false, 1.6)
|
# Lichtkern im Schleier
|
||||||
r.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
_kern = Prims.sphere(body, Vector3(0, 1.15, 0), 0.26, _farbe, 2.2)
|
||||||
_ringe.append(r)
|
_kern_mat = _kern.material_override as StandardMaterial3D
|
||||||
# kleiner Blick nach vorn, damit man die Laufrichtung sieht
|
_kern.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
Prims.box(body, Vector3(0, 1.55, -0.36), Vector3(0.5, 0.12, 0.12), Color(1, 1, 1), false, 2.0)
|
_kopf = Prims.sphere(body, Vector3(0, 2.0, 0), 0.3, _farbe.lightened(0.4), 0.6)
|
||||||
|
_kopf.name = "Kopf"
|
||||||
|
_kopf_mat = _kopf.material_override as StandardMaterial3D
|
||||||
|
for sx: float in [-1.0, 1.0]:
|
||||||
|
var auge := Prims.box(body, Vector3(sx * 0.11, 2.04, -0.27), Vector3(0.07, 0.11, 0.05),
|
||||||
|
Color(1, 1, 1), false, 3.0)
|
||||||
|
auge.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
_augen.append(auge)
|
||||||
|
for i in range(FUNKEN_MAX):
|
||||||
|
var f := Prims.sphere(body, Vector3.ZERO, 0.08, _farbe.lightened(0.3), 2.6)
|
||||||
|
f.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
f.visible = false
|
||||||
|
_funken.append(f)
|
||||||
|
for i in range(3):
|
||||||
|
var k := Prims.sphere(self, Vector3.ZERO, 0.16 - i * 0.04, _farbe, 1.8)
|
||||||
|
k.top_level = true
|
||||||
|
k.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
k.visible = false
|
||||||
|
_schleppe.append(k)
|
||||||
|
_schleppe_pos.append(global_position)
|
||||||
|
|
||||||
pivot = Node3D.new()
|
pivot = Node3D.new()
|
||||||
pivot.position.y = 1.5
|
pivot.position.y = 1.0
|
||||||
add_child(pivot)
|
add_child(pivot)
|
||||||
arm = SpringArm3D.new()
|
arm = Node3D.new()
|
||||||
arm.spring_length = 6.5
|
arm.rotation.x = PITCH
|
||||||
arm.margin = 0.2
|
|
||||||
var arm_form := SphereShape3D.new()
|
|
||||||
arm_form.radius = 0.45 # Kugel statt Strahl: die Kamera klemmt nicht in Wänden
|
|
||||||
arm.shape = arm_form
|
|
||||||
pivot.add_child(arm)
|
pivot.add_child(arm)
|
||||||
arm.add_excluded_object(get_rid())
|
|
||||||
cam = Camera3D.new()
|
cam = Camera3D.new()
|
||||||
cam.position.x = 0.7 # Blick über die Schulter, am Ende des Arms
|
cam.position.z = ZOOM[0]
|
||||||
cam.fov = 74.0
|
cam.fov = FOV
|
||||||
|
cam.near = 0.5
|
||||||
arm.add_child(cam)
|
arm.add_child(cam)
|
||||||
set_mode(Mode.TRAUM)
|
set_mode(Mode.TRAUM)
|
||||||
|
|
||||||
@@ -74,44 +110,37 @@ func set_mode(m: int) -> void:
|
|||||||
if m == Mode.GEIST:
|
if m == Mode.GEIST:
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
arm.collision_mask = 0
|
body.scale = Vector3.ONE * 0.6
|
||||||
arm.spring_length = 5.0
|
_schleier_mat.albedo_color.a = 0.3
|
||||||
pivot.position.y = 2.1
|
|
||||||
body.scale = Vector3.ONE * 0.72
|
|
||||||
_kern.material_override = Prims.mat(Color(0.65, 0.9, 1.0, 0.26), 0.5)
|
|
||||||
for r in _ringe:
|
|
||||||
r.material_override = Prims.mat(Color(0.7, 0.95, 1.0, 0.20), 0.8)
|
|
||||||
r.scale = Vector3(0.55, 1.0, 0.55)
|
|
||||||
else:
|
else:
|
||||||
collision_layer = 1
|
collision_layer = 1
|
||||||
collision_mask = 1
|
collision_mask = 1
|
||||||
arm.collision_mask = 1
|
body.scale = Vector3.ONE * 0.8
|
||||||
arm.spring_length = 6.5
|
_schleier_mat.albedo_color.a = 0.55
|
||||||
pivot.position.y = 1.5
|
|
||||||
body.scale = Vector3.ONE
|
|
||||||
_kern.material_override = Prims.mat(Color(0.55, 0.85, 1.0), 0.9)
|
|
||||||
for r in _ringe:
|
|
||||||
r.material_override = Prims.mat(Color(0.6, 0.9, 1.0), 1.6)
|
|
||||||
r.scale = Vector3.ONE
|
|
||||||
velocity = Vector3.ZERO
|
velocity = Vector3.ZERO
|
||||||
|
_schwebe_ziel = -1.0
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if not look_enabled:
|
if not look_enabled:
|
||||||
return
|
return
|
||||||
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
if event.is_action_pressed("drehen"):
|
||||||
yaw -= event.relative.x * MAUS
|
_dreh_rest += DREHSCHRITT
|
||||||
pitch = clampf(pitch - event.relative.y * MAUS, -1.3, 0.6)
|
elif event is InputEventMouseButton and event.pressed:
|
||||||
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||||
|
_zoom_stufe = 0
|
||||||
|
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||||
|
_zoom_stufe = 1
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
|
var dreh := minf(_dreh_rest, delta * 4.0)
|
||||||
|
yaw += dreh
|
||||||
|
_dreh_rest -= dreh
|
||||||
pivot.rotation.y = yaw
|
pivot.rotation.y = yaw
|
||||||
arm.rotation.x = pitch
|
cam.position.z = lerpf(cam.position.z, float(ZOOM[_zoom_stufe]), 1.0 - pow(0.01, delta))
|
||||||
|
|
||||||
for i in range(_ringe.size()):
|
_beleben(delta)
|
||||||
var r: MeshInstance3D = _ringe[i]
|
|
||||||
r.rotation.y += delta * (0.8 + 0.5 * i)
|
|
||||||
r.position.y = 0.5 + i * 0.9 + sin(Time.get_ticks_msec() * 0.001 * (1.2 + i * 0.4)) * 0.12
|
|
||||||
|
|
||||||
var input := Vector2.ZERO
|
var input := Vector2.ZERO
|
||||||
if look_enabled:
|
if look_enabled:
|
||||||
@@ -128,8 +157,9 @@ func _physics_process(delta: float) -> void:
|
|||||||
elif dir.length() > 0.01:
|
elif dir.length() > 0.01:
|
||||||
folgt = null # eigene Bewegung löst die Verfolgung
|
folgt = null # eigene Bewegung löst die Verfolgung
|
||||||
else:
|
else:
|
||||||
var schulter := folgt.global_position + Vector3(0, 1.6, 0) \
|
var rechts := Basis(Vector3.UP, yaw) * Vector3(1, 0, 0)
|
||||||
- blickrichtung() * FOLGE_ABSTAND
|
var schulter := folgt.global_position + Vector3(0, 1.4, 0) \
|
||||||
|
+ rechts * FOLGE_ABSTAND - blickrichtung() * 0.8
|
||||||
global_position = global_position.lerp(schulter, 1.0 - pow(0.004, delta))
|
global_position = global_position.lerp(schulter, 1.0 - pow(0.004, delta))
|
||||||
# weich nachziehen, aber an der Leine: sonst reißt der Faden beim Gehen
|
# weich nachziehen, aber an der Leine: sonst reißt der Faden beim Gehen
|
||||||
var ab := global_position - schulter
|
var ab := global_position - schulter
|
||||||
@@ -141,12 +171,24 @@ func _physics_process(delta: float) -> void:
|
|||||||
|
|
||||||
if mode == Mode.GEIST:
|
if mode == Mode.GEIST:
|
||||||
var v := dir * SPEED_GEIST
|
var v := dir * SPEED_GEIST
|
||||||
|
velocity.x = lerpf(velocity.x, v.x, 1.0 - pow(0.001, delta))
|
||||||
|
velocity.z = lerpf(velocity.z, v.z, 1.0 - pow(0.001, delta))
|
||||||
|
# Steigen und Sinken wechseln die Etage: der Geist gleitet ein Stockwerk
|
||||||
if look_enabled:
|
if look_enabled:
|
||||||
if Input.is_action_pressed("steigen"):
|
var von: float = _schwebe_ziel if _schwebe_ziel >= 0.0 else global_position.y
|
||||||
v.y += SPEED_GEIST * 0.75
|
if Input.is_action_just_pressed("steigen"):
|
||||||
if Input.is_action_pressed("sinken"):
|
_schwebe_ziel = clampf(von + Orte.ETAGE, 0.4, 45.0)
|
||||||
v.y -= SPEED_GEIST * 0.75
|
if Input.is_action_just_pressed("sinken"):
|
||||||
velocity = velocity.lerp(v, 1.0 - pow(0.001, delta))
|
_schwebe_ziel = clampf(von - Orte.ETAGE, 0.4, 45.0)
|
||||||
|
if _schwebe_ziel >= 0.0:
|
||||||
|
var dy := _schwebe_ziel - global_position.y
|
||||||
|
velocity.y = clampf(dy * 6.0, -SPEED_GEIST, SPEED_GEIST)
|
||||||
|
if absf(dy) < 0.05:
|
||||||
|
global_position.y = _schwebe_ziel
|
||||||
|
velocity.y = 0.0
|
||||||
|
_schwebe_ziel = -1.0
|
||||||
|
else:
|
||||||
|
velocity.y = 0.0
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
if global_position.y < 0.4:
|
if global_position.y < 0.4:
|
||||||
global_position.y = 0.4
|
global_position.y = 0.4
|
||||||
@@ -173,3 +215,47 @@ func _physics_process(delta: float) -> void:
|
|||||||
|
|
||||||
func blickrichtung() -> Vector3:
|
func blickrichtung() -> Vector3:
|
||||||
return -Vector3(sin(yaw), 0, cos(yaw))
|
return -Vector3(sin(yaw), 0, cos(yaw))
|
||||||
|
|
||||||
|
|
||||||
|
## Schweben, Funken, Schweif und Modusfarbe -- der Zustand ist am Geist ablesbar.
|
||||||
|
func _beleben(delta: float) -> void:
|
||||||
|
var t := Time.get_ticks_msec() * 0.001
|
||||||
|
var ziel := FARBE_TRAUM
|
||||||
|
if mode == Mode.GEIST:
|
||||||
|
ziel = FARBE_FOLGE if folgt != null else FARBE_GEIST
|
||||||
|
if not Game.situation.is_empty() and fmod(t, 0.5) < 0.12:
|
||||||
|
ziel = ziel.lightened(0.6)
|
||||||
|
_farbe = _farbe.lerp(ziel, 1.0 - pow(0.02, delta))
|
||||||
|
_schleier_mat.albedo_color = Color(_farbe, _schleier_mat.albedo_color.a)
|
||||||
|
_schleier_mat.emission = _farbe
|
||||||
|
_kern_mat.albedo_color = _farbe
|
||||||
|
_kern_mat.emission = _farbe
|
||||||
|
_kopf_mat.albedo_color = _farbe.lightened(0.4)
|
||||||
|
_kopf_mat.emission = _farbe.lightened(0.4)
|
||||||
|
|
||||||
|
body.position.y = sin(t * 1.6) * 0.08
|
||||||
|
_kern.scale = Vector3.ONE * (1.0 + sin(t * 3.1) * 0.12)
|
||||||
|
var tempo_flach := Vector2(velocity.x, velocity.z).length()
|
||||||
|
var neigung := -0.16 * clampf(tempo_flach / SPEED_TRAUM, 0.0, 1.0)
|
||||||
|
body.rotation.x = lerpf(body.rotation.x, neigung, 1.0 - pow(0.01, delta))
|
||||||
|
|
||||||
|
var anzahl := clampi(Game.known.size(), 1, FUNKEN_MAX)
|
||||||
|
_kreis += delta * (0.9 + 0.7 * float(Game.tempo - 1))
|
||||||
|
for i in range(FUNKEN_MAX):
|
||||||
|
var f: MeshInstance3D = _funken[i]
|
||||||
|
f.visible = i < anzahl
|
||||||
|
if not f.visible:
|
||||||
|
continue
|
||||||
|
var w := _kreis + float(i) * TAU / float(anzahl)
|
||||||
|
f.position = Vector3(cos(w) * 0.85, 1.2 + sin(w * 2.0 + float(i)) * 0.3, sin(w) * 0.85)
|
||||||
|
f.scale = Vector3.ONE * (0.8 + 0.3 * sin(t * 4.0 + float(i)))
|
||||||
|
|
||||||
|
var kopf_welt := body.to_global(Vector3(0, 1.1, 0))
|
||||||
|
var sichtbar := tempo_flach > 1.0
|
||||||
|
for i in range(_schleppe.size()):
|
||||||
|
var k: MeshInstance3D = _schleppe[i]
|
||||||
|
var vorher: Vector3 = kopf_welt if i == 0 else _schleppe_pos[i - 1]
|
||||||
|
_schleppe_pos[i] = (_schleppe_pos[i] as Vector3).lerp(vorher, 1.0 - pow(0.0002 * (i + 1), delta))
|
||||||
|
k.global_position = _schleppe_pos[i]
|
||||||
|
k.visible = sichtbar
|
||||||
|
k.material_override = _kern_mat
|
||||||
|
|||||||
@@ -108,9 +108,10 @@ static func label(parent: Node, pos: Vector3, text: String, size: float = 0.35,
|
|||||||
## door_dir: Vector3.FORWARD/BACK/LEFT/RIGHT -- auf welcher Seite die Lücke sitzt.
|
## door_dir: Vector3.FORWARD/BACK/LEFT/RIGHT -- auf welcher Seite die Lücke sitzt.
|
||||||
static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color,
|
static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color,
|
||||||
floor_c: Color, door_dir: Vector3, door_w: float = 3.0,
|
floor_c: Color, door_dir: Vector3, door_w: float = 3.0,
|
||||||
with_floor: bool = true) -> void:
|
with_floor: bool = true) -> Array:
|
||||||
var t := 0.4
|
var t := 0.4
|
||||||
var h := size.y
|
var h := size.y
|
||||||
|
var waende: Array = []
|
||||||
if with_floor:
|
if with_floor:
|
||||||
box(parent, center + Vector3(0, -0.1, 0), Vector3(size.x, 0.2, size.z), floor_c)
|
box(parent, center + Vector3(0, -0.1, 0), Vector3(size.x, 0.2, size.z), floor_c)
|
||||||
var seiten: Array[Vector3] = [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]
|
var seiten: Array[Vector3] = [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]
|
||||||
@@ -126,9 +127,90 @@ static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color,
|
|||||||
var seg_size: Vector3 = Vector3(seg, h, t) if quer else Vector3(t, h, seg)
|
var seg_size: Vector3 = Vector3(seg, h, t) if quer else Vector3(t, h, seg)
|
||||||
for s: float in [-1.0, 1.0]:
|
for s: float in [-1.0, 1.0]:
|
||||||
var seg_pos: Vector3 = center + off + along * (s * (door_w * 0.5 + seg * 0.5))
|
var seg_pos: Vector3 = center + off + along * (s * (door_w * 0.5 + seg * 0.5))
|
||||||
box(parent, seg_pos, seg_size, wall_c)
|
waende.append(_wand(box(parent, seg_pos, seg_size, wall_c), d))
|
||||||
if h >= 3.0:
|
if h >= 3.0:
|
||||||
var sturz: Vector3 = Vector3(door_w, h * 0.3, t) if quer else Vector3(t, h * 0.3, door_w)
|
var sturz: Vector3 = Vector3(door_w, h * 0.3, t) if quer else Vector3(t, h * 0.3, door_w)
|
||||||
box(parent, center + off + Vector3(0, h * 0.35, 0), sturz, wall_c)
|
waende.append(_wand(box(parent, center + off + Vector3(0, h * 0.35, 0), sturz, wall_c), d))
|
||||||
else:
|
else:
|
||||||
box(parent, center + off, voll, wall_c)
|
waende.append(_wand(box(parent, center + off, voll, wall_c), d))
|
||||||
|
return waende
|
||||||
|
|
||||||
|
|
||||||
|
static func _meshes_von(b: Dictionary) -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
for n in b["etagen"]:
|
||||||
|
_sammle(n, out)
|
||||||
|
for n in b["dach"]:
|
||||||
|
_sammle(n, out)
|
||||||
|
for liste in b["waende"]:
|
||||||
|
for w in liste:
|
||||||
|
if not out.has(w):
|
||||||
|
out.append(w)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
static func _sammle(n: Node, out: Array) -> void:
|
||||||
|
if n is MeshInstance3D and not out.has(n):
|
||||||
|
out.append(n)
|
||||||
|
for k in n.get_children():
|
||||||
|
_sammle(k, out)
|
||||||
|
|
||||||
|
|
||||||
|
static func _wand(mi: MeshInstance3D, normale: Vector3) -> MeshInstance3D:
|
||||||
|
mi.set_meta("normal", normale)
|
||||||
|
return mi
|
||||||
|
|
||||||
|
|
||||||
|
## Etagenschnitt wie bei den Sims: steht der Spieler in einem Bau, verschwinden
|
||||||
|
## das Dach, die Etagen über ihm und die beiden Wände, die zur Kamera zeigen.
|
||||||
|
## Ein Bau: {c, size, hoch, etagen, dach, waende, voll}. "voll" heißt: ein
|
||||||
|
## geschlossener Klotz, der ganz verschwindet.
|
||||||
|
## Ein Bau zwischen Kamera und Spieler wird durchsichtig, sonst verschwände
|
||||||
|
## der Spieler hinter jedem Nachbarhaus.
|
||||||
|
static func schnitt(bauten: Array, spieler: Vector3, kamera: Vector3) -> void:
|
||||||
|
for b in bauten:
|
||||||
|
var c: Vector3 = b["c"]
|
||||||
|
var g: Vector2 = b["size"]
|
||||||
|
var drin: bool = absf(spieler.x - c.x) < g.x * 0.5 + 0.3 \
|
||||||
|
and absf(spieler.z - c.z) < g.y * 0.5 + 0.3 \
|
||||||
|
and spieler.y > c.y - 0.5 and spieler.y < c.y + float(b["hoch"]) + 0.5
|
||||||
|
var kasten := AABB(Vector3(c.x - g.x * 0.5, c.y, c.z - g.y * 0.5),
|
||||||
|
Vector3(g.x, float(b["hoch"]), g.y))
|
||||||
|
var im_weg: bool = not drin \
|
||||||
|
and kasten.intersects_segment(kamera, spieler + Vector3(0, 0.8, 0)) != null
|
||||||
|
if bool(b.get("im_weg", false)) != im_weg:
|
||||||
|
b["im_weg"] = im_weg
|
||||||
|
if not b.has("meshes"):
|
||||||
|
b["meshes"] = _meshes_von(b)
|
||||||
|
for mi in b["meshes"]:
|
||||||
|
(mi as GeometryInstance3D).transparency = 0.82 if im_weg else 0.0
|
||||||
|
var etagen: Array = b["etagen"]
|
||||||
|
var pf := clampi(int(floor((spieler.y - c.y + 0.6) / Orte.ETAGE)), 0, maxi(0, etagen.size() - 1))
|
||||||
|
for f in range(etagen.size()):
|
||||||
|
(etagen[f] as Node3D).visible = not drin or (f <= pf and not bool(b.get("voll", false)))
|
||||||
|
for d in b["dach"]:
|
||||||
|
(d as Node3D).visible = not drin
|
||||||
|
var waende: Array = b["waende"]
|
||||||
|
for f in range(waende.size()):
|
||||||
|
for w in waende[f]:
|
||||||
|
var n: Vector3 = w.get_meta("normal")
|
||||||
|
(w as Node3D).visible = not (drin and f == pf and n.dot(kamera - c) > 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
## Ein Satteldach: ein geschlossenes Prisma über den Wänden. Der First läuft
|
||||||
|
## entlang der längeren Seite, das Dach steht ringsum 0,6 über.
|
||||||
|
static func dach(parent: Node, center: Vector3, size: Vector3, c: Color) -> MeshInstance3D:
|
||||||
|
var quer := size.x >= size.z # First entlang X
|
||||||
|
var spann: float = (size.z if quer else size.x) + 1.2
|
||||||
|
var laenge: float = (size.x if quer else size.z) + 1.2
|
||||||
|
var hoehe := spann * 0.5 * tan(0.62)
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
var pm := PrismMesh.new()
|
||||||
|
pm.size = Vector3(spann, hoehe, laenge)
|
||||||
|
mi.mesh = pm
|
||||||
|
mi.material_override = mat(c.darkened(0.3))
|
||||||
|
mi.position = center + Vector3(0, size.y + hoehe * 0.5, 0)
|
||||||
|
if quer:
|
||||||
|
mi.rotation.y = PI * 0.5
|
||||||
|
parent.add_child(mi)
|
||||||
|
return mi
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func _baue_hud() -> void:
|
|||||||
_root.add_child(_hud)
|
_root.add_child(_hud)
|
||||||
|
|
||||||
var steuerung := Label.new()
|
var steuerung := Label.new()
|
||||||
steuerung.text = "J Journal · 1-3 Tempo · Esc"
|
steuerung.text = "J Journal · 1-3 Tempo · Q Kamera · Mausrad Zoom · Esc"
|
||||||
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||||
steuerung.position = Vector2(-260, 16)
|
steuerung.position = Vector2(-260, 16)
|
||||||
steuerung.size = Vector2(240, 24)
|
steuerung.size = Vector2(240, 24)
|
||||||
|
|||||||
350
tests/verify.gd
350
tests/verify.gd
@@ -26,6 +26,7 @@ func _ready() -> void:
|
|||||||
await test_journal()
|
await test_journal()
|
||||||
await test_traumdorf()
|
await test_traumdorf()
|
||||||
await test_dorf_organisch()
|
await test_dorf_organisch()
|
||||||
|
await test_huegel()
|
||||||
await test_haus_betreten()
|
await test_haus_betreten()
|
||||||
await test_hoersaal()
|
await test_hoersaal()
|
||||||
await test_mission_annehmen()
|
await test_mission_annehmen()
|
||||||
@@ -35,6 +36,8 @@ func _ready() -> void:
|
|||||||
await test_wege_machbar()
|
await test_wege_machbar()
|
||||||
await test_geist()
|
await test_geist()
|
||||||
await test_verfolgung()
|
await test_verfolgung()
|
||||||
|
await test_charakter()
|
||||||
|
await test_kamera_und_etagen()
|
||||||
await test_gehen_bei_tempo()
|
await test_gehen_bei_tempo()
|
||||||
await test_diagnose()
|
await test_diagnose()
|
||||||
await test_notieren_im_spiel()
|
await test_notieren_im_spiel()
|
||||||
@@ -166,6 +169,8 @@ func _druecke(anfang: String) -> bool:
|
|||||||
func laufe(von: Vector3, yaw: float, frames: int) -> Vector3:
|
func laufe(von: Vector3, yaw: float, frames: int) -> Vector3:
|
||||||
main.player.set_mode(Player.Mode.TRAUM)
|
main.player.set_mode(Player.Mode.TRAUM)
|
||||||
main.player.look_enabled = true
|
main.player.look_enabled = true
|
||||||
|
if welt() is DreamWorld:
|
||||||
|
von.y = maxf(von.y, DreamDB.hoehe_bei(von.x, von.z) + 1.0) # nie im Hügel starten
|
||||||
main.player.global_position = von
|
main.player.global_position = von
|
||||||
main.player.velocity = Vector3.ZERO
|
main.player.velocity = Vector3.ZERO
|
||||||
main.player.yaw = yaw
|
main.player.yaw = yaw
|
||||||
@@ -375,6 +380,103 @@ func test_traumdorf() -> void:
|
|||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
|
||||||
|
## Die Insel ist ein Hügel: innen eben, außen ansteigend, jedes Gebäude auf
|
||||||
|
## einer Terrasse. Spieler und Bewohner laufen darauf, statt hindurchzufallen.
|
||||||
|
func test_huegel() -> void:
|
||||||
|
abschnitt("Die Insel ist ein Hügel")
|
||||||
|
var uhr_vorher := Game.time_h
|
||||||
|
var tag_vorher := Game.day
|
||||||
|
p(is_equal_approx(DreamDB.hang(0.0), 0.0) and is_equal_approx(DreamDB.hang(DreamDB.FLACH), 0.0),
|
||||||
|
"Bis %.0f Einheiten ist es eben" % DreamDB.FLACH)
|
||||||
|
p(DreamDB.hang(DreamDB.FLACH + 20.0) > 3.0 and DreamDB.hang(200.0) <= DreamDB.GIPFEL,
|
||||||
|
"Dahinter steigt es an, bis zum Gipfel bei %.0f (bei +20: %.1f)" %
|
||||||
|
[DreamDB.GIPFEL, DreamDB.hang(DreamDB.FLACH + 20.0)])
|
||||||
|
p(DreamDB.hoehe_bei(0.0, 2.0) == 0.0, "Der Dorfplatz liegt eben")
|
||||||
|
var tor_terr := DreamDB.terrasse("tor")
|
||||||
|
p(is_equal_approx(DreamDB.hoehe_bei(0.0, -34.0), tor_terr)
|
||||||
|
and is_equal_approx(DreamDB.hoehe_bei(0.0, -34.0 + DreamDB.TOR_FLAECHE * 0.5 + 2.0), tor_terr),
|
||||||
|
"Das Tor steht auf einer Terrasse, eben mit seiner Dorfseite (%.1f)" % tor_terr)
|
||||||
|
var stufe := absf(DreamDB.hoehe_bei(0.0, 25.4) - DreamDB.terrasse("hoersaal"))
|
||||||
|
p(stufe < 0.1, "Vor der Hörsaaltür ist keine Stufe (%.2f)" % stufe)
|
||||||
|
|
||||||
|
# Ein Dorf mit vielen Häusern: manche stehen am Hang, jedes auf einer Terrasse
|
||||||
|
var merker: Array = Game.completed.duplicate()
|
||||||
|
Game.completed.clear()
|
||||||
|
for i in range(14):
|
||||||
|
Game.completed.append("huegel_%d" % i)
|
||||||
|
Game.welt_erzeugen()
|
||||||
|
var am_hang := 0
|
||||||
|
var schief: Array = []
|
||||||
|
for key in DreamDB.PLACES:
|
||||||
|
var s: Array = DreamDB.PLACES[key]["size"]
|
||||||
|
if float(s[0]) <= 0.0:
|
||||||
|
continue
|
||||||
|
var c := Orte.center(DreamDB.PLACES, key)
|
||||||
|
var terr := DreamDB.terrasse(key)
|
||||||
|
if terr > 0.5:
|
||||||
|
am_hang += 1
|
||||||
|
for dx in [-1.0, 1.0]:
|
||||||
|
for dz in [-1.0, 1.0]:
|
||||||
|
var h := DreamDB.hoehe_bei(c.x + dx * float(s[0]) * 0.45, c.z + dz * float(s[1]) * 0.45)
|
||||||
|
if absf(h - terr) > 0.01:
|
||||||
|
schief.append(key)
|
||||||
|
p(am_hang >= 3, "Mit 14 Häusern stehen welche am Hang (%d)" % am_hang)
|
||||||
|
p(schief.is_empty(), "Jedes Gebäude steht innen eben auf seiner Terrasse%s" %
|
||||||
|
("" if schief.is_empty() else " — nicht: " + ", ".join(schief)))
|
||||||
|
|
||||||
|
main._traumwelt()
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
# Bewohner laufen einen Tag lang auf dem Boden, nicht darunter oder darüber
|
||||||
|
var abweichung := 0.0
|
||||||
|
Game.time_h = 0.0
|
||||||
|
for i in range(3000):
|
||||||
|
schritte(1)
|
||||||
|
for w in welt().bewohner:
|
||||||
|
var soll := DreamDB.hoehe_bei(w.global_position.x, w.global_position.z)
|
||||||
|
abweichung = maxf(abweichung, absf(w.global_position.y - soll))
|
||||||
|
p(abweichung < 0.05, "%d Bewohner bleiben auf dem Boden (Abweichung %.2f)" %
|
||||||
|
[welt().bewohner.size(), abweichung])
|
||||||
|
|
||||||
|
# Der Spieler kommt den Hang hinauf und fällt nicht hindurch. Dafür eine
|
||||||
|
# Richtung, in der kein Gebäude im Weg steht.
|
||||||
|
var richtung := Vector3.ZERO
|
||||||
|
var wenigste := 999
|
||||||
|
for k in range(24):
|
||||||
|
var a := TAU * float(k) / 24.0
|
||||||
|
var d := Vector3(cos(a), 0, sin(a))
|
||||||
|
var treffer := 0
|
||||||
|
for r in range(int(DreamDB.FLACH), int(DreamDB.FLACH) + 24, 2):
|
||||||
|
var q := d * float(r)
|
||||||
|
for key in DreamDB.PLACES:
|
||||||
|
var s: Array = DreamDB.PLACES[key]["size"]
|
||||||
|
if float(s[0]) <= 0.0:
|
||||||
|
continue
|
||||||
|
var c := Orte.center(DreamDB.PLACES, key)
|
||||||
|
if absf(q.x - c.x) < float(s[0]) * 0.5 + 2.5 and absf(q.z - c.z) < float(s[1]) * 0.5 + 2.5:
|
||||||
|
treffer += 1
|
||||||
|
if treffer < wenigste:
|
||||||
|
wenigste = treffer
|
||||||
|
richtung = d
|
||||||
|
p(wenigste == 0, "Eine freie Richtung den Hang hinauf gibt es (beste: %d Treffer)" % wenigste)
|
||||||
|
var start := richtung * (DreamDB.FLACH + 1.0) + Vector3(0, 1.0, 0)
|
||||||
|
var yaw := atan2(-richtung.x, -richtung.z)
|
||||||
|
var ende: Vector3 = await laufe(start, yaw, 140)
|
||||||
|
var boden_dort := DreamDB.hoehe_bei(ende.x, ende.z)
|
||||||
|
var weit := Vector2(ende.x, ende.z).length() - Vector2(start.x, start.z).length()
|
||||||
|
p(weit > 6.0, "Er läuft hangaufwärts (%.0f Einheiten weiter außen)" % weit)
|
||||||
|
p(ende.y > start.y + 1.0, "Und kommt dabei höher (y %.1f -> %.1f)" % [start.y, ende.y])
|
||||||
|
p(ende.y >= boden_dort - 0.2 and ende.y < boden_dort + 2.5,
|
||||||
|
"Er steht auf dem Boden, nicht darunter (y %.1f, Boden %.1f)" % [ende.y, boden_dort])
|
||||||
|
|
||||||
|
Game.completed.assign(merker)
|
||||||
|
Game.time_h = uhr_vorher
|
||||||
|
Game.day = tag_vorher
|
||||||
|
Game.welt_erzeugen()
|
||||||
|
main._traumwelt()
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
|
||||||
func test_haus_betreten() -> void:
|
func test_haus_betreten() -> void:
|
||||||
abschnitt("Orte betreten")
|
abschnitt("Orte betreten")
|
||||||
p(not Game.ort_offen(DreamDB.haus_key(0)), "Ohne Bewohner steht kein Haus")
|
p(not Game.ort_offen(DreamDB.haus_key(0)), "Ohne Bewohner steht kein Haus")
|
||||||
@@ -586,8 +688,8 @@ func test_tagesablauf() -> void:
|
|||||||
|
|
||||||
schritte(30, 0.05, 2.0)
|
schritte(30, 0.05, 2.0)
|
||||||
p(mensch.activity == "schlaeft", "Um 02:00 schläft er (%s)" % mensch.activity)
|
p(mensch.activity == "schlaeft", "Um 02:00 schläft er (%s)" % mensch.activity)
|
||||||
p(mensch.global_position.distance_to(
|
p(_xz(mensch.global_position).distance_to(
|
||||||
Orte.center(HumanDB.PLACES, str(mensch.data["heim"]))) < 5.0,
|
_xz(Orte.center(HumanDB.PLACES, str(mensch.data["heim"])))) < 5.0,
|
||||||
"Und zwar zu Hause")
|
"Und zwar zu Hause")
|
||||||
|
|
||||||
# Jede Station des eigenen Tagesablaufs wird pünktlich erreicht
|
# Jede Station des eigenen Tagesablaufs wird pünktlich erreicht
|
||||||
@@ -704,6 +806,250 @@ func test_geist() -> void:
|
|||||||
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
|
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
|
||||||
|
|
||||||
|
|
||||||
|
func _farbabstand(a: Color, b: Color) -> float:
|
||||||
|
return Vector3(a.r, a.g, a.b).distance_to(Vector3(b.r, b.g, b.b))
|
||||||
|
|
||||||
|
|
||||||
|
func test_charakter() -> void:
|
||||||
|
abschnitt("Der Geist und die Menschen sind am Körper lesbar")
|
||||||
|
ui().schliessen()
|
||||||
|
var sp: Player = main.player
|
||||||
|
sp.set_mode(Player.Mode.GEIST)
|
||||||
|
sp.look_enabled = true
|
||||||
|
sp.folgt = null
|
||||||
|
p(sp._augen.size() == 2 and sp._augen[0].position.z < 0.0, "Zwei Augen, vorn am Kopf")
|
||||||
|
p(sp._funken.size() == Player.FUNKEN_MAX, "%d Funken stehen bereit" % Player.FUNKEN_MAX)
|
||||||
|
for i in range(60):
|
||||||
|
await get_tree().physics_frame
|
||||||
|
var sichtbar := 0
|
||||||
|
for f in sp._funken:
|
||||||
|
if f.visible:
|
||||||
|
sichtbar += 1
|
||||||
|
var soll := clampi(Game.known.size(), 1, Player.FUNKEN_MAX)
|
||||||
|
p(sichtbar == soll, "Je gelernter Technik kreist ein Funke (%d bei %d Techniken)" %
|
||||||
|
[sichtbar, Game.known.size()])
|
||||||
|
p(_farbabstand(sp._farbe, Player.FARBE_GEIST) < 0.1, "Beim Beobachten leuchtet er kühl")
|
||||||
|
var mensch: Bewohner = welt().humans[0]
|
||||||
|
sp.global_position = mensch.global_position + Vector3(0, 1.5, 2.0)
|
||||||
|
sp.folgt = mensch
|
||||||
|
for i in range(90):
|
||||||
|
await get_tree().physics_frame
|
||||||
|
p(_farbabstand(sp._farbe, Player.FARBE_FOLGE) < 0.1, "Beim Verfolgen leuchtet er warm")
|
||||||
|
sp.folgt = null
|
||||||
|
Input.action_press("vor")
|
||||||
|
for i in range(40):
|
||||||
|
await get_tree().physics_frame
|
||||||
|
Input.action_release("vor")
|
||||||
|
var kopf: Vector3 = sp.body.to_global(Vector3(0, 1.1, 0))
|
||||||
|
var hinten := 0
|
||||||
|
for k in sp._schleppe:
|
||||||
|
if k.visible and (k as Node3D).global_position.distance_to(kopf) > 0.3:
|
||||||
|
hinten += 1
|
||||||
|
p(hinten >= 2, "Beim Fliegen zieht er einen Schweif hinter sich her (%d von 3)" % hinten)
|
||||||
|
for i in range(40):
|
||||||
|
await get_tree().physics_frame
|
||||||
|
var ruht := true
|
||||||
|
for k in sp._schleppe:
|
||||||
|
if k.visible:
|
||||||
|
ruht = false
|
||||||
|
p(ruht, "Im Stand verschwindet der Schweif")
|
||||||
|
|
||||||
|
# Haltung der Menschen: Angst duckt, Erschöpfung beugt -- bis überwunden
|
||||||
|
var merker := Game.ueberwunden.duplicate(true)
|
||||||
|
var muede: Bewohner = null
|
||||||
|
var bang: Bewohner = null
|
||||||
|
var frei: Bewohner = null
|
||||||
|
for m in welt().humans:
|
||||||
|
var pr: Array = m.data.get("probleme", [])
|
||||||
|
if muede == null and pr.has("erschoepfung"):
|
||||||
|
muede = m
|
||||||
|
elif bang == null and pr.has("angst"):
|
||||||
|
bang = m
|
||||||
|
elif frei == null and not pr.has("angst") and not pr.has("erschoepfung"):
|
||||||
|
frei = m
|
||||||
|
p(muede != null and bang != null and frei != null, "Müde, ängstliche und freie Menschen sind da")
|
||||||
|
if muede != null:
|
||||||
|
Game.ueberwunden.erase(str(muede.data["id"]))
|
||||||
|
if bang != null:
|
||||||
|
Game.ueberwunden.erase(str(bang.data["id"]))
|
||||||
|
schritte(80)
|
||||||
|
p(muede != null and muede._ober.rotation.x < -0.25,
|
||||||
|
"Erschöpfung beugt den Oberkörper (%.2f)" % (muede._ober.rotation.x if muede else 0.0))
|
||||||
|
p(bang != null and bang._ober.scale.x < 0.9,
|
||||||
|
"Angst macht klein (%.2f)" % (bang._ober.scale.x if bang else 0.0))
|
||||||
|
p(frei != null and absf(frei._ober.rotation.x) < 0.05 and frei._ober.scale.x > 0.98,
|
||||||
|
"Ohne diese Klemmen steht man aufrecht")
|
||||||
|
if muede != null:
|
||||||
|
Game.ueberwunden[str(muede.data["id"])] = {"erschoepfung": true}
|
||||||
|
schritte(200)
|
||||||
|
p(muede._ober.rotation.x > -0.05, "Überwunden richtet er sich auf (%.2f)" % muede._ober.rotation.x)
|
||||||
|
Game.ueberwunden = merker
|
||||||
|
|
||||||
|
# Hüte nach Beruf
|
||||||
|
var kappen: Dictionary = {}
|
||||||
|
for m in welt().humans:
|
||||||
|
var typ := str(HumanDB.BERUF_TAGESTYP.get(str(m.data.get("beruf", "")), ""))
|
||||||
|
kappen[typ] = m._ober.get_child_count()
|
||||||
|
p(kappen.has("heim") and kappen.has("frueh") and kappen.has("buero")
|
||||||
|
and int(kappen["frueh"]) > int(kappen["heim"]) and int(kappen["buero"]) > int(kappen["heim"]),
|
||||||
|
"Berufe tragen verschiedene Kopfbedeckungen (%s)" % str(kappen))
|
||||||
|
p(mensch._arme.size() == 2, "Jeder hat zwei Arme")
|
||||||
|
sp.velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _xz(v: Vector3) -> Vector2:
|
||||||
|
return Vector2(v.x, v.z)
|
||||||
|
|
||||||
|
|
||||||
|
func test_kamera_und_etagen() -> void:
|
||||||
|
abschnitt("Feste Kamera, kleine Menschen, Stadt mit Stockwerken")
|
||||||
|
ui().schliessen()
|
||||||
|
var sp: Player = main.player
|
||||||
|
sp.set_mode(Player.Mode.GEIST)
|
||||||
|
sp.look_enabled = true
|
||||||
|
sp.folgt = null
|
||||||
|
p(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "Die Maus ist frei")
|
||||||
|
p(is_equal_approx(sp.arm.rotation.x, Player.PITCH) and sp.cam.get_parent() == sp.arm,
|
||||||
|
"Die Kamera schaut fest von schräg oben (%.0f°)" % rad_to_deg(-Player.PITCH))
|
||||||
|
sp.yaw = Player.CAM_YAW
|
||||||
|
var yaw_vorher: float = sp.yaw
|
||||||
|
var dreh := InputEventAction.new()
|
||||||
|
dreh.action = "drehen"
|
||||||
|
dreh.pressed = true
|
||||||
|
Input.parse_input_event(dreh)
|
||||||
|
for i in range(80):
|
||||||
|
await get_tree().physics_frame
|
||||||
|
p(absf(angle_difference(yaw_vorher + Player.DREHSCHRITT, sp.yaw)) < 0.05,
|
||||||
|
"Q dreht die Kamera um 90° (%.0f°)" % rad_to_deg(sp.yaw - yaw_vorher))
|
||||||
|
var rad := InputEventMouseButton.new()
|
||||||
|
rad.button_index = MOUSE_BUTTON_WHEEL_DOWN
|
||||||
|
rad.pressed = true
|
||||||
|
Input.parse_input_event(rad)
|
||||||
|
for i in range(150):
|
||||||
|
await get_tree().physics_frame
|
||||||
|
p(absf(sp.cam.position.z - float(Player.ZOOM[1])) < 0.5,
|
||||||
|
"Mausrad zoomt heraus (%.1f)" % sp.cam.position.z)
|
||||||
|
rad = InputEventMouseButton.new()
|
||||||
|
rad.button_index = MOUSE_BUTTON_WHEEL_UP
|
||||||
|
rad.pressed = true
|
||||||
|
Input.parse_input_event(rad)
|
||||||
|
for i in range(150):
|
||||||
|
await get_tree().physics_frame
|
||||||
|
p(absf(sp.cam.position.z - float(Player.ZOOM[0])) < 0.5,
|
||||||
|
"Und wieder heran (%.1f)" % sp.cam.position.z)
|
||||||
|
|
||||||
|
var mensch: Bewohner = welt().humans[0]
|
||||||
|
p(mensch._visual.scale.x < 0.85 and sp.body.scale.x < 0.7,
|
||||||
|
"Menschen und Geist sind kleiner (%.2f / %.2f)" % [mensch._visual.scale.x, sp.body.scale.x])
|
||||||
|
|
||||||
|
# Stockwerke
|
||||||
|
var mehr := 0
|
||||||
|
for key in HumanDB.PLACES:
|
||||||
|
if Orte.etagen(HumanDB.PLACES, key) > 1:
|
||||||
|
mehr += 1
|
||||||
|
p(mehr >= 12, "%d Gebäude haben mehrere Stockwerke" % mehr)
|
||||||
|
var oben: Bewohner = null
|
||||||
|
for m in welt().humans:
|
||||||
|
if m.etage(str(m.data["heim"])) > 0:
|
||||||
|
oben = m
|
||||||
|
break
|
||||||
|
p(oben != null, "Jemand wohnt über dem Erdgeschoss")
|
||||||
|
var heim := str(oben.data["heim"])
|
||||||
|
var e := oben.etage(heim)
|
||||||
|
var c := Orte.center(HumanDB.PLACES, heim)
|
||||||
|
var g: Array = HumanDB.PLACES[heim]["size"]
|
||||||
|
var t0 := Orte.treppe(HumanDB.PLACES, heim, 0)
|
||||||
|
var t1 := Orte.treppe(HumanDB.PLACES, heim, 1)
|
||||||
|
var im_haus: bool = absf(t0.x - c.x) < float(g[0]) * 0.5 and absf(t0.z - c.z) < float(g[1]) * 0.5 \
|
||||||
|
and absf(t1.x - c.x) < float(g[0]) * 0.5 and absf(t1.z - c.z) < float(g[1]) * 0.5
|
||||||
|
p(im_haus and is_equal_approx(t1.y, Orte.ETAGE), "Das Treppenhaus steht im Haus, ein Podest je Etage")
|
||||||
|
|
||||||
|
var uhr := Game.time_h
|
||||||
|
var zuhause := false
|
||||||
|
for i in range(3000):
|
||||||
|
schritte(1, 0.05, 2.0)
|
||||||
|
if oben.place == heim and oben._path.is_empty():
|
||||||
|
zuhause = true
|
||||||
|
break
|
||||||
|
p(zuhause and absf(oben.global_position.y - Orte.hoehe(e)) < 0.2,
|
||||||
|
"Nachts liegt %s in seiner Wohnung im %d. Stock (y = %.1f)" %
|
||||||
|
[oben.data["name"], e, oben.global_position.y])
|
||||||
|
var weg_stunde := -1.0
|
||||||
|
for r in oben.data["routine"]:
|
||||||
|
if str(r[1]) != heim:
|
||||||
|
weg_stunde = float(r[0]) + 0.5
|
||||||
|
break
|
||||||
|
var draussen := false
|
||||||
|
var tief := 99.0
|
||||||
|
for i in range(3000):
|
||||||
|
schritte(1, 0.05, weg_stunde)
|
||||||
|
tief = minf(tief, oben.global_position.y)
|
||||||
|
if _xz(oben.global_position).distance_to(_xz(c)) > float(g[0]) * 0.5 + 3.0:
|
||||||
|
draussen = absf(oben.global_position.y) < 0.3
|
||||||
|
break
|
||||||
|
p(draussen, "Auf die Straße kommt er über die Treppe wieder nach unten (y = %.1f)" %
|
||||||
|
oben.global_position.y)
|
||||||
|
|
||||||
|
# Etagenschnitt
|
||||||
|
var bau: Dictionary = {}
|
||||||
|
for b in welt()._bauten:
|
||||||
|
if (b["c"] as Vector3).distance_to(c) < 0.1:
|
||||||
|
bau = b
|
||||||
|
var n := Orte.etagen(HumanDB.PLACES, heim)
|
||||||
|
p(not bau.is_empty() and (bau["etagen"] as Array).size() == n,
|
||||||
|
"Sein Haus steht mit allen %d Etagen da" % n)
|
||||||
|
sp.global_position = c + Vector3(0, 1.4, 0)
|
||||||
|
sp.velocity = Vector3.ZERO
|
||||||
|
sp.yaw = Player.CAM_YAW
|
||||||
|
await get_tree().physics_frame
|
||||||
|
welt()._process(0.05)
|
||||||
|
var obere_weg := true
|
||||||
|
for f in range(1, n):
|
||||||
|
if (bau["etagen"][f] as Node3D).visible:
|
||||||
|
obere_weg = false
|
||||||
|
var wand_weg := 0
|
||||||
|
for w in bau["waende"][0]:
|
||||||
|
if not (w as Node3D).visible:
|
||||||
|
wand_weg += 1
|
||||||
|
p(obere_weg and not (bau["dach"][0] as Node3D).visible,
|
||||||
|
"Steht der Geist im Erdgeschoss, verschwinden Dach und obere Etagen")
|
||||||
|
p(wand_weg >= 2 and wand_weg < (bau["waende"][0] as Array).size(),
|
||||||
|
"Die Wände zur Kamera hin sind weg, die hinteren bleiben (%d von %d Teile)" %
|
||||||
|
[wand_weg, (bau["waende"][0] as Array).size()])
|
||||||
|
Input.action_press("steigen")
|
||||||
|
for i in range(60):
|
||||||
|
await get_tree().physics_frame
|
||||||
|
Input.action_release("steigen")
|
||||||
|
p(absf(sp.global_position.y - (1.4 + Orte.ETAGE)) < 0.1,
|
||||||
|
"Leertaste hebt den Geist genau ein Stockwerk (y = %.1f)" % sp.global_position.y)
|
||||||
|
welt()._process(0.05)
|
||||||
|
p((bau["etagen"][0] as Node3D).visible and (bau["etagen"][1] as Node3D).visible
|
||||||
|
and (n < 3 or not (bau["etagen"][2] as Node3D).visible),
|
||||||
|
"Im ersten Stock sieht man Erdgeschoss und ersten Stock")
|
||||||
|
sp.global_position = c + Vector3(float(g[0]) + 4.0, 1.4, 0)
|
||||||
|
await get_tree().physics_frame
|
||||||
|
welt()._process(0.05)
|
||||||
|
var alles := (bau["dach"][0] as Node3D).visible
|
||||||
|
for f in range(n):
|
||||||
|
if not (bau["etagen"][f] as Node3D).visible:
|
||||||
|
alles = false
|
||||||
|
for w in bau["waende"][0]:
|
||||||
|
if not (w as Node3D).visible:
|
||||||
|
alles = false
|
||||||
|
p(alles, "Draußen steht das Haus wieder ganz")
|
||||||
|
|
||||||
|
# Klick auf einen Menschen
|
||||||
|
sp.global_position = oben.global_position + Vector3(2.0, 1.4, 2.0)
|
||||||
|
sp.velocity = Vector3.ZERO
|
||||||
|
await get_tree().physics_frame
|
||||||
|
var schirm := sp.cam.unproject_position(oben.global_position + Vector3(0, 0.8, 0))
|
||||||
|
main._klick(schirm)
|
||||||
|
p(sp.folgt == oben, "Ein Klick auf einen Menschen hängt den Geist an ihn")
|
||||||
|
main._klick(schirm)
|
||||||
|
p(sp.folgt == null, "Noch ein Klick löst ihn wieder")
|
||||||
|
Game.time_h = uhr
|
||||||
|
|
||||||
|
|
||||||
## Bei hohem Tempo wird der Schritt je Bild größer als die Ankunftsschwelle.
|
## Bei hohem Tempo wird der Schritt je Bild größer als die Ankunftsschwelle.
|
||||||
## Wer dann über den Wegpunkt hinausläuft, pendelt um ihn herum statt anzukommen.
|
## Wer dann über den Wegpunkt hinausläuft, pendelt um ihn herum statt anzukommen.
|
||||||
func test_gehen_bei_tempo() -> void:
|
func test_gehen_bei_tempo() -> void:
|
||||||
|
|||||||
Reference in New Issue
Block a user