This commit is contained in:
Team3
2026-09-16 13:08:59 +02:00
parent 89fad5dc7a
commit e813b5f7aa
17 changed files with 483 additions and 247 deletions

View File

@@ -180,9 +180,13 @@ nicht immer — Übung entscheidet.
Etage ist, ist nicht im Fokus — durch die Decke sieht man nichts. Etage ist, ist nicht im Fokus — durch die Decke sieht man nichts.
* **F** oder ein Klick hängt dich an einen Menschen: du gehst seiner Spur * **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 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 Tempo 3, wo du ihm zu Fuß nie folgen könntest. Steht er weit weg, planst du
Leine dich nach. **F** noch einmal — oder ein Schritt mit **WASD** — lässt den Weg zu ihm über die Straßen, aus deinem Haus heraus und in seines
ihn wieder los. hinein. Bleibst du hängen, zieht die Leine dich nach; wer sich mit gedrückter
Taste zwei Sekunden lang nicht rührt, steht wieder auf der Straße. **F** noch einmal —
oder ein Schritt mit **WASD** — lässt ihn wieder los.
* Schilder tragen nur die Mission, der Fokus, der Verfolgte und wer unter der
Maus steht. Sonst überdecken sich im Café acht Namen.
* **R notieren.** Was den Traum aufhält, steht nirgends geschrieben. Du siehst * **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 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 schief? Triffst du den Moment, steht die Ursache im Journal und der Slot
@@ -236,7 +240,12 @@ eintauschen könnte. Ein Erfolg bringt 2 EP, ein Fehlversuch 1 — auch der zäh
## Prüfen ## Prüfen
godot --headless --path . tests/verify.tscn # Prüfungen der Spiellogik tests/run.sh # Parse-Vorlauf, dann die ganze Suite (Zeitraffer 8×)
tests/run.sh schnell # ohne die langen Simulationen, für den Loop
Der Parse-Vorlauf lädt nur alle Skripte: ein Parse-Fehler ließe die Suite
sonst bis zum Timeout hängen. Die Suite läuft mit `Engine.time_scale`, dazu
misst sie jeden Abschnitt und nennt am Ende die langsamsten.
godot --path . tests/shots.tscn # 28 Screenshots nach user://shots/ godot --path . tests/shots.tscn # 28 Screenshots nach user://shots/
`verify` fährt das echte Spiel: leeres Traumdorf, einziehende Wesen mit ihren `verify` fährt das echte Spiel: leeres Traumdorf, einziehende Wesen mit ihren

View File

@@ -222,6 +222,9 @@ func _process(roh: float) -> void:
_aura.scale.z = _aura.scale.x _aura.scale.z = _aura.scale.x
_label.text = beschriftung() _label.text = beschriftung()
# Auf der Erde tragen nur Fokus, Verfolgte, Mission und wer unter der Maus
# steht ein Schild -- sonst überdecken sich acht Namen im Café.
_label.visible = not mit_mission or Game.markiert.has(str(data["id"]))
_label.modulate = Color(0.55, 0.85, 0.6) if erledigt else ( _label.modulate = Color(0.55, 0.85, 0.6) if erledigt else (
Color(1, 0.9, 0.6) if aktiv else Color(1, 1, 1)) Color(1, 0.9, 0.6) if aktiv else Color(1, 1, 1))

View File

@@ -89,7 +89,9 @@ func _bebauung() -> void:
0.10 + rng.randf() * 0.14, 0.62 + rng.randf() * 0.26) 0.10 + rng.randf() * 0.14, 0.62 + rng.randf() * 0.26)
var bau := Node3D.new() var bau := Node3D.new()
add_child(bau) add_child(bau)
Prims.box(bau, c + Vector3(0, hoehe * 0.5, 0), Vector3(g.x, hoehe, g.y), ton) var klotz := Prims.box(bau, c + Vector3(0, hoehe * 0.5, 0), Vector3(g.x, hoehe, g.y), ton, false)
# Kollision etwas größer: so schließt sich die Fuge zum Nachbarn, niemand keilt sich hinein
Prims._add_box_body(klotz, Vector3(g.x + 0.9, hoehe, g.y + 0.9))
for f in range(n): for f in range(n):
_fenster(bau, c + Vector3(0, Orte.hoehe(f), 0), g, ton) _fenster(bau, c + Vector3(0, Orte.hoehe(f), 0), g, ton)
Prims.box(bau, c + Vector3(0, hoehe + 0.2, 0), Prims.box(bau, c + Vector3(0, hoehe + 0.2, 0),
@@ -188,51 +190,86 @@ func _decke(parent: Node, key: String, cf: Vector3, _groesse: Vector3, farbe: Co
Prims.box(parent, mitte, Vector3(mass.x, 0.2, mass.y), farbe) 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 ## Ein paar Möbel, damit man sieht, wofür ein Raum da ist. Gestellt wird im
## Treppenstreifen. ## Rahmen der Tür (quer / zur Tür hin); was in der Türzone, im Treppenstreifen
## oder auf dem Weg von der Tür zur Treppe stünde, bleibt weg.
func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String, func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String,
ort: String = "") -> void: ort: String = "") -> void:
if ort == "": if ort == "":
ort = key ort = key
var w := maxf(0.5, groesse.x * 0.5 - 2.0) var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[ort]["dir"]]
var h := maxf(0.5, groesse.z * 0.5 - 2.0) var hd: float = (groesse.z if absf(d.z) > 0.0 else groesse.x) * 0.5
var frei := func(v: Vector3, halb: float) -> Vector3: var hq: float = (groesse.x if absf(d.z) > 0.0 else groesse.z) * 0.5
var p := Orte.frei_von_treppe(HumanDB.PLACES, ort, c + v, halb) var w := maxf(0.5, hq - 2.0)
p.y = c.y + v.y var h := maxf(0.5, hd - 2.0)
return p
if key.begins_with("heim_"): if key.begins_with("heim_"):
Prims.box(parent, frei.call(Vector3(-w * 0.6, 0.35, -h * 0.5), 1.5), Vector3(1.9, 0.7, 3.0), # an der Türwand, links und rechts der Tür -- die Rückwand gehört der Treppe
_moebel(parent, ort, c, hq - 1.35, hd - 1.6, 0.35, Vector3(1.9, 0.7, 2.4),
Color(0.85, 0.85, 0.90)) Color(0.85, 0.85, 0.90))
Prims.box(parent, frei.call(Vector3(w * 0.6, 0.75, 0), 1.1), Vector3(1.5, 0.15, 2.2), if _moebel(parent, ort, c, -(hq - 1.15), hd - 1.6, 0.75, Vector3(1.5, 0.15, 2.2),
farbe.darkened(0.5)) farbe.darkened(0.5)):
Prims.box(parent, frei.call(Vector3(w * 0.6, 1.05, 0), 0.1), Vector3(0.8, 0.55, 0.1), _moebel(parent, ort, c, -(hq - 1.15), hd - 1.6, 1.05, 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(parent, frei.call(Vector3(-w * 0.5 + float(i) * w * 0.5, 0.4, 0.4), 0.7), 0.7, 0.8, _moebel(parent, ort, c, -w * 0.5 + float(i) * w * 0.5, 0.4, 0.4,
Color(0.55, 0.40, 0.30)) Vector3(1.4, 0.8, 1.4), Color(0.55, 0.40, 0.30), true)
Prims.box(parent, frei.call(Vector3(0, 0.55, -h * 0.7), 0.4), Vector3(groesse.x - 3.0, 1.1, 0.8), _moebel(parent, ort, c, -hq * 0.5, -(h * 0.7), 0.55, Vector3(hq - 1.5, 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(parent, frei.call(Vector3(-w * 0.5 + float(i) * w * 0.5, 0.75, 0), 0.8), _moebel(parent, ort, c, -w * 0.5 + float(i) * w * 0.5, h * 0.3, 0.75,
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(parent, frei.call(Vector3(0, 0.55, h * 0.5), 0.5), Vector3(groesse.x - 3.0, 1.1, 1.0), _moebel(parent, ort, c, -hq * 0.45, hd - 3.4, 0.55, Vector3(hq - 1.2, 1.1, 1.0),
Color(0.70, 0.55, 0.35)) Color(0.70, 0.55, 0.35))
Prims.box(parent, frei.call(Vector3(0, 0.85, -h * 0.6), 1.3), Vector3(2.6, 1.7, 1.2), _moebel(parent, ort, c, -(hq - 1.6), -(h * 0.6), 0.85, 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(parent, frei.call(Vector3(0, 0.5, -h * 0.6 + float(i) * h * 0.6), 0.35), _moebel(parent, ort, c, -hq * 0.4, -h * 0.6 + float(i) * h * 0.6, 0.5,
Vector3(groesse.x - 3.0, 0.2, 0.7), Color(0.66, 0.62, 0.58)) Vector3(hq - 1.5, 0.2, 0.7), Color(0.66, 0.62, 0.58))
Prims.box(parent, frei.call(Vector3(0, 1.1, -h), 0.1), Vector3(groesse.x * 0.55, 1.6, 0.15), _moebel(parent, ort, c, -hq * 0.4, -h, 1.1, Vector3(hq * 0.8, 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(parent, frei.call(Vector3(0, 0.6, 0), 0.7), Vector3(groesse.x - 4.0, 1.2, 1.4), _moebel(parent, ort, c, -hq * 0.4, 0.0, 0.6, Vector3(hq * 0.8 - 1.0, 1.2, 1.4),
Color(0.52, 0.48, 0.46)) Color(0.52, 0.48, 0.46))
## Ein Möbelstück: aq quer zur Tür, ad zur Tür hin (positiv = zur Tür), Maße
## als (quer, hoch, tief). Steht es im Weg, wird es nicht gestellt.
func _moebel(parent: Node, ort: String, c: Vector3, aq: float, ad: float, y: float,
mass: Vector3, farbe: Color, rund: bool = false) -> bool:
var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[ort]["dir"]]
var quer := Vector3(-d.z, 0, d.x)
var pos := c + quer * aq + d * ad
pos.y = c.y + y
var s: Array = HumanDB.PLACES[ort]["size"]
var hd: float = (float(s[1]) if absf(d.z) > 0.0 else float(s[0])) * 0.5
var halb := maxf(mass.x, mass.z) * 0.5
# Türzone: 2,2 tief hinter der Tür, 4 breit
if ad + mass.z * 0.5 > hd - 2.2 and absf(aq) - mass.x * 0.5 < 2.0:
return false
# Treppenstreifen
var r := Orte.treppen_rahmen(HumanDB.PLACES, ort)
var n: Vector3 = r["n"]
var tief_n: float = (mass.z if absf(n.dot(d)) > 0.5 else mass.x) * 0.5
if (pos - c).dot(n) + tief_n > float(r["halb_n"]) - Orte.TREPPE_TIEFE - 0.3:
return false
# Weg von der Tür zur Treppe
var von := Orte.innen_tuer(HumanDB.PLACES, ort)
var bis := Orte.treppe(HumanDB.PLACES, ort, 0)
var flach := Vector3(pos.x, 0, pos.z)
if Geometry3D.get_closest_point_to_segment(flach, von, bis).distance_to(flach) < halb + 0.9:
return false
var welt_mass := mass if absf(d.z) > 0.0 else Vector3(mass.z, mass.y, mass.x)
if rund:
Prims.cyl(parent, pos, mass.x * 0.5, mass.y, farbe)
else:
Prims.box(parent, pos, welt_mass, farbe)
return true
## Der mittlere Block bleibt frei: Rasen, Teich, Bäume, Bänke. Der Park ist ## Der mittlere Block bleibt frei: Rasen, Teich, Bäume, Bänke. Der Park ist
## der einzige Ort im Bezirk, an dem nicht gebaut wird. ## der einzige Ort im Bezirk, an dem nicht gebaut wird.
func _park() -> void: func _park() -> void:

View File

@@ -32,6 +32,7 @@ var problem_stand: Dictionary = {} # "human:problem" -> gelungene Anwendung
var ueberwunden: Dictionary = {} # human_id -> {problem -> true} var ueberwunden: Dictionary = {} # human_id -> {problem -> true}
var gesehen_zaehler: Dictionary = {} # "human:problem" -> wie oft dabeigewesen var gesehen_zaehler: Dictionary = {} # "human:problem" -> wie oft dabeigewesen
var completed: Array = [] var completed: Array = []
var markiert: Dictionary = {} # human_id -> true: wer gerade ein Schild trägt
var time_h: float = 7.0 var time_h: float = 7.0
var day: int = 1 var day: int = 1
var teleport_lock: float = 0.0 var teleport_lock: float = 0.0

View File

@@ -305,6 +305,7 @@ static func _kreuzung(a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> float:
## volle Breite, die Ost- und Westreihe nur dazwischen -- sonst überlappen die ## volle Breite, die Ost- und Westreihe nur dazwischen -- sonst überlappen die
## Ecken. ## Ecken.
static func _block(mitte: Vector3, groesse: Vector2, rng: RandomNumberGenerator) -> void: static func _block(mitte: Vector3, groesse: Vector2, rng: RandomNumberGenerator) -> void:
var erste := PARZELLEN.size()
var tiefe := minf(RAND_TIEFE, minf(groesse.x, groesse.y) * 0.34) var tiefe := minf(RAND_TIEFE, minf(groesse.x, groesse.y) * 0.34)
var seiten := [ var seiten := [
["back", Vector3(0, 0, 1), groesse.x, groesse.y], ["back", Vector3(0, 0, 1), groesse.x, groesse.y],
@@ -346,6 +347,7 @@ static func _block(mitte: Vector3, groesse: Vector2, rng: RandomNumberGenerator)
var ende := mitte + richtung * ((innen.y if laengs else innen.x) * 0.5 - 2.0) var ende := mitte + richtung * ((innen.y if laengs else innen.x) * 0.5 - 2.0)
_strasse(start, ende, ROJI) _strasse(start, ende, ROJI)
var flanke := Vector3(1, 0, 0) if laengs else Vector3(0, 0, 1) var flanke := Vector3(1, 0, 0) if laengs else Vector3(0, 0, 1)
_gasse_freischneiden(erste, start, ende, flanke)
var p_breite := (innen.x if laengs else innen.y) * 0.5 - ROJI * 0.5 - 0.8 var p_breite := (innen.x if laengs else innen.y) * 0.5 - ROJI * 0.5 - 0.8
if p_breite < PARZELLE_MIN * 0.8: if p_breite < PARZELLE_MIN * 0.8:
return return
@@ -366,6 +368,46 @@ static func _block(mitte: Vector3, groesse: Vector2, rng: RandomNumberGenerator)
}) })
## Die Gasse läuft durch die Randreihe in den Block. Eine Parzelle, die auf
## ihr steht, wird in zwei Hälften links und rechts der Gasse geteilt --
## sonst stünde ein Haus quer über der Einfahrt.
static func _gasse_freischneiden(ab: int, start: Vector3, ende: Vector3, flanke: Vector3) -> void:
var quer_ist := absf(flanke.x) > 0.0 # Gasse läuft in z, Flanke ist x
var breite := ROJI * 0.5 + 0.8
var g_mitte: float = start.x if quer_ist else start.z
var g_von: float = minf(start.z, ende.z) if quer_ist else minf(start.x, ende.x)
var g_bis: float = maxf(start.z, ende.z) if quer_ist else maxf(start.x, ende.x)
var neu: Array = []
var weg: Array = []
for i in range(ab, PARZELLEN.size()):
var p: Dictionary = PARZELLEN[i]
var pos: Vector3 = p["pos"]
var s: Vector2 = p["size"]
var pf: float = pos.x if quer_ist else pos.z # Lage entlang der Flanke
var pl: float = pos.z if quer_ist else pos.x # Lage entlang der Gasse
var hf: float = (s.x if quer_ist else s.y) * 0.5
var hl: float = (s.y if quer_ist else s.x) * 0.5
if pl + hl < g_von or pl - hl > g_bis:
continue
if pf + hf < g_mitte - breite or pf - hf > g_mitte + breite:
continue
weg.append(i)
for teil in [[pf - hf, g_mitte - breite], [g_mitte + breite, pf + hf]]:
var w: float = float(teil[1]) - float(teil[0])
if w < 3.0:
continue
var m: float = (float(teil[0]) + float(teil[1])) * 0.5
neu.append({
"pos": Vector3(m, 0, pl) if quer_ist else Vector3(pl, 0, m),
"size": Vector2(w, s.y) if quer_ist else Vector2(s.x, w),
"dir": p["dir"],
})
weg.reverse()
for i in weg:
PARZELLEN.remove_at(i)
PARZELLEN.append_array(neu)
# ------------------------------------------------------------ Belegung # ------------------------------------------------------------ Belegung
static func _park_eintragen() -> void: static func _park_eintragen() -> void:
@@ -419,10 +461,13 @@ static func _sonderbauten_verteilen() -> void:
## Die Wohnhäuser: gleichmäßig über den Bezirk verteilt, damit die Wege ## Die Wohnhäuser: gleichmäßig über den Bezirk verteilt, damit die Wege
## nicht alle in derselben Ecke enden. ## nicht alle in derselben Ecke enden.
const WOHNUNG_MIN := 7.0 # schmaler passt kein Treppenhaus mehr neben die Tür
static func _wohnhaeuser_verteilen() -> void: static func _wohnhaeuser_verteilen() -> void:
var frei: Array = [] var frei: Array = []
for i in range(PARZELLEN.size()): for i in range(PARZELLEN.size()):
if not PARZELLEN[i].has("belegt"): var g: Vector2 = PARZELLEN[i]["size"]
if not PARZELLEN[i].has("belegt") and minf(g.x, g.y) >= WOHNUNG_MIN:
frei.append(i) frei.append(i)
if frei.is_empty(): if frei.is_empty():
return return

View File

@@ -72,10 +72,43 @@ func _process(delta: float) -> void:
Game.notiz_sperre = maxf(0.0, Game.notiz_sperre - delta) Game.notiz_sperre = maxf(0.0, Game.notiz_sperre - delta)
Game.situationen_ticken(dt, auf_erde and not Game.schlaeft_ziel()) Game.situationen_ticken(dt, auf_erde and not Game.schlaeft_ziel())
_fokus_suchen() _fokus_suchen()
_markieren()
_dabeisein() _dabeisein()
_hinweis_setzen() _hinweis_setzen()
## Wer ein Schild trägt: Mission, Fokus, Verfolgter und wer unter der Maus steht.
func _markieren() -> void:
Game.markiert = {}
if Game.active_mission != "":
Game.markiert[Game.active_mission] = true
if _fokus_mensch != null:
Game.markiert[str(_fokus_mensch.data["id"])] = true
if player.folgt != null and is_instance_valid(player.folgt):
Game.markiert[str(player.folgt.data["id"])] = true
if auf_erde:
var unter_maus := _mensch_unter_maus(get_viewport().get_mouse_position())
if unter_maus != null:
Game.markiert[str(unter_maus.data["id"])] = true
## Der Mensch, der auf dem Bildschirm am nächsten am Punkt steht -- oder null.
func _mensch_unter_maus(pos: Vector2) -> Bewohner:
if not auf_erde:
return null
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
return bester
func _fokus_suchen() -> void: func _fokus_suchen() -> void:
_fokus = null _fokus = null
_fokus_mensch = null _fokus_mensch = null
@@ -96,7 +129,9 @@ func _fokus_suchen() -> void:
if d < bestm: if d < bestm:
bestm = d bestm = d
_fokus_mensch = m _fokus_mensch = m
if _fokus_mensch != null and (_fokus == null or bestm < best): if player.folgt != null and is_instance_valid(player.folgt):
_fokus_mensch = player.folgt # wer sich angehängt hat, ist dabei -- auch auf der Treppe
if _fokus_mensch != null and (_fokus == null or bestm < best or _fokus_mensch == player.folgt):
_fokus = null _fokus = null
elif _fokus != null: elif _fokus != null:
_fokus_mensch = null _fokus_mensch = null
@@ -209,25 +244,14 @@ func _notieren() -> void:
## Klick auf einen Menschen: an ihn hängen, noch ein Klick löst wieder. ## Klick auf einen Menschen: an ihn hängen, noch ein Klick löst wieder.
func _klick(pos: Vector2) -> void: func _klick(pos: Vector2) -> void:
if not auf_erde: var bester := _mensch_unter_maus(pos)
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: if bester == null:
return return
if player.folgt == bester: if player.folgt == bester:
player.folgt = null player.folgt = null
Game.notified.emit("Du lässt %s wieder los." % bester.data["name"], Color(0.8, 0.85, 1)) Game.notified.emit("Du lässt %s wieder los." % bester.data["name"], Color(0.8, 0.85, 1))
else: else:
player.folgt = bester player.anhaengen(bester)
Game.notified.emit("Du hängst dich an %s." % bester.data["name"], Color(0.7, 0.9, 1)) Game.notified.emit("Du hängst dich an %s." % bester.data["name"], Color(0.7, 0.9, 1))
@@ -244,7 +268,7 @@ func _verfolgen() -> void:
Game.notified.emit("Geh näher an einen Menschen heran, um ihm zu folgen.", Game.notified.emit("Geh näher an einen Menschen heran, um ihm zu folgen.",
Color(1, 0.85, 0.5)) Color(1, 0.85, 0.5))
return return
player.folgt = _fokus_mensch player.anhaengen(_fokus_mensch)
Game.notified.emit("Du hängst dich an %s." % _fokus_mensch.data["name"], Game.notified.emit("Du hängst dich an %s." % _fokus_mensch.data["name"],
Color(0.7, 0.9, 1)) Color(0.7, 0.9, 1))

View File

@@ -140,7 +140,8 @@ static func treppen_decke(places: Dictionary, key: String) -> Array:
var halb_a := float(r["halb_a"]) var halb_a := float(r["halb_a"])
var out: Array = [] var out: Array = []
# Hauptplatte: alles außer dem Streifen an der Treppenwand # 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)]) if halb_n * 2.0 - TREPPE_TIEFE > 0.2:
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 # Rest des Streifens jenseits des Lochs
var ende: float = _podest_a(r, 1) - PODEST var ende: float = _podest_a(r, 1) - PODEST
var rest := ende + halb_a var rest := ende + halb_a
@@ -176,6 +177,53 @@ static func innen_tuer(places: Dictionary, key: String) -> Vector3:
return c + d * (halb - 1.2) return c + d * (halb - 1.2)
## Ort, in dessen Grundriss der Punkt liegt -- oder "".
static func ort_bei(places: Dictionary, p: Vector3) -> String:
for key in places:
var s: Array = places[key]["size"]
if float(s[0]) <= 0.0:
continue
var c := center(places, key)
if absf(p.x - c.x) <= float(s[0]) * 0.5 and absf(p.z - c.z) <= float(s[1]) * 0.5:
return key
return ""
## Weg für einen Körper von einem freien Punkt zu einem Menschen: aus dem
## eigenen Haus heraus, über das Netz, durch seine Tür und in seine Etage.
static func route_zu(places: Dictionary, netz: Netz, von: Vector3, nach: Vector3,
nach_ort: String, nach_etage: int) -> Array:
var pts: Array = []
var start := von
var von_ort := ort_bei(places, von)
if von_ort != "" and gebaeude(places, von_ort):
var e := int(round(von.y / ETAGE))
if e > 0:
pts.append(treppe(places, von_ort, e))
pts.append_array(treppen_weg(places, von_ort, e, 0))
pts.append(innen_tuer(places, von_ort))
start = door(places, von_ort)
pts.append(start)
var drin := nach_ort != "" and gebaeude(places, nach_ort)
var ende := door(places, nach_ort) if drin else nach
if netz != null:
var a: int = int(netz.andocken(start)["knoten"])
var b: int = int(netz.andocken(ende)["knoten"])
for idx in netz.pfad(a, b):
pts.append(netz.knoten[idx])
if drin:
pts.append(door(places, nach_ort))
pts.append(innen_tuer(places, nach_ort))
if nach_etage > 0:
pts.append(treppe(places, nach_ort, 0))
pts.append_array(treppen_weg(places, nach_ort, 0, nach_etage))
var ziel := nach
if drin:
ziel.y = hoehe(nach_etage)
pts.append(ziel)
return pts
## Ein Ort ist ein Gebäude, wenn er Grundriss und Tür hat. ## Ein Ort ist ein Gebäude, wenn er Grundriss und Tür hat.
static func gebaeude(places: Dictionary, key: String) -> bool: static func gebaeude(places: Dictionary, key: String) -> bool:
return places.has(key) and float(places[key]["size"][0]) > 0.0 \ return places.has(key) and float(places[key]["size"][0]) > 0.0 \

View File

@@ -11,6 +11,7 @@ const GRAVITY := 24.0
const JUMP := 9.0 const JUMP := 9.0
const FOLGE_ABSTAND := 2.5 # so weit bleibt der Geist auf der Spur zurück 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 LEINE := 14.0 # weiter weg reißt die Leine: er wird nachgezogen
const FOLGE_NAH := 5.0 # näher dran braucht es keinen Anlauf
const SCHWEBE := 0.25 # eine Handbreit über dem Boden const SCHWEBE := 0.25 # eine Handbreit über dem Boden
const PITCH := -0.96 # 55° von oben, fest wie in Diablo const PITCH := -0.96 # 55° von oben, fest wie in Diablo
const CAM_YAW := PI * 0.25 # Blick diagonal über den Bezirk const CAM_YAW := PI * 0.25 # Blick diagonal über den Bezirk
@@ -28,6 +29,9 @@ var arm: Node3D
var cam: Camera3D var cam: Camera3D
var _dreh_rest := 0.0 # noch zu drehender Winkel (Q) var _dreh_rest := 0.0 # noch zu drehender Winkel (Q)
var _zoom_stufe := 0 var _zoom_stufe := 0
var _anlauf: Array = [] # Weg zu dem Menschen, an den man sich gehängt hat
var _stockt := 0.0 # Sekunden ohne Vorankommen
var spruenge := 0 # wie oft die Leine nachziehen musste
var body: Node3D var body: Node3D
var _schleier: MeshInstance3D var _schleier: MeshInstance3D
var _kern: MeshInstance3D var _kern: MeshInstance3D
@@ -51,7 +55,7 @@ const FUNKEN_MAX := 6
func _ready() -> void: func _ready() -> void:
var cs := CollisionShape3D.new() var cs := CollisionShape3D.new()
var shape := CapsuleShape3D.new() var shape := CapsuleShape3D.new()
shape.radius = 0.4 shape.radius = 0.35 # passt durch die 0,8 m zwischen zwei Häusern
shape.height = 1.6 shape.height = 1.6
cs.shape = shape cs.shape = shape
cs.position.y = 0.8 cs.position.y = 0.8
@@ -164,12 +168,33 @@ func _physics_process(delta: float) -> void:
move_and_slide() move_and_slide()
if global_position.y < -400.0: if global_position.y < -400.0:
global_position.y = 2.0 global_position.y = 2.0
_eingeklemmt(delta, dir.length() > 0.01)
if dir.length() > 0.01: if dir.length() > 0.01:
var ziel := atan2(-dir.x, -dir.z) var ziel := atan2(-dir.x, -dir.z)
body.rotation.y = lerp_angle(body.rotation.y, ziel, 1.0 - pow(0.0005, delta)) body.rotation.y = lerp_angle(body.rotation.y, ziel, 1.0 - pow(0.0005, delta))
## Wer mit gedrückter Taste zwei Sekunden nicht vorankommt, steckt fest --
## dann setzt ihn die Stadt auf die nächste Straße.
func _eingeklemmt(delta: float, will: bool) -> void:
if mode != Mode.GEIST or not will or Vector2(velocity.x, velocity.z).length() > 0.3:
_stockt = 0.0
return
_stockt += delta
if _stockt < 2.0:
return
_stockt = 0.0
var netz: Netz = HumanDB.NETZ
if netz == null or netz.knoten.is_empty():
return
var nah: Vector3 = netz.knoten[int(netz.andocken(global_position)["knoten"])]
global_position = Vector3(nah.x, 0.3, nah.z)
velocity = Vector3.ZERO
spruenge += 1
Game.notified.emit("Eingeklemmt -- du stehst wieder auf der Straße.", Color(0.8, 0.85, 1))
func blickrichtung() -> Vector3: func blickrichtung() -> Vector3:
return -Vector3(sin(yaw), 0, cos(yaw)) return -Vector3(sin(yaw), 0, cos(yaw))
@@ -185,11 +210,33 @@ func _fallen(delta: float) -> void:
velocity.y -= GRAVITY * delta velocity.y -= GRAVITY * delta
## An einen Menschen hängen. Steht er weit weg, wird erst der Weg zu ihm
## geplant: aus dem Haus, über die Straßen, in seine Wohnung.
func anhaengen(m: Bewohner) -> void:
folgt = m
_anlauf = []
_stockt = 0.0
if global_position.distance_to(m.global_position) > FOLGE_NAH:
var ort: String = m.place if m._path.is_empty() else ""
_anlauf = Orte.route_zu(m.places, m.netz, global_position, m.global_position,
ort, m.etage(ort) if ort != "" else 0)
## Folgen heißt: seiner Spur nachgehen, durch dieselben Türen und Treppen. ## 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. ## Er läuft so schnell wie nötig, um dranzubleiben -- auch bei hohem Tempo.
func _an_der_leine(delta: float) -> void: func _an_der_leine(delta: float) -> void:
var mensch := folgt as Bewohner var mensch := folgt as Bewohner
var ziel: Vector3 = mensch.spur_punkt(FOLGE_ABSTAND) var ziel: Vector3
if not _anlauf.is_empty():
ziel = _anlauf[0]
var bis := ziel - global_position
bis.y = 0.0
if bis.length() < 0.6 and absf(ziel.y - global_position.y) < 1.6:
_anlauf.pop_front()
if _anlauf.is_empty():
ziel = mensch.spur_punkt(FOLGE_ABSTAND)
else:
ziel = _anlauf[0]
var d := ziel - global_position var d := ziel - global_position
d.y = 0.0 d.y = 0.0
var tempo := maxf(SPEED_GEIST, Bewohner.GEHTEMPO * Game.zeit_faktor() * 1.4) var tempo := maxf(SPEED_GEIST, Bewohner.GEHTEMPO * Game.zeit_faktor() * 1.4)
@@ -203,9 +250,17 @@ func _an_der_leine(delta: float) -> void:
velocity.z = 0.0 velocity.z = 0.0
_fallen(delta) _fallen(delta)
move_and_slide() move_and_slide()
if global_position.distance_to(mensch.global_position) > LEINE: if d.length() > 0.6 and Vector2(velocity.x, velocity.z).length() < 0.3:
_stockt += delta
else:
_stockt = 0.0
var zu_weit: bool = _anlauf.is_empty() \
and global_position.distance_to(mensch.global_position) > LEINE
if _stockt > 2.0 or zu_weit:
global_position = ziel # hängen geblieben: die Leine zieht ihn nach global_position = ziel # hängen geblieben: die Leine zieht ihn nach
velocity = Vector3.ZERO velocity = Vector3.ZERO
_stockt = 0.0
spruenge += 1
## Schweben, Funken, Schweif und Modusfarbe -- der Zustand ist am Geist ablesbar. ## Schweben, Funken, Schweif und Modusfarbe -- der Zustand ist am Geist ablesbar.

View File

@@ -113,7 +113,8 @@ static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color,
var h := size.y var h := size.y
var waende: Array = [] 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) # 3 cm über dem Gelände, sonst flimmert der Boden gegen die Insel
box(parent, center + Vector3(0, -0.07, 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]
for d in seiten: for d in seiten:
var quer := absf(d.z) > 0.0 # Wand liegt entlang der X-Achse var quer := absf(d.z) > 0.0 # Wand liegt entlang der X-Achse
@@ -184,6 +185,9 @@ static func schnitt(bauten: Array, spieler: Vector3, kamera: Vector3) -> void:
b["meshes"] = _meshes_von(b) b["meshes"] = _meshes_von(b)
for mi in b["meshes"]: for mi in b["meshes"]:
(mi as GeometryInstance3D).transparency = 0.82 if im_weg else 0.0 (mi as GeometryInstance3D).transparency = 0.82 if im_weg else 0.0
(mi as GeometryInstance3D).cast_shadow = \
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF if im_weg \
else GeometryInstance3D.SHADOW_CASTING_SETTING_ON
var etagen: Array = b["etagen"] var etagen: Array = b["etagen"]
var pf := clampi(int(floor((spieler.y - c.y + 0.6) / Orte.ETAGE)), 0, maxi(0, etagen.size() - 1)) 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()): for f in range(etagen.size()):

View File

@@ -1,91 +0,0 @@
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()

View File

@@ -1,6 +0,0 @@
[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")

View File

@@ -1,54 +0,0 @@
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)

View File

@@ -1,6 +0,0 @@
[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")

24
tests/parse.gd Normal file
View File

@@ -0,0 +1,24 @@
extends Node
## Parse-Vorlauf: lädt jedes Skript einmal und beendet sich mit Fehlercode.
## Ein Parse-Fehler in der Suite ließe Godot sonst bis zum Timeout hängen.
func _ready() -> void:
var fehler := 0
var anzahl := 0
for pfad in _skripte("res://scripts") + _skripte("res://tests"):
anzahl += 1
var s: Variant = load(pfad)
if s == null or not (s is GDScript) or not (s as GDScript).can_instantiate():
fehler += 1
print("PARSE FEHLER " + pfad)
print("Parse-Vorlauf: %d Skripte, %d Fehler" % [anzahl, fehler])
get_tree().quit(1 if fehler > 0 else 0)
func _skripte(ordner: String) -> Array:
var out: Array = []
for name in DirAccess.get_files_at(ordner):
if name.ends_with(".gd") and not name.begins_with("_"):
out.append(ordner + "/" + name)
out.sort()
return out

6
tests/parse.tscn Normal file
View File

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

20
tests/run.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Prüflauf: erst der Parse-Vorlauf (Sekunden), dann die Suite.
# tests/run.sh voller Lauf
# tests/run.sh schnell ohne die langen Simulationen
cd "$(dirname "$0")/.." || exit 2
log=${TMPDIR:-/tmp}/traumwandler
mkdir -p "$log"
if ! timeout 90 godot --headless --path . tests/parse.tscn > "$log/parse.log" 2>&1; then
grep -E "SCRIPT ERROR|PARSE FEHLER|Parse-Vorlauf" "$log/parse.log"
echo "Parse-Vorlauf fehlgeschlagen"
exit 1
fi
timeout 600 godot --headless --path . tests/verify.tscn -- "$@" > "$log/suite.log" 2>&1
code=$?
echo "OK: $(grep -c ' OK ' "$log/suite.log")"
grep -E "FEHLT|Fehlende|SCRIPT ERROR|Parse Error|Langsamste|Schnelllauf" "$log/suite.log"
fehler=$(grep -E "^(SCRIPT )?ERROR:" "$log/suite.log" | grep -v "leaked" | sort | uniq -c | sort -rn)
if [ -n "$fehler" ]; then echo "Engine-Fehler:"; echo "$fehler" | head -10; [ $code -eq 0 ] && code=3; fi
[ $code -eq 124 ] && echo "Timeout -- Log: $log/suite.log"
exit $code

View File

@@ -5,6 +5,16 @@ var fehler := 0
var main: Node3D var main: Node3D
const ZEITRAFFER := 8.0 # die Engine läuft im Test achtmal schneller
## Abschnitte, die ganze Tage oder viele Läufe simulieren -- im Schnelllauf übersprungen.
const LANGSAM := ["test_tagesablauf", "test_verfolgung", "test_erster_traum_schaffbar",
"test_huegel", "test_dorf_organisch", "test_welt_waechst"]
var schnell := false
var _uhr_abschnitt := 0
var _dauern: Array = []
var _uebersprungen := 0
func p(ok: bool, text: String) -> void: func p(ok: bool, text: String) -> void:
print((" OK " if ok else " FEHLT ") + text) print((" OK " if ok else " FEHLT ") + text)
if not ok: if not ok:
@@ -12,53 +22,91 @@ func p(ok: bool, text: String) -> void:
func abschnitt(t: String) -> void: func abschnitt(t: String) -> void:
_abschnitt_ende()
print("\n== " + t) print("\n== " + t)
_dauern.append([t, 0.0])
_uhr_abschnitt = Time.get_ticks_msec()
func _abschnitt_ende() -> void:
if _dauern.is_empty():
return
_dauern[_dauern.size() - 1][1] = float(Time.get_ticks_msec() - _uhr_abschnitt) / 1000.0
## Wer den Traum mit reinen Game-Aufrufen vorantreibt, tickt keine Momente:
## ein laufender Moment hielte den Fortschritt sonst für immer an.
func _ohne_momente() -> void:
Game.situation = {}
Game.situation_pause = 1.0e6
## Ruft einen Testabschnitt auf -- oder überspringt ihn im Schnelllauf.
func lauf(name: String) -> void:
if schnell and LANGSAM.has(name):
_uebersprungen += 1
return
await Callable(self, name).call()
func _ready() -> void: func _ready() -> void:
schnell = OS.get_cmdline_user_args().has("schnell")
# Zeitraffer: die Uhr läuft achtmal schneller, der Physikschritt bleibt 1/60 s --
# sonst fiele der Spieler bei großen Schritten durch den Boden.
Engine.time_scale = ZEITRAFFER
Engine.physics_ticks_per_second = int(60.0 * ZEITRAFFER)
Engine.max_physics_steps_per_frame = 64
seed(20240) seed(20240)
main = load("res://scenes/main.tscn").instantiate() main = load("res://scenes/main.tscn").instantiate()
add_child(main) add_child(main)
await get_tree().process_frame await get_tree().process_frame
await get_tree().process_frame await get_tree().process_frame
await test_start() await lauf("test_start")
await test_journal() await lauf("test_journal")
await test_traumdorf() await lauf("test_traumdorf")
await test_dorf_organisch() await lauf("test_dorf_organisch")
await test_huegel() await lauf("test_huegel")
await test_haus_betreten() await lauf("test_haus_betreten")
await test_hoersaal() await lauf("test_hoersaal")
await test_mission_annehmen() await lauf("test_mission_annehmen")
await test_reise_zur_erde() await lauf("test_reise_zur_erde")
await test_bezirk() await lauf("test_bezirk")
await test_tagesablauf() await lauf("test_tagesablauf")
await test_wege_machbar() await lauf("test_wege_machbar")
await test_geist() await lauf("test_geist")
await test_verfolgung() await lauf("test_verfolgung")
await test_charakter() await lauf("test_charakter")
await test_kamera_und_etagen() await lauf("test_kamera_und_etagen")
await test_gehen_bei_tempo() await lauf("test_gehen_bei_tempo")
await test_diagnose() await lauf("test_diagnose")
await test_notieren_im_spiel() await lauf("test_notieren_im_spiel")
await test_tagesablauf_anzeige() await lauf("test_tagesablauf_anzeige")
await test_ablauf_im_hud() await lauf("test_ablauf_im_hud")
await test_probleme_und_momente() await lauf("test_probleme_und_momente")
await test_eigene_menschen() await lauf("test_eigene_menschen")
await test_systeme() await lauf("test_systeme")
await test_tempo_des_traums() await lauf("test_tempo_des_traums")
await test_cooldown() await lauf("test_cooldown")
await test_chance_verdeckt() await lauf("test_chance_verdeckt")
await test_mission_abschliessen() await lauf("test_mission_abschliessen")
await test_welt_waechst() await lauf("test_welt_waechst")
await test_techniken_verbessern_sich() await lauf("test_techniken_verbessern_sich")
await test_journal_ueberall() await lauf("test_journal_ueberall")
await test_steuerung() await lauf("test_steuerung")
await test_erster_traum_schaffbar() await lauf("test_erster_traum_schaffbar")
await test_brett_nie_leer() await lauf("test_brett_nie_leer")
await test_lesen_lohnt_sich() await lauf("test_lesen_lohnt_sich")
await test_tempo() await lauf("test_tempo")
_abschnitt_ende()
_dauern.sort_custom(func(x, y): return x[1] > y[1])
var langsam: Array = []
for i in range(mini(5, _dauern.size())):
langsam.append("%s %.0f s" % [_dauern[i][0], _dauern[i][1]])
print("\nLangsamste Abschnitte: " + " · ".join(langsam))
if schnell:
print("Schnelllauf: %d Abschnitte übersprungen" % _uebersprungen)
print("\n=========================================") print("\n=========================================")
print("Fehlende Prüfungen: %d" % fehler) print("Fehlende Prüfungen: %d" % fehler)
print("=========================================") print("=========================================")
@@ -817,7 +865,6 @@ func test_charakter() -> void:
sp.set_mode(Player.Mode.GEIST) sp.set_mode(Player.Mode.GEIST)
sp.look_enabled = true sp.look_enabled = true
sp.folgt = null sp.folgt = null
var moment_merker: Dictionary = Game.situation.duplicate(true)
Game.situation = {} # ein laufender Moment ließe die Farbe flackern 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._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) p(sp._funken.size() == Player.FUNKEN_MAX, "%d Funken stehen bereit" % Player.FUNKEN_MAX)
@@ -837,7 +884,6 @@ func test_charakter() -> void:
for i in range(90): for i in range(90):
await get_tree().physics_frame await get_tree().physics_frame
p(_farbabstand(sp._farbe, Player.FARBE_FOLGE) < 0.1, "Beim Verfolgen leuchtet er warm") p(_farbabstand(sp._farbe, Player.FARBE_FOLGE) < 0.1, "Beim Verfolgen leuchtet er warm")
Game.situation = moment_merker
sp.folgt = null sp.folgt = null
sp.global_position = Vector3(0, 0.5, 9.0) # freier Rasen im Park sp.global_position = Vector3(0, 0.5, 9.0) # freier Rasen im Park
sp.velocity = Vector3.ZERO sp.velocity = Vector3.ZERO
@@ -954,6 +1000,18 @@ func test_kamera_und_etagen() -> void:
if Orte.etagen(HumanDB.PLACES, key) > 1: if Orte.etagen(HumanDB.PLACES, key) > 1:
mehr += 1 mehr += 1
p(mehr >= 12, "%d Gebäude haben mehrere Stockwerke" % mehr) p(mehr >= 12, "%d Gebäude haben mehrere Stockwerke" % mehr)
var eng := 0
for key in HumanDB.PLACES:
var sz: Array = HumanDB.PLACES[key]["size"]
if float(sz[0]) > 0.0 and minf(float(sz[0]), float(sz[1])) < Orte.TREPPE_TIEFE + 2.0:
eng += 1
p(eng == 0, "Jedes Gebäude hat Platz für Treppenhaus und Decke (%d zu schmal)" % eng)
for b in welt()._bauten:
for mi in Prims._meshes_von(b):
var bm := (mi as MeshInstance3D).mesh as BoxMesh
if bm != null and (bm.size.x <= 0.0 or bm.size.y <= 0.0 or bm.size.z <= 0.0):
eng += 1
p(eng == 0, "Keine Platte mit negativer Größe")
var oben: Bewohner = null var oben: Bewohner = null
for m in welt().humans: for m in welt().humans:
if m.etage(str(m.data["heim"])) > 0: if m.etage(str(m.data["heim"])) > 0:
@@ -1064,6 +1122,40 @@ func test_kamera_und_etagen() -> void:
p(sp.folgt == oben, "Ein Klick auf einen Menschen hängt den Geist an ihn") p(sp.folgt == oben, "Ein Klick auf einen Menschen hängt den Geist an ihn")
main._klick(schirm) main._klick(schirm)
p(sp.folgt == null, "Noch ein Klick löst ihn wieder") p(sp.folgt == null, "Noch ein Klick löst ihn wieder")
# Anlauf: von weit weg zu Fuß über die Straße bis in seine Wohnung
var daheim := false
for i in range(3000):
schritte(1, 0.05, 2.0)
if oben.place == heim and oben._path.is_empty():
daheim = true
break
sp.global_position = Vector3(0, 0.3, 12.0) # im Park, weit weg
sp.velocity = Vector3.ZERO
sp.spruenge = 0
sp.anhaengen(oben)
p(daheim and sp._anlauf.size() >= 4 and (sp._anlauf.back() as Vector3).y > Orte.ETAGE - 0.1,
"Von weit weg plant der Geist den Weg über die Straße in seine Etage (%d Punkte)" %
sp._anlauf.size())
var angekommen := false
for i in range(2400):
await get_tree().physics_frame
if sp.global_position.distance_to(oben.global_position) < 4.0:
angekommen = true
break
p(angekommen and sp.spruenge == 0,
"Und kommt zu Fuß bei ihm an, ohne dass die Leine nachziehen muss (%d Sprünge, Abstand %.1f)" %
[sp.spruenge, sp.global_position.distance_to(oben.global_position)])
# Schilder: nur wer markiert ist
schritte(2)
var fremde_schilder := 0
for m in welt().humans:
if m != oben and (m._label as Label3D).visible and not Game.markiert.has(str(m.data["id"])):
fremde_schilder += 1
p((oben._label as Label3D).visible and fremde_schilder == 0,
"Nur der Verfolgte trägt ein Schild, die anderen nicht (%d fremde)" % fremde_schilder)
sp.folgt = null
Game.time_h = uhr Game.time_h = uhr
@@ -1160,7 +1252,7 @@ func test_verfolgung() -> void:
p(main.player.folgt == null, "Wer selbst losgeht, lässt ihn los") p(main.player.folgt == null, "Wer selbst losgeht, lässt ihn los")
# Die Taste löst ebenfalls # Die Taste löst ebenfalls
main.player.global_position = mensch.global_position + Vector3(0, 1.5, 2.0) main.player.global_position = mensch.global_position + Vector3(0, 0.3, 0)
main.player.velocity = Vector3.ZERO main.player.velocity = Vector3.ZERO
main._process(0.05) main._process(0.05)
main._verfolgen() main._verfolgen()
@@ -1963,6 +2055,7 @@ func test_systeme() -> void:
## Je mehr Ursachen abgedeckt sind, desto schneller kommt der Traum voran. ## Je mehr Ursachen abgedeckt sind, desto schneller kommt der Traum voran.
func test_tempo_des_traums() -> void: func test_tempo_des_traums() -> void:
abschnitt("Tempo — je besser die Systeme, desto schneller") abschnitt("Tempo — je besser die Systeme, desto schneller")
_ohne_momente()
var id := "mensch_0" var id := "mensch_0"
Game.active_mission = id Game.active_mission = id
Game.systeme.clear() Game.systeme.clear()
@@ -2023,6 +2116,7 @@ func test_tempo_des_traums() -> void:
func test_mission_abschliessen() -> void: func test_mission_abschliessen() -> void:
abschnitt("Mission abschließen") abschnitt("Mission abschließen")
_ohne_momente()
var id := "mensch_0" var id := "mensch_0"
Game.active_mission = id Game.active_mission = id
Game.progress[id] = 0.0 Game.progress[id] = 0.0
@@ -2042,6 +2136,7 @@ func test_mission_abschliessen() -> void:
func test_welt_waechst() -> void: func test_welt_waechst() -> void:
abschnitt("Erfüllte Träume lassen das Dorf wachsen") abschnitt("Erfüllte Träume lassen das Dorf wachsen")
_ohne_momente()
var zugeordnet: Dictionary = {} var zugeordnet: Dictionary = {}
var doppelt: Array = [] var doppelt: Array = []
for i in range(TechDB.LIST.size()): for i in range(TechDB.LIST.size()):
@@ -2153,6 +2248,7 @@ func _dorf_platz_pruefen() -> void:
func test_techniken_verbessern_sich() -> void: func test_techniken_verbessern_sich() -> void:
abschnitt("Techniken verbessern sich nur durch Anwendung") abschnitt("Techniken verbessern sich nur durch Anwendung")
_ohne_momente()
Game.known.clear() Game.known.clear()
Game.systeme.clear() Game.systeme.clear()
Game.cooldown.clear() Game.cooldown.clear()
@@ -2260,7 +2356,7 @@ func test_steuerung() -> void:
"D schiebt den Spieler nach rechts (x = %.1f)" % main.player.global_position.x) "D schiebt den Spieler nach rechts (x = %.1f)" % main.player.global_position.x)
# Ein geschlossener Klotz hält ihn auf: von der Straße aus gegen die Wand # Ein geschlossener Klotz hält ihn auf: von der Straße aus gegen die Wand
var war_erde := main.auf_erde var war_erde: bool = main.auf_erde
if not war_erde: if not war_erde:
main._erde() main._erde()
await get_tree().process_frame await get_tree().process_frame
@@ -2289,6 +2385,22 @@ func test_steuerung() -> void:
Input.action_release("vor") Input.action_release("vor")
var tiefe_im_klotz: float = halb - (main.player.global_position - kc).dot(richtung) 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)) p(tiefe_im_klotz < 0.6, "Wände halten den Geist auf (%.1f m in der Wand)" % maxf(0.0, tiefe_im_klotz))
# Eingeklemmt zwischen zwei Klötzen: nach zwei Sekunden Drücken steht er wieder auf der Straße
var quer_r := Vector3(-richtung.z, 0, richtung.x)
var breit: float = (kg.x if absf(richtung.z) > 0.0 else kg.y) * 0.5
main.player.global_position = kc + quer_r * (breit + 0.4) + Vector3(0, 0.3, 0)
main.player.velocity = Vector3.ZERO
main.player.spruenge = 0
main.player.yaw = atan2(richtung.x, richtung.z)
Input.action_press("vor")
for i in range(200):
await get_tree().physics_frame
Input.action_release("vor")
var fuge: Vector3 = kc + quer_r * (breit + 0.4)
var weg_von_fuge: float = _xz(main.player.global_position).distance_to(_xz(fuge))
p(main.player.spruenge >= 1 or weg_von_fuge > 3.0,
"In der Fuge zwischen zwei Klötzen bleibt er nicht stecken (%.1f m weg, %d Befreiungen)" %
[weg_von_fuge, main.player.spruenge])
if not war_erde: if not war_erde:
main._traumwelt() # die folgenden Messungen erwarten die Traumwelt main._traumwelt() # die folgenden Messungen erwarten die Traumwelt
await get_tree().process_frame await get_tree().process_frame
@@ -2300,6 +2412,7 @@ func test_steuerung() -> void:
## nicht den ganzen Tag um. ## nicht den ganzen Tag um.
func test_cooldown() -> void: func test_cooldown() -> void:
abschnitt("Techniken haben einen Cooldown") abschnitt("Techniken haben einen Cooldown")
_ohne_momente()
var id := "mensch_0" var id := "mensch_0"
Game.active_mission = id Game.active_mission = id
Game.cooldown.clear() Game.cooldown.clear()
@@ -2387,6 +2500,7 @@ func test_chance_verdeckt() -> void:
## sonst hinge Loop 2 fest: ohne Traum kein neuer Weg, ohne Weg kein Aushang. ## sonst hinge Loop 2 fest: ohne Traum kein neuer Weg, ohne Weg kein Aushang.
func test_brett_nie_leer() -> void: func test_brett_nie_leer() -> void:
abschnitt("Das Brett läuft nie leer") abschnitt("Das Brett läuft nie leer")
_ohne_momente()
var merk_c: Array = Game.completed.duplicate() var merk_c: Array = Game.completed.duplicate()
var merk_k: Dictionary = Game.known.duplicate() var merk_k: Dictionary = Game.known.duplicate()
var leer_bei := -1 var leer_bei := -1
@@ -2744,6 +2858,7 @@ func _erster_traum_tage(saat: int) -> int:
Game.progress.clear() Game.progress.clear()
Game.completed.clear() Game.completed.clear()
Game.active_mission = "" Game.active_mission = ""
_ohne_momente()
Game.welt_erzeugen() Game.welt_erzeugen()
for id in _basiswege(): for id in _basiswege():
Game.learn(str(id)) Game.learn(str(id))
@@ -2794,6 +2909,7 @@ func _bestes_system(id: String) -> int:
func test_erster_traum_schaffbar() -> void: func test_erster_traum_schaffbar() -> void:
abschnitt("Der erste Traum ist mit dem Startkapital zu schaffen") abschnitt("Der erste Traum ist mit dem Startkapital zu schaffen")
_ohne_momente()
Game.completed.clear() Game.completed.clear()
Game.welt_erzeugen() Game.welt_erzeugen()
p(Game.offene_techniken() == 1, p(Game.offene_techniken() == 1,
@@ -2819,6 +2935,7 @@ func test_erster_traum_schaffbar() -> void:
## kommt schneller ans Ziel als wer irgendwo irgendwas einbaut. ## kommt schneller ans Ziel als wer irgendwo irgendwas einbaut.
func test_lesen_lohnt_sich() -> void: func test_lesen_lohnt_sich() -> void:
abschnitt("Die Technik zu lesen zahlt sich aus") abschnitt("Die Technik zu lesen zahlt sich aus")
_ohne_momente()
var id := "mensch_0" var id := "mensch_0"
Game.known.clear() Game.known.clear()
for t in TechDB.LIST: for t in TechDB.LIST: