This commit is contained in:
Team3
2026-09-16 12:08:04 +02:00
parent 936773c4e9
commit 89fad5dc7a
12 changed files with 493 additions and 167 deletions

View File

@@ -34,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.
Oben links stehen nur Zeit und Mission. Alles Weitere steht im Journal.
In der Traumwelt springt **Leertaste**; auf der Erde wechselt sie ein
Stockwerk nach oben, **Umschalt** eines nach unten.
In der Traumwelt springt **Leertaste**. Auf der Erde gibt es keinen Sprung:
der Geist nimmt die Treppe.
## Spielschleife
@@ -172,14 +172,17 @@ nicht immer — Übung entscheidet.
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
* Du bist ein unsichtbarer Geist, der eine Handbreit über dem Boden schwebt,
sich aber bewegt wie ein Mensch: Wände halten, Türen und Treppen sind die
Wege, Schwerkraft gilt. 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
und über den ganzen Tag, ohne ihm nachlaufen zu müssen. **F** noch einmal —
oder ein Schritt mit **WASD** — lässt ihn wieder los.
* **F** oder ein Klick hängt dich an einen Menschen: du gehst seiner Spur
nach, durch dieselben Türen und Treppen, so schnell wie nötig — auch bei
Tempo 3, wo du ihm zu Fuß nie folgen könntest. Bleibst du hängen, zieht die
Leine dich nach. **F** noch einmal — oder ein Schritt mit **WASD** — lässt
ihn wieder los.
* **R notieren.** Was den Traum aufhält, steht nirgends geschrieben. Du siehst
nur, was der Mensch tut, und musst selbst entscheiden: läuft hier etwas
schief? Triffst du den Moment, steht die Ursache im Journal und der Slot
@@ -221,7 +224,7 @@ eintauschen könnte. Ein Erfolg bringt 2 EP, ein Fehlversuch 1 — auch der zäh
scripts/orte.gd Orte, Türen, Wege, Etagen, Treppenhaus (beide Welten)
scripts/bewohner.gd Bewohner mit Tagesablauf, Hut nach Beruf, Haltung nach Klemme
scripts/main.gd Weltenwechsel, Fokus, Interaktion
scripts/player.gd Geist: feste Kamera, Etagenwechsel, Schleier, Funken, Verfolgung
scripts/player.gd Geist: feste Kamera, Schwerkraft, Spur-Leine, Schleier, Funken
scripts/dream_world.gd Insel mit Dorf
scripts/earth_world.gd Stadt mit Stockwerken, Treppen, Etagenschnitt, Tag-Nacht-Wechsel
scripts/ursachen.gd Ursachen, blockierende Tätigkeiten, Belohnungen

View File

@@ -28,6 +28,7 @@ var _path: Array = []
var _letzte_aktivitaet: String = ""
var boden: Callable = Callable() # Höhe des Bodens (x, z) -> y; leer heißt eben
var slot: int = 0
var spur: Array = [] # letzte Positionen -- der Geist folgt ihr durch Türen
var _bob: float = 0.0
var _offset: Vector3
@@ -87,33 +88,19 @@ func setup(d: Dictionary, index: int, orte: Dictionary,
global_position = Orte.center(places, place) + _offset + Vector3(0, Orte.hoehe(etage(place)), 0)
## Punkt auf der Spur, der mindestens `abstand` hinter dem Menschen liegt.
func spur_punkt(abstand: float) -> Vector3:
for i in range(spur.size() - 1, -1, -1):
if (spur[i] as Vector3).distance_to(global_position) >= abstand:
return spur[i]
return spur[0] if not spur.is_empty() else global_position
## 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:
@@ -180,10 +167,10 @@ func _process(roh: float) -> void:
slot = int(soll[2])
if soll[0] != place:
var war_unterwegs := not _path.is_empty()
_path = Orte.route(places, place, soll[0], _offset, netz, anker)
_path = Orte.route_voll(places, place, soll[0], _offset,
data.get("etage", {}), netz, anker, war_unterwegs)
if war_unterwegs and _path.size() > 1:
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
_path = _mit_etagen(_path, place, soll[0], war_unterwegs)
place = soll[0]
activity = "unterwegs"
elif _path.is_empty() and activity != soll[1]:
@@ -213,6 +200,10 @@ func _process(roh: float) -> void:
_haltung(delta, not _path.is_empty())
if boden.is_valid():
global_position.y = float(boden.call(global_position.x, global_position.z))
if spur.is_empty() or (spur.back() as Vector3).distance_to(global_position) > 0.4:
spur.append(global_position)
if spur.size() > 120:
spur.pop_front()
_visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0,
1.0 - pow(0.01, delta))

View File

@@ -122,10 +122,12 @@ func _orte() -> void:
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))
Orte.DIR_VEC[pl["dir"]] if f == 0 else Vector3.ZERO, tuer, f == 0))
if f > 0:
_decke(eb, key, cf, groesse, farbe.darkened(0.35))
_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_")
else "heim_", key)
if f + 1 < n:
_treppe(eb, key, f)
var licht := OmniLight3D.new()
@@ -159,51 +161,75 @@ func _fenster(parent: Node, cf: Vector3, g: Vector2, ton: Color) -> void:
glas, false)
## Eine Rampe von Podest zu Podest -- die Treppe, die die Bewohner hochgehen.
## Ein Lauf von Podest zu Podest -- die Treppe, die Bewohner und Geist
## hochgehen. Sie trägt, deshalb mit Kollision.
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)
var lauf := Orte.treppen_lauf(HumanDB.PLACES, key, f)
var von: Vector3 = lauf[0]
var bis: Vector3 = lauf[1]
var d := bis - von
var stufe := Prims.box(parent, (von + bis) * 0.5, Vector3(1.6, 0.18, d.length()),
Color(0.55, 0.50, 0.46))
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)
var r := Orte.treppen_rahmen(HumanDB.PLACES, key)
var podest := Orte.treppe(HumanDB.PLACES, key, f + 1)
var mass := Orte._xz(r["a"], Orte.PODEST * 2.0, Orte.TREPPE_TIEFE - 0.8)
Prims.box(parent, podest + Vector3(0, -0.09, 0), Vector3(mass.x, 0.18, mass.y),
Color(0.55, 0.50, 0.46))
## Ein paar Möbel, damit man sieht, wofür ein Raum da ist.
func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
## Der Boden einer oberen Etage lässt über dem Treppenhaus ein Loch frei --
## sonst stieße, wer die Treppe hochkommt, mit dem Kopf an die Decke.
func _decke(parent: Node, key: String, cf: Vector3, _groesse: Vector3, farbe: Color) -> void:
for platte in Orte.treppen_decke(HumanDB.PLACES, key):
var mitte: Vector3 = platte[0]
var mass: Vector2 = platte[1]
mitte.y = cf.y - 0.1
Prims.box(parent, mitte, Vector3(mass.x, 0.2, mass.y), farbe)
## Ein paar Möbel, damit man sieht, wofür ein Raum da ist. Nichts steht im
## Treppenstreifen.
func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String,
ort: String = "") -> void:
if ort == "":
ort = key
var w := maxf(0.5, groesse.x * 0.5 - 2.0)
var h := maxf(0.5, groesse.z * 0.5 - 2.0)
var frei := func(v: Vector3, halb: float) -> Vector3:
var p := Orte.frei_von_treppe(HumanDB.PLACES, ort, c + v, halb)
p.y = c.y + v.y
return p
if key.begins_with("heim_"):
Prims.box(parent, c + Vector3(-w * 0.6, 0.35, -h * 0.5), Vector3(1.9, 0.7, 3.0),
Prims.box(parent, frei.call(Vector3(-w * 0.6, 0.35, -h * 0.5), 1.5), Vector3(1.9, 0.7, 3.0),
Color(0.85, 0.85, 0.90))
Prims.box(parent, c + Vector3(w * 0.6, 0.75, 0), Vector3(1.5, 0.15, 2.2),
Prims.box(parent, frei.call(Vector3(w * 0.6, 0.75, 0), 1.1), Vector3(1.5, 0.15, 2.2),
farbe.darkened(0.5))
Prims.box(parent, c + Vector3(w * 0.6, 1.05, 0), Vector3(0.8, 0.55, 0.1),
Prims.box(parent, frei.call(Vector3(w * 0.6, 1.05, 0), 0.1), Vector3(0.8, 0.55, 0.1),
Color(0.2, 0.25, 0.3))
elif key == "cafe":
for i in range(3):
Prims.cyl(parent, c + Vector3(-w * 0.5 + float(i) * w * 0.5, 0.4, 0.4), 0.7, 0.8,
Prims.cyl(parent, frei.call(Vector3(-w * 0.5 + float(i) * w * 0.5, 0.4, 0.4), 0.7), 0.7, 0.8,
Color(0.55, 0.40, 0.30))
Prims.box(parent, c + Vector3(0, 0.55, -h * 0.7), Vector3(groesse.x - 3.0, 1.1, 0.8),
Prims.box(parent, frei.call(Vector3(0, 0.55, -h * 0.7), 0.4), Vector3(groesse.x - 3.0, 1.1, 0.8),
Color(0.45, 0.32, 0.24))
elif key == "buero":
for i in range(3):
Prims.box(parent, c + Vector3(-w * 0.5 + float(i) * w * 0.5, 0.75, 0),
Prims.box(parent, frei.call(Vector3(-w * 0.5 + float(i) * w * 0.5, 0.75, 0), 0.8),
Vector3(1.6, 0.15, 1.2), Color(0.75, 0.75, 0.78))
elif key == "baeckerei":
Prims.box(parent, c + Vector3(0, 0.55, h * 0.5), Vector3(groesse.x - 3.0, 1.1, 1.0),
Prims.box(parent, frei.call(Vector3(0, 0.55, h * 0.5), 0.5), Vector3(groesse.x - 3.0, 1.1, 1.0),
Color(0.70, 0.55, 0.35))
Prims.box(parent, c + Vector3(0, 0.85, -h * 0.6), Vector3(2.6, 1.7, 1.2),
Prims.box(parent, frei.call(Vector3(0, 0.85, -h * 0.6), 1.3), Vector3(2.6, 1.7, 1.2),
Color(0.4, 0.4, 0.42))
elif key == "uni":
for i in range(3):
Prims.box(parent, c + Vector3(0, 0.5, -h * 0.6 + float(i) * h * 0.6),
Prims.box(parent, frei.call(Vector3(0, 0.5, -h * 0.6 + float(i) * h * 0.6), 0.35),
Vector3(groesse.x - 3.0, 0.2, 0.7), Color(0.66, 0.62, 0.58))
Prims.box(parent, c + Vector3(0, 1.1, -h), Vector3(groesse.x * 0.55, 1.6, 0.15),
Prims.box(parent, frei.call(Vector3(0, 1.1, -h), 0.1), Vector3(groesse.x * 0.55, 1.6, 0.15),
Color(0.15, 0.25, 0.20))
elif key == "werkstatt":
Prims.box(parent, c + Vector3(0, 0.6, 0), Vector3(groesse.x - 4.0, 1.2, 1.4),
Prims.box(parent, frei.call(Vector3(0, 0.6, 0), 0.7), Vector3(groesse.x - 4.0, 1.2, 1.4),
Color(0.52, 0.48, 0.46))

View File

@@ -59,7 +59,6 @@ func _setup_input() -> void:
"links": [KEY_A, KEY_LEFT],
"rechts": [KEY_D, KEY_RIGHT],
"steigen": [KEY_SPACE],
"sinken": [KEY_SHIFT],
"interagieren": [KEY_E],
"folgen": [KEY_F],
"notieren": [KEY_R],

View File

@@ -388,7 +388,9 @@ static func _sonderbauten_verteilen() -> void:
# Das Café gehört in die Mitte -- jeder Tagesablauf führt dorthin.
# Die übrigen Sonderbauten verteilen sich rundum.
var wunsch := Vector3.ZERO
if str(SONDER[s][0]) != "cafe":
if str(SONDER[s][0]) == "werkstatt":
wunsch = Vector3(0, 0, 30.0) # kurze Mittagspause: das Café muss nah sein
elif str(SONDER[s][0]) != "cafe":
var w := TAU * float(s) / float(SONDER.size() - 1)
wunsch = Vector3(cos(w), 0, sin(w)) * 42.0
var idx: int = -1

View File

@@ -59,9 +59,13 @@ 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:
const TREPPE_LAUF := 6.0 # Abstand der Wendepodeste entlang der Wand
const TREPPE_TIEFE := 4.4 # Streifen an der Treppenwand: zwei Läufe nebeneinander
const PODEST := 0.9 # halbe Podestlänge
## Rahmen des Treppenhauses: an der längeren Wand, die nicht die Türwand ist.
## n zeigt von der Mitte zur Treppenwand, a läuft an ihr entlang.
static func treppen_rahmen(places: Dictionary, key: String) -> Dictionary:
var pl: Dictionary = places[key]
var c := center(places, key)
var d: Vector3 = DIR_VEC[pl["dir"]]
@@ -70,11 +74,98 @@ static func treppe(places: Dictionary, key: String, e: int) -> Vector3:
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)
if halb_q >= halb_d:
return {"c": c, "n": -d, "a": quer, "halb_n": halb_d, "halb_a": halb_q}
return {"c": c, "n": -quer, "a": -d, "halb_n": halb_q, "halb_a": halb_d}
static func _lauf(r: Dictionary) -> float:
return minf(TREPPE_LAUF, float(r["halb_a"]) * 2.0 - 2.8)
static func _podest_a(r: Dictionary, e: int) -> float:
var p0: float = float(r["halb_a"]) - 1.4
return p0 if e % 2 == 0 else p0 - _lauf(r)
## Wendepodest der Etage e: zwei Läufe liegen nebeneinander, das Podest
## verbindet sie. Der obere Lauf ist so nie die Decke des unteren.
static func treppe(places: Dictionary, key: String, e: int) -> Vector3:
var r := treppen_rahmen(places, key)
var p: Vector3 = r["c"] + r["n"] * (float(r["halb_n"]) - TREPPE_TIEFE * 0.5) \
+ r["a"] * _podest_a(r, e)
p.y = hoehe(e)
return p
## Ein Lauf: [Anfang auf Etage f, Ende auf Etage f + 1].
static func treppen_lauf(places: Dictionary, key: String, f: int) -> Array:
var r := treppen_rahmen(places, key)
var tiefe: float = float(r["halb_n"]) - (1.4 if f % 2 == 0 else 3.0)
var richtung := -1.0 if f % 2 == 0 else 1.0
var von: Vector3 = r["c"] + r["n"] * tiefe + r["a"] * (_podest_a(r, f) + richtung * PODEST)
var bis: Vector3 = r["c"] + r["n"] * tiefe + r["a"] * (_podest_a(r, f + 1) - richtung * PODEST)
von.y = hoehe(f)
bis.y = hoehe(f + 1)
return [von, bis]
## Wegpunkte durchs Treppenhaus von Etage a nach Etage b, ohne a selbst:
## Laufanfang, Laufende, Podest -- je Etage.
static func treppen_weg(places: Dictionary, key: String, a: int, b: int) -> Array:
var out: Array = []
if b > a:
for f in range(a, b):
var l := treppen_lauf(places, key, f)
out.append(l[0])
out.append(l[1])
out.append(treppe(places, key, f + 1))
else:
for f in range(a - 1, b - 1, -1):
var l := treppen_lauf(places, key, f)
out.append(l[1])
out.append(l[0])
out.append(treppe(places, key, f))
return out
## Deckenplatten einer oberen Etage: alles außer dem Treppenloch.
## Liefert [[mitte, groesse_xz], ...] ohne Höhe.
static func treppen_decke(places: Dictionary, key: String) -> Array:
var r := treppen_rahmen(places, key)
var c: Vector3 = r["c"]
var n: Vector3 = r["n"]
var a: Vector3 = r["a"]
var halb_n := float(r["halb_n"])
var halb_a := float(r["halb_a"])
var out: Array = []
# Hauptplatte: alles außer dem Streifen an der Treppenwand
out.append([c - n * (TREPPE_TIEFE * 0.5), _xz(a, halb_a * 2.0, halb_n * 2.0 - TREPPE_TIEFE)])
# Rest des Streifens jenseits des Lochs
var ende: float = _podest_a(r, 1) - PODEST
var rest := ende + halb_a
if rest > 0.3:
out.append([c + n * (halb_n - TREPPE_TIEFE * 0.5) + a * ((ende - halb_a) * 0.5),
_xz(a, rest, TREPPE_TIEFE)])
return out
## Kastenmaß in Weltachsen: laenge_a entlang a, laenge_n quer dazu.
static func _xz(a: Vector3, laenge_a: float, laenge_n: float) -> Vector2:
return Vector2(laenge_a, laenge_n) if absf(a.x) > 0.0 else Vector2(laenge_n, laenge_a)
## Rückt einen Punkt aus dem Treppenstreifen heraus (für Möbel).
static func frei_von_treppe(places: Dictionary, key: String, pos: Vector3, halb: float) -> Vector3:
var r := treppen_rahmen(places, key)
var n: Vector3 = r["n"]
var pn: float = (pos - r["c"]).dot(n)
var grenze: float = float(r["halb_n"]) - TREPPE_TIEFE - halb - 0.2
if pn > grenze:
pos -= n * (pn - grenze)
return pos
## Punkt gleich hinter der Tür, im Haus.
static func innen_tuer(places: Dictionary, key: String) -> Vector3:
var pl: Dictionary = places[key]
@@ -85,15 +176,42 @@ static func innen_tuer(places: Dictionary, key: String) -> Vector3:
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
## Ein Ort ist ein Gebäude, wenn er Grundriss und Tür hat.
static func gebaeude(places: Dictionary, key: String) -> bool:
return places.has(key) and float(places[key]["size"][0]) > 0.0 \
and DIR_VEC[places[key]["dir"]] != Vector3.ZERO
## Route mit allem, was ein Körper braucht: aus der Etage runter, gerade durch
## die Tür, über das Netz, durch die nächste Tür und die Treppe hinauf.
## `etagen` sagt, in welcher Etage der Mensch an welchem Ort ist.
static func route_voll(places: Dictionary, a: String, b: String, offset: Vector3,
etagen: Dictionary, netz: Netz = null, anker: Dictionary = {},
war_unterwegs: bool = false) -> Array:
var pts := route(places, a, b, offset, netz, anker)
if pts.is_empty():
return pts
if gebaeude(places, a) and not war_unterwegs:
var e_von := int(etagen.get(a, 0))
var runter: Array = []
if e_von > 0:
runter.append(treppe(places, a, e_von))
runter.append_array(treppen_weg(places, a, e_von, 0))
runter.append(innen_tuer(places, a))
runter.append(door(places, a))
runter.append_array(pts)
pts = runter
if gebaeude(places, b):
var e_nach := int(etagen.get(b, 0))
var ziel: Vector3 = pts.pop_back()
pts.append(door(places, b))
pts.append(innen_tuer(places, b))
if e_nach > 0:
pts.append(treppe(places, b, 0))
pts.append_array(treppen_weg(places, b, 0, e_nach))
ziel.y = hoehe(e_nach)
pts.append(ziel)
return pts
static func center(places: Dictionary, key: String) -> Vector3:

View File

@@ -9,8 +9,9 @@ const SPEED_TRAUM := 8.0
const SPEED_GEIST := 11.0
const GRAVITY := 24.0
const JUMP := 9.0
const FOLGE_ABSTAND := 2.2
const FOLGE_SPIEL := 1.5
const FOLGE_ABSTAND := 2.5 # so weit bleibt der Geist auf der Spur zurück
const LEINE := 14.0 # weiter weg reißt die Leine: er wird nachgezogen
const SCHWEBE := 0.25 # eine Handbreit über dem Boden
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
@@ -27,7 +28,6 @@ var arm: Node3D
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 _schleier: MeshInstance3D
var _kern: MeshInstance3D
@@ -51,10 +51,10 @@ const FUNKEN_MAX := 6
func _ready() -> void:
var cs := CollisionShape3D.new()
var shape := CapsuleShape3D.new()
shape.radius = 0.45
shape.height = 1.9
shape.radius = 0.4
shape.height = 1.6
cs.shape = shape
cs.position.y = 0.95
cs.position.y = 0.8
add_child(cs)
body = Node3D.new()
@@ -107,18 +107,15 @@ func _ready() -> void:
func set_mode(m: int) -> void:
mode = m
folgt = null
collision_layer = 1
collision_mask = 1
if m == Mode.GEIST:
collision_layer = 0
collision_mask = 0
body.scale = Vector3.ONE * 0.6
_schleier_mat.albedo_color.a = 0.3
else:
collision_layer = 1
collision_mask = 1
body.scale = Vector3.ONE * 0.8
_schleier_mat.albedo_color.a = 0.55
velocity = Vector3.ZERO
_schwebe_ziel = -1.0
func _unhandled_input(event: InputEvent) -> void:
@@ -157,56 +154,16 @@ func _physics_process(delta: float) -> void:
elif dir.length() > 0.01:
folgt = null # eigene Bewegung löst die Verfolgung
else:
var rechts := Basis(Vector3.UP, yaw) * Vector3(1, 0, 0)
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))
# weich nachziehen, aber an der Leine: sonst reißt der Faden beim Gehen
var ab := global_position - schulter
if ab.length() > FOLGE_SPIEL:
global_position = schulter + ab.normalized() * FOLGE_SPIEL
velocity = Vector3.ZERO
body.rotation.y = lerp_angle(body.rotation.y, yaw, 1.0 - pow(0.001, delta))
_an_der_leine(delta)
return
if mode == Mode.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:
var von: float = _schwebe_ziel if _schwebe_ziel >= 0.0 else global_position.y
if Input.is_action_just_pressed("steigen"):
_schwebe_ziel = clampf(von + Orte.ETAGE, 0.4, 45.0)
if Input.is_action_just_pressed("sinken"):
_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()
if global_position.y < 0.4:
global_position.y = 0.4
if global_position.y > 45.0:
global_position.y = 45.0
else:
velocity.x = lerpf(velocity.x, dir.x * SPEED_TRAUM, 1.0 - pow(0.0005, delta))
velocity.z = lerpf(velocity.z, dir.z * SPEED_TRAUM, 1.0 - pow(0.0005, delta))
if is_on_floor():
if look_enabled and Input.is_action_pressed("steigen"):
velocity.y = JUMP
else:
velocity.y = -1.0
else:
velocity.y -= GRAVITY * delta
move_and_slide()
if global_position.y < -400.0:
global_position.y = 2.0
var tempo := SPEED_GEIST if mode == Mode.GEIST else SPEED_TRAUM
velocity.x = lerpf(velocity.x, dir.x * tempo, 1.0 - pow(0.0005, delta))
velocity.z = lerpf(velocity.z, dir.z * tempo, 1.0 - pow(0.0005, delta))
_fallen(delta)
move_and_slide()
if global_position.y < -400.0:
global_position.y = 2.0
if dir.length() > 0.01:
var ziel := atan2(-dir.x, -dir.z)
@@ -217,6 +174,40 @@ func blickrichtung() -> Vector3:
return -Vector3(sin(yaw), 0, cos(yaw))
## Schwerkraft in beiden Welten. Springen kann nur der Läufer im Traum.
func _fallen(delta: float) -> void:
if is_on_floor():
if mode == Mode.TRAUM and look_enabled and Input.is_action_pressed("steigen"):
velocity.y = JUMP
else:
velocity.y = -1.0
else:
velocity.y -= GRAVITY * delta
## Folgen heißt: seiner Spur nachgehen, durch dieselben Türen und Treppen.
## Er läuft so schnell wie nötig, um dranzubleiben -- auch bei hohem Tempo.
func _an_der_leine(delta: float) -> void:
var mensch := folgt as Bewohner
var ziel: Vector3 = mensch.spur_punkt(FOLGE_ABSTAND)
var d := ziel - global_position
d.y = 0.0
var tempo := maxf(SPEED_GEIST, Bewohner.GEHTEMPO * Game.zeit_faktor() * 1.4)
if d.length() > 0.3:
var v := d.normalized() * minf(tempo, d.length() / delta)
velocity.x = v.x
velocity.z = v.z
body.rotation.y = lerp_angle(body.rotation.y, atan2(-d.x, -d.z), 1.0 - pow(0.001, delta))
else:
velocity.x = 0.0
velocity.z = 0.0
_fallen(delta)
move_and_slide()
if global_position.distance_to(mensch.global_position) > LEINE:
global_position = ziel # hängen geblieben: die Leine zieht ihn nach
velocity = Vector3.ZERO
## Schweben, Funken, Schweif und Modusfarbe -- der Zustand ist am Geist ablesbar.
func _beleben(delta: float) -> void:
var t := Time.get_ticks_msec() * 0.001
@@ -233,7 +224,7 @@ func _beleben(delta: float) -> void:
_kopf_mat.albedo_color = _farbe.lightened(0.4)
_kopf_mat.emission = _farbe.lightened(0.4)
body.position.y = sin(t * 1.6) * 0.08
body.position.y = (SCHWEBE if mode == Mode.GEIST else 0.0) + 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)

91
tests/_probe.gd Normal file
View File

@@ -0,0 +1,91 @@
extends Node
var main: Node3D
func _ready() -> void:
seed(20240)
main = load("res://scenes/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
main.ui.schliessen()
main._erde()
await get_tree().process_frame
# B: engste Wege mit Türen und Treppen
var liste: Array = []
for m in HumanDB.LIST:
var r: Array = m["routine"]
for i in range(r.size()):
var von: String = String(r[i - 1][1]) if i > 0 else String(r[r.size() - 1][1])
var nach: String = String(r[i][1])
if von == nach:
continue
var pts: Array = Orte.route_voll(HumanDB.PLACES, von, nach, Vector3.ZERO, m.get("etage", {}), HumanDB.NETZ, HumanDB.ANKER)
var l := 0.0
var pos := Orte.center(HumanDB.PLACES, von) + Vector3(0, Orte.hoehe(int(m.get("etage", {}).get(von, 0))), 0)
for pt in pts:
l += pos.distance_to(pt)
pos = pt
var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() else (24.0 - float(r[i][0]))
liste.append([slot_h * 15.0 - l / 8.0, "%s %s->%s %.1fs / %.0fs" % [m["name"], von, nach, l / 8.0, slot_h * 15.0]])
liste.sort_custom(func(a, b): return a[0] < b[0])
for k in range(6):
print("ENG %s" % liste[k][1])
# C: Treppe hoch, was blockiert?
var sp: Player = main.player
var oben: Bewohner = null
for m in main.welt.humans:
if m.etage(str(m.data["heim"])) > 0:
oben = m
break
var heim := str(oben.data["heim"])
var pl: Dictionary = HumanDB.PLACES[heim]
print("HAUS %s size=%s dir=%s etagen=%d" % [heim, str(pl["size"]), pl["dir"], Orte.etagen(HumanDB.PLACES, heim)])
var t1 := Orte.treppe(HumanDB.PLACES, heim, 1)
var stufen: Array = Orte.treppen_lauf(HumanDB.PLACES, heim, 0)
sp.global_position = (stufen[0] as Vector3) + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
var lauf: Vector3 = stufen[1] - stufen[0]
lauf.y = 0.0
sp.yaw = atan2(-lauf.x, -lauf.z)
Input.action_press("vor")
var hoechste := 0.0
for i in range(70):
await get_tree().physics_frame
hoechste = maxf(hoechste, sp.global_position.y)
if i % 10 == 9 or i == 69:
var k := sp.get_last_slide_collision()
var txt := "-"
if k != null and k.get_collider() != null:
var body: Node = k.get_collider()
var mi: Node = body.get_parent()
txt = "%s pos=%s size=%s normal=%s" % [mi.name, str(mi.global_position), str((mi as MeshInstance3D).mesh.get("size")) if mi is MeshInstance3D else "?", str(k.get_normal())]
print("TREPPE f=%d pos=%s y=%.2f koll=%s" % [i, str(sp.global_position), sp.global_position.y, txt])
Input.action_release("vor")
print("TREPPE hoechste=%.2f t1=%s" % [hoechste, str(t1)])
# E: Verfolgung -- wo reißt die Leine?
var mensch: Bewohner = main.welt.humans[0]
sp.global_position = mensch.global_position + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
sp.folgt = mensch
var weiteste := 0.0
for i in range(2400):
await get_tree().physics_frame
var d := sp.global_position.distance_to(mensch.global_position)
if d > weiteste:
weiteste = d
if d > 6.0 and i % 5 == 0:
var k := sp.get_last_slide_collision()
var txt := "-"
if k != null and k.get_collider() != null:
var mi: Node = (k.get_collider() as Node).get_parent()
txt = "%s pos=%s" % [mi.name, str(mi.global_position)]
print("LEINE f=%d d=%.1f geist=%s mensch=%s ort=%s pfad=%d koll=%s" % [i, d, str(sp.global_position), str(mensch.global_position), mensch.place, mensch._path.size(), txt])
print("LEINE weiteste=%.1f" % weiteste)
# D: wieder anhängen
sp.folgt = null
sp.global_position = mensch.global_position + Vector3(0, 1.5, 2.0)
sp.velocity = Vector3.ZERO
main._process(0.05)
print("ANHAENGEN fokus_mensch=%s fokus=%s d=%.2f geist=%s mensch=%s auf_erde=%s offen=%s" % [
str(main._fokus_mensch), str(main._fokus), sp.global_position.distance_to(mensch.global_position + Vector3(0, 1, 0)),
str(sp.global_position), str(mensch.global_position), str(main.auf_erde), str(main.ui.offen)])
get_tree().quit()

6
tests/_probe.tscn Normal file
View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_probe.gd" id="1_p"]
[node name="Probe" type="Node"]
script = ExtResource("1_p")

54
tests/_probe2.gd Normal file
View File

@@ -0,0 +1,54 @@
extends Node
var main: Node3D
const OUT := "user://treppe/"
func _ready() -> void:
seed(7)
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUT))
main = load("res://scenes/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
main.ui.schliessen()
Game.time_h = 11.0
main._erde()
await get_tree().process_frame
var sp: Player = main.player
var oben: Bewohner = null
for m in main.welt.humans:
if m.etage(str(m.data["heim"])) > 0 and Orte.etagen(HumanDB.PLACES, str(m.data["heim"])) >= 3:
oben = m
break
var heim := str(oben.data["heim"])
# 1 Erdgeschoss, am Fuß der Treppe
var t0 := Orte.treppe(HumanDB.PLACES, heim, 0)
var innen := Orte.innen_tuer(HumanDB.PLACES, heim)
sp.global_position = innen + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
await knipse("1_erdgeschoss_treppe")
# 2 auf dem Wendepodest im ersten Stock
sp.global_position = Orte.treppe(HumanDB.PLACES, heim, 1) + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
await knipse("2_podest_erster_stock")
# 3 Leine auf der Straße: an jemanden hängen, der unterwegs ist
var laeufer: Bewohner = null
for i in range(3000):
await get_tree().process_frame
for m in main.welt.humans:
if m._path.size() > 3 and m.etage(m.place) == 0:
laeufer = m
break
if laeufer != null:
break
sp.global_position = laeufer.global_position + Vector3(0, 0.3, 0)
sp.folgt = laeufer
for i in range(240):
await get_tree().process_frame
await knipse("3_leine_strasse")
get_tree().quit()
func knipse(name: String) -> void:
for i in range(30):
await get_tree().process_frame
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png(OUT + name + ".png")
print("Bild: " + name)

6
tests/_probe2.tscn Normal file
View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/_probe2.gd" id="1_p"]
[node name="Probe" type="Node"]
script = ExtResource("1_p")

View File

@@ -760,7 +760,8 @@ func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array,
var nach: String = String(r[i][1])
if von == nach:
continue
var dauer := _weg_laenge(places, von, nach, netz, anker) / Bewohner.GEHTEMPO
var dauer := _weg_laenge(places, von, nach, m.get("etage", {}), netz, anker) \
/ Bewohner.GEHTEMPO
var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() \
else (24.0 - float(r[i][0]))
var fenster := slot_h * sek_pro_stunde
@@ -775,11 +776,11 @@ func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array,
p(alles_ok, "%s — jeder Weg passt, engster Fall: %s" % [welt_name, engste_text])
func _weg_laenge(places: Dictionary, von: String, nach: String,
func _weg_laenge(places: Dictionary, von: String, nach: String, etagen: Dictionary,
netz: Netz = null, anker: Dictionary = {}) -> float:
var pts: Array = Orte.route(places, von, nach, Vector3.ZERO, netz, anker)
var pts: Array = Orte.route_voll(places, von, nach, Vector3.ZERO, etagen, netz, anker)
var l := 0.0
var pos := Orte.center(places, von)
var pos := Orte.center(places, von) + Vector3(0, Orte.hoehe(int(etagen.get(von, 0))), 0)
for pt in pts:
l += pos.distance_to(pt)
pos = pt
@@ -789,15 +790,14 @@ func _weg_laenge(places: Dictionary, von: String, nach: String,
func test_geist() -> void:
abschnitt("Geist-Bewegung")
p(main.player.mode == Player.Mode.GEIST, "Spieler ist auf der Erde ein Geist")
p(main.player.collision_layer == 0 and main.player.collision_mask == 0,
"Der Geist kollidiert mit nichts — er schwebt durch Wände")
var vor: Vector3 = main.player.global_position
p(main.player.collision_mask == 1, "Der Geist stößt an Wände wie ein Mensch")
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3(6, 0, 0)
for i in range(30):
for i in range(60):
await get_tree().physics_frame
p(main.player.global_position.x > 0.5, "Der Geist bewegt sich frei durch den Raum")
p(absf(main.player.global_position.y - 8.0) < 0.5, "Ohne Schwerkraft fällt er nicht")
p(main.player.global_position.x > 0.5, "Der Geist bewegt sich durch den Park")
p(main.player.global_position.y < 0.6 and main.player.is_on_floor(),
"Die Schwerkraft holt ihn auf den Boden (y = %.2f)" % main.player.global_position.y)
var lena: Bewohner = welt().humans[0]
var davor: Vector3 = lena.global_position
@@ -817,6 +817,8 @@ func test_charakter() -> void:
sp.set_mode(Player.Mode.GEIST)
sp.look_enabled = true
sp.folgt = null
var moment_merker: Dictionary = Game.situation.duplicate(true)
Game.situation = {} # ein laufender Moment ließe die Farbe flackern
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):
@@ -835,7 +837,11 @@ func test_charakter() -> void:
for i in range(90):
await get_tree().physics_frame
p(_farbabstand(sp._farbe, Player.FARBE_FOLGE) < 0.1, "Beim Verfolgen leuchtet er warm")
Game.situation = moment_merker
sp.folgt = null
sp.global_position = Vector3(0, 0.5, 9.0) # freier Rasen im Park
sp.velocity = Vector3.ZERO
sp.yaw = 0.0
Input.action_press("vor")
for i in range(40):
await get_tree().physics_frame
@@ -1016,12 +1022,23 @@ func test_kamera_und_etagen() -> void:
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):
var stufen: Array = Orte.treppen_lauf(HumanDB.PLACES, heim, 0)
sp.global_position = (stufen[0] as Vector3) + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
var lauf: Vector3 = stufen[1] - stufen[0]
lauf.y = 0.0
sp.yaw = atan2(-lauf.x, -lauf.z)
Input.action_press("vor")
var hoechste := 0.0
for i in range(70):
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)
hoechste = maxf(hoechste, sp.global_position.y)
Input.action_release("vor")
p(hoechste > Orte.ETAGE - 0.3,
"Über die Treppe kommt der Geist in den ersten Stock (y = %.1f)" % hoechste)
sp.global_position = t1 + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
await get_tree().physics_frame
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),
@@ -1102,7 +1119,14 @@ func test_verfolgung() -> void:
main.player.yaw = 0.0
var mensch: Bewohner = welt().humans[0]
Game.active_mission = str(mensch.data["id"])
main.player.global_position = mensch.global_position + Vector3(0, 1.5, 2.0)
# kurz vor seinem nächsten Ortswechsel einsteigen, damit er auch losgeht
var routine: Array = mensch.data["routine"]
for k in range(routine.size()):
if str(routine[k][1]) != mensch.place and float(routine[k][0]) > 0.5:
Game.time_h = float(routine[k][0]) - 0.4
break
schritte(5)
main.player.global_position = mensch.global_position + Vector3(0, 0.3, 0)
main.player.velocity = Vector3.ZERO
main._process(0.05)
p(main._fokus_mensch == mensch, "Der Mensch ist im Fokus")
@@ -1114,9 +1138,8 @@ func test_verfolgung() -> void:
var weiteste := 0.0
var gelaufen := false
var im_fokus := 0
for i in range(900):
schritte(1)
await get_tree().physics_frame
for i in range(2400):
await get_tree().physics_frame # nur die Engine steppt: sonst laufen die Menschen doppelt
if not mensch._path.is_empty():
gelaufen = true
if main._fokus_mensch == mensch:
@@ -1124,8 +1147,8 @@ func test_verfolgung() -> void:
weiteste = maxf(weiteste,
main.player.global_position.distance_to(mensch.global_position))
p(gelaufen, "Er wechselt in dieser Zeit den Ort")
p(weiteste < 6.0, "Der Geist bleibt die ganze Zeit dicht dabei (höchstens %.1f)" % weiteste)
p(im_fokus > 850, "Und behält ihn im Fokus (%d von 900 Schritten)" % im_fokus)
p(weiteste < 8.0, "Der Geist bleibt die ganze Zeit dicht dabei (höchstens %.1f)" % weiteste)
p(im_fokus > 2000, "Und behält ihn im Fokus (%d von 2400 Schritten)" % im_fokus)
p(main._fokus_mensch == mensch or main.player.folgt == mensch,
"Unterwegs bleibt er notierbar")
@@ -2203,8 +2226,8 @@ func test_journal_ueberall() -> void:
func test_steuerung() -> void:
abschnitt("Steuerung")
var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "sinken",
"interagieren", "journal", "tempo_1", "tempo_2", "tempo_3"]
var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "drehen",
"interagieren", "folgen", "notieren", "journal", "tempo_1", "tempo_2", "tempo_3"]
var alle := true
for a in aktionen:
if not InputMap.has_action(a) or InputMap.action_get_events(a).is_empty():
@@ -2236,23 +2259,39 @@ func test_steuerung() -> void:
p(main.player.global_position.x > 1.0,
"D schiebt den Spieler nach rechts (x = %.1f)" % main.player.global_position.x)
main.player.global_position = Vector3(0, 8.0, 0)
# Ein geschlossener Klotz hält ihn auf: von der Straße aus gegen die Wand
var war_erde := main.auf_erde
if not war_erde:
main._erde()
await get_tree().process_frame
main.player.look_enabled = true
var klotz: Dictionary = {}
var richtung := Vector3.ZERO
for b in welt()._bauten:
if not bool(b.get("voll", false)):
continue
for pz in HumanDB.PARZELLEN:
if (pz["pos"] as Vector3).distance_to(b["c"]) < 0.1:
klotz = b
richtung = Orte.DIR_VEC[pz["dir"]]
if not klotz.is_empty():
break
p(not klotz.is_empty(), "Es gibt geschlossene Klötze")
var kc: Vector3 = klotz["c"]
var kg: Vector2 = klotz["size"]
var halb: float = (kg.y if absf(richtung.z) > 0.0 else kg.x) * 0.5
main.player.global_position = kc + richtung * (halb + 2.0) + Vector3(0, 0.3, 0)
main.player.velocity = Vector3.ZERO
Input.action_press("steigen")
for i in range(40):
main.player.yaw = atan2(richtung.x, richtung.z) # "vor" zeigt zum Klotz
Input.action_press("vor")
for i in range(60):
await get_tree().physics_frame
Input.action_release("steigen")
p(main.player.global_position.y > 9.0,
"Leertaste lässt den Geist steigen (y = %.1f)" % main.player.global_position.y)
main.player.velocity = Vector3.ZERO
var hoch: float = main.player.global_position.y
Input.action_press("sinken")
for i in range(40):
await get_tree().physics_frame
Input.action_release("sinken")
p(main.player.global_position.y < hoch - 1.0,
"Umschalt lässt den Geist sinken (y = %.1f)" % main.player.global_position.y)
Input.action_release("vor")
var tiefe_im_klotz: float = halb - (main.player.global_position - kc).dot(richtung)
p(tiefe_im_klotz < 0.6, "Wände halten den Geist auf (%.1f m in der Wand)" % maxf(0.0, tiefe_im_klotz))
if not war_erde:
main._traumwelt() # die folgenden Messungen erwarten die Traumwelt
await get_tree().process_frame
## Nach jedem Einsatz braucht eine Technik Ruhe. Ein Weg allein deckt daher