This commit is contained in:
Team3
2026-09-16 01:23:42 +02:00
parent 12cbb88692
commit 936773c4e9
13 changed files with 1300 additions and 314 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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