From e813b5f7aa507ece155d781a13bd45361b1f16a0 Mon Sep 17 00:00:00 2001 From: Team3 Date: Wed, 16 Sep 2026 13:08:59 +0200 Subject: [PATCH] update --- README.md | 17 +++- scripts/bewohner.gd | 3 + scripts/earth_world.gd | 85 +++++++++++++----- scripts/game.gd | 1 + scripts/human_db.gd | 47 +++++++++- scripts/main.gd | 54 +++++++---- scripts/orte.gd | 50 ++++++++++- scripts/player.gd | 63 ++++++++++++- scripts/prims.gd | 6 +- tests/_probe.gd | 91 ------------------- tests/_probe.tscn | 6 -- tests/_probe2.gd | 54 ----------- tests/_probe2.tscn | 6 -- tests/parse.gd | 24 +++++ tests/parse.tscn | 6 ++ tests/run.sh | 20 +++++ tests/verify.gd | 197 ++++++++++++++++++++++++++++++++--------- 17 files changed, 483 insertions(+), 247 deletions(-) delete mode 100644 tests/_probe.gd delete mode 100644 tests/_probe.tscn delete mode 100644 tests/_probe2.gd delete mode 100644 tests/_probe2.tscn create mode 100644 tests/parse.gd create mode 100644 tests/parse.tscn create mode 100755 tests/run.sh diff --git a/README.md b/README.md index b58ccaa..993f48f 100644 --- a/README.md +++ b/README.md @@ -180,9 +180,13 @@ nicht immer — Übung entscheidet. 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 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. + Tempo 3, wo du ihm zu Fuß nie folgen könntest. Steht er weit weg, planst du + den Weg zu ihm über die Straßen, aus deinem Haus heraus und in seines + 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 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 @@ -236,7 +240,12 @@ eintauschen könnte. Ein Erfolg bringt 2 EP, ein Fehlversuch 1 — auch der zäh ## 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/ `verify` fährt das echte Spiel: leeres Traumdorf, einziehende Wesen mit ihren diff --git a/scripts/bewohner.gd b/scripts/bewohner.gd index 6504207..a87fbaf 100644 --- a/scripts/bewohner.gd +++ b/scripts/bewohner.gd @@ -222,6 +222,9 @@ func _process(roh: float) -> void: _aura.scale.z = _aura.scale.x _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 ( Color(1, 0.9, 0.6) if aktiv else Color(1, 1, 1)) diff --git a/scripts/earth_world.gd b/scripts/earth_world.gd index 1eedcae..a930106 100644 --- a/scripts/earth_world.gd +++ b/scripts/earth_world.gd @@ -89,7 +89,9 @@ func _bebauung() -> void: 0.10 + rng.randf() * 0.14, 0.62 + rng.randf() * 0.26) var bau := Node3D.new() 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): _fenster(bau, c + Vector3(0, Orte.hoehe(f), 0), g, ton) 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) -## Ein paar Möbel, damit man sieht, wofür ein Raum da ist. Nichts steht im -## Treppenstreifen. +## Ein paar Möbel, damit man sieht, wofür ein Raum da ist. Gestellt wird im +## 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, 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 + var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[ort]["dir"]] + var hd: float = (groesse.z if absf(d.z) > 0.0 else groesse.x) * 0.5 + var hq: float = (groesse.x if absf(d.z) > 0.0 else groesse.z) * 0.5 + var w := maxf(0.5, hq - 2.0) + var h := maxf(0.5, hd - 2.0) 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)) - 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, 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)) + if _moebel(parent, ort, c, -(hq - 1.15), hd - 1.6, 0.75, Vector3(1.5, 0.15, 2.2), + farbe.darkened(0.5)): + _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)) elif key == "cafe": 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, - Color(0.55, 0.40, 0.30)) - 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, -w * 0.5 + float(i) * w * 0.5, 0.4, 0.4, + Vector3(1.4, 0.8, 1.4), Color(0.55, 0.40, 0.30), true) + _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)) elif key == "buero": 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)) 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)) - 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)) elif key == "uni": for i in range(3): - 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, 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 * 0.6 + float(i) * h * 0.6, 0.5, + Vector3(hq - 1.5, 0.2, 0.7), Color(0.66, 0.62, 0.58)) + _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)) 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)) +## 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 einzige Ort im Bezirk, an dem nicht gebaut wird. func _park() -> void: diff --git a/scripts/game.gd b/scripts/game.gd index 6e175f6..511cfbf 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -32,6 +32,7 @@ var problem_stand: Dictionary = {} # "human:problem" -> gelungene Anwendung var ueberwunden: Dictionary = {} # human_id -> {problem -> true} var gesehen_zaehler: Dictionary = {} # "human:problem" -> wie oft dabeigewesen var completed: Array = [] +var markiert: Dictionary = {} # human_id -> true: wer gerade ein Schild trägt var time_h: float = 7.0 var day: int = 1 var teleport_lock: float = 0.0 diff --git a/scripts/human_db.gd b/scripts/human_db.gd index da066d9..bdd4405 100644 --- a/scripts/human_db.gd +++ b/scripts/human_db.gd @@ -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 ## Ecken. 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 seiten := [ ["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) _strasse(start, ende, ROJI) 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 if p_breite < PARZELLE_MIN * 0.8: 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 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 ## 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: var frei: Array = [] 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) if frei.is_empty(): return diff --git a/scripts/main.gd b/scripts/main.gd index 313abb4..34b3fa2 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -72,10 +72,43 @@ func _process(delta: float) -> void: Game.notiz_sperre = maxf(0.0, Game.notiz_sperre - delta) Game.situationen_ticken(dt, auf_erde and not Game.schlaeft_ziel()) _fokus_suchen() + _markieren() _dabeisein() _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: _fokus = null _fokus_mensch = null @@ -96,7 +129,9 @@ func _fokus_suchen() -> void: if d < bestm: bestm = d _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 elif _fokus != 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. 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 + var bester := _mensch_unter_maus(pos) 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 + player.anhaengen(bester) 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.", Color(1, 0.85, 0.5)) return - player.folgt = _fokus_mensch + player.anhaengen(_fokus_mensch) Game.notified.emit("Du hängst dich an %s." % _fokus_mensch.data["name"], Color(0.7, 0.9, 1)) diff --git a/scripts/orte.gd b/scripts/orte.gd index 9091dfb..cab164b 100644 --- a/scripts/orte.gd +++ b/scripts/orte.gd @@ -140,7 +140,8 @@ static func treppen_decke(places: Dictionary, key: String) -> Array: 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)]) + 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 var ende: float = _podest_a(r, 1) - PODEST var rest := ende + halb_a @@ -176,6 +177,53 @@ static func innen_tuer(places: Dictionary, key: String) -> Vector3: 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. static func gebaeude(places: Dictionary, key: String) -> bool: return places.has(key) and float(places[key]["size"][0]) > 0.0 \ diff --git a/scripts/player.gd b/scripts/player.gd index 7391c83..6ac6d1d 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -11,6 +11,7 @@ const GRAVITY := 24.0 const JUMP := 9.0 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 FOLGE_NAH := 5.0 # näher dran braucht es keinen Anlauf 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 @@ -28,6 +29,9 @@ var arm: Node3D var cam: Camera3D var _dreh_rest := 0.0 # noch zu drehender Winkel (Q) 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 _schleier: MeshInstance3D var _kern: MeshInstance3D @@ -51,7 +55,7 @@ const FUNKEN_MAX := 6 func _ready() -> void: var cs := CollisionShape3D.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 cs.shape = shape cs.position.y = 0.8 @@ -164,12 +168,33 @@ func _physics_process(delta: float) -> void: move_and_slide() if global_position.y < -400.0: global_position.y = 2.0 - + _eingeklemmt(delta, dir.length() > 0.01) if dir.length() > 0.01: var ziel := atan2(-dir.x, -dir.z) 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: return -Vector3(sin(yaw), 0, cos(yaw)) @@ -185,11 +210,33 @@ func _fallen(delta: float) -> void: 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. ## 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 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 d.y = 0.0 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 _fallen(delta) 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 velocity = Vector3.ZERO + _stockt = 0.0 + spruenge += 1 ## Schweben, Funken, Schweif und Modusfarbe -- der Zustand ist am Geist ablesbar. diff --git a/scripts/prims.gd b/scripts/prims.gd index b551cd0..c3ef058 100644 --- a/scripts/prims.gd +++ b/scripts/prims.gd @@ -113,7 +113,8 @@ static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color, var h := size.y var waende: Array = [] 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] for d in seiten: 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) for mi in b["meshes"]: (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 pf := clampi(int(floor((spieler.y - c.y + 0.6) / Orte.ETAGE)), 0, maxi(0, etagen.size() - 1)) for f in range(etagen.size()): diff --git a/tests/_probe.gd b/tests/_probe.gd deleted file mode 100644 index 28509cf..0000000 --- a/tests/_probe.gd +++ /dev/null @@ -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() diff --git a/tests/_probe.tscn b/tests/_probe.tscn deleted file mode 100644 index 039981d..0000000 --- a/tests/_probe.tscn +++ /dev/null @@ -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") diff --git a/tests/_probe2.gd b/tests/_probe2.gd deleted file mode 100644 index 73ef0e1..0000000 --- a/tests/_probe2.gd +++ /dev/null @@ -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) diff --git a/tests/_probe2.tscn b/tests/_probe2.tscn deleted file mode 100644 index 47043d2..0000000 --- a/tests/_probe2.tscn +++ /dev/null @@ -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") diff --git a/tests/parse.gd b/tests/parse.gd new file mode 100644 index 0000000..02ba047 --- /dev/null +++ b/tests/parse.gd @@ -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 diff --git a/tests/parse.tscn b/tests/parse.tscn new file mode 100644 index 0000000..01c1b38 --- /dev/null +++ b/tests/parse.tscn @@ -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") diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..3fecbb3 --- /dev/null +++ b/tests/run.sh @@ -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 diff --git a/tests/verify.gd b/tests/verify.gd index 4f1cac6..51ffb9b 100644 --- a/tests/verify.gd +++ b/tests/verify.gd @@ -5,6 +5,16 @@ var fehler := 0 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: print((" OK " if ok else " FEHLT ") + text) if not ok: @@ -12,53 +22,91 @@ func p(ok: bool, text: String) -> void: func abschnitt(t: String) -> void: + _abschnitt_ende() 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: + 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) main = load("res://scenes/main.tscn").instantiate() add_child(main) await get_tree().process_frame await get_tree().process_frame - await test_start() - await test_journal() - await test_traumdorf() - await test_dorf_organisch() - await test_huegel() - await test_haus_betreten() - await test_hoersaal() - await test_mission_annehmen() - await test_reise_zur_erde() - await test_bezirk() - await test_tagesablauf() - await test_wege_machbar() - await test_geist() - await test_verfolgung() - await test_charakter() - await test_kamera_und_etagen() - await test_gehen_bei_tempo() - await test_diagnose() - await test_notieren_im_spiel() - await test_tagesablauf_anzeige() - await test_ablauf_im_hud() - await test_probleme_und_momente() - await test_eigene_menschen() - await test_systeme() - await test_tempo_des_traums() - await test_cooldown() - await test_chance_verdeckt() - await test_mission_abschliessen() - await test_welt_waechst() - await test_techniken_verbessern_sich() - await test_journal_ueberall() - await test_steuerung() - await test_erster_traum_schaffbar() - await test_brett_nie_leer() - await test_lesen_lohnt_sich() - await test_tempo() + await lauf("test_start") + await lauf("test_journal") + await lauf("test_traumdorf") + await lauf("test_dorf_organisch") + await lauf("test_huegel") + await lauf("test_haus_betreten") + await lauf("test_hoersaal") + await lauf("test_mission_annehmen") + await lauf("test_reise_zur_erde") + await lauf("test_bezirk") + await lauf("test_tagesablauf") + await lauf("test_wege_machbar") + await lauf("test_geist") + await lauf("test_verfolgung") + await lauf("test_charakter") + await lauf("test_kamera_und_etagen") + await lauf("test_gehen_bei_tempo") + await lauf("test_diagnose") + await lauf("test_notieren_im_spiel") + await lauf("test_tagesablauf_anzeige") + await lauf("test_ablauf_im_hud") + await lauf("test_probleme_und_momente") + await lauf("test_eigene_menschen") + await lauf("test_systeme") + await lauf("test_tempo_des_traums") + await lauf("test_cooldown") + await lauf("test_chance_verdeckt") + await lauf("test_mission_abschliessen") + await lauf("test_welt_waechst") + await lauf("test_techniken_verbessern_sich") + await lauf("test_journal_ueberall") + await lauf("test_steuerung") + await lauf("test_erster_traum_schaffbar") + await lauf("test_brett_nie_leer") + await lauf("test_lesen_lohnt_sich") + 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("Fehlende Prüfungen: %d" % fehler) print("=========================================") @@ -817,7 +865,6 @@ 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) @@ -837,7 +884,6 @@ 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 @@ -954,6 +1000,18 @@ func test_kamera_und_etagen() -> void: if Orte.etagen(HumanDB.PLACES, key) > 1: mehr += 1 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 for m in welt().humans: 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") main._klick(schirm) 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 @@ -1160,7 +1252,7 @@ func test_verfolgung() -> void: p(main.player.folgt == null, "Wer selbst losgeht, lässt ihn los") # 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._process(0.05) main._verfolgen() @@ -1963,6 +2055,7 @@ func test_systeme() -> void: ## Je mehr Ursachen abgedeckt sind, desto schneller kommt der Traum voran. func test_tempo_des_traums() -> void: abschnitt("Tempo — je besser die Systeme, desto schneller") + _ohne_momente() var id := "mensch_0" Game.active_mission = id Game.systeme.clear() @@ -2023,6 +2116,7 @@ func test_tempo_des_traums() -> void: func test_mission_abschliessen() -> void: abschnitt("Mission abschließen") + _ohne_momente() var id := "mensch_0" Game.active_mission = id Game.progress[id] = 0.0 @@ -2042,6 +2136,7 @@ func test_mission_abschliessen() -> void: func test_welt_waechst() -> void: abschnitt("Erfüllte Träume lassen das Dorf wachsen") + _ohne_momente() var zugeordnet: Dictionary = {} var doppelt: Array = [] for i in range(TechDB.LIST.size()): @@ -2153,6 +2248,7 @@ func _dorf_platz_pruefen() -> void: func test_techniken_verbessern_sich() -> void: abschnitt("Techniken verbessern sich nur durch Anwendung") + _ohne_momente() Game.known.clear() Game.systeme.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) # 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: main._erde() await get_tree().process_frame @@ -2289,6 +2385,22 @@ func test_steuerung() -> void: 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)) + # 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: main._traumwelt() # die folgenden Messungen erwarten die Traumwelt await get_tree().process_frame @@ -2300,6 +2412,7 @@ func test_steuerung() -> void: ## nicht den ganzen Tag um. func test_cooldown() -> void: abschnitt("Techniken haben einen Cooldown") + _ohne_momente() var id := "mensch_0" Game.active_mission = id 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. func test_brett_nie_leer() -> void: abschnitt("Das Brett läuft nie leer") + _ohne_momente() var merk_c: Array = Game.completed.duplicate() var merk_k: Dictionary = Game.known.duplicate() var leer_bei := -1 @@ -2744,6 +2858,7 @@ func _erster_traum_tage(saat: int) -> int: Game.progress.clear() Game.completed.clear() Game.active_mission = "" + _ohne_momente() Game.welt_erzeugen() for id in _basiswege(): Game.learn(str(id)) @@ -2794,6 +2909,7 @@ func _bestes_system(id: String) -> int: func test_erster_traum_schaffbar() -> void: abschnitt("Der erste Traum ist mit dem Startkapital zu schaffen") + _ohne_momente() Game.completed.clear() Game.welt_erzeugen() p(Game.offene_techniken() == 1, @@ -2819,6 +2935,7 @@ func test_erster_traum_schaffbar() -> void: ## kommt schneller ans Ziel als wer irgendwo irgendwas einbaut. func test_lesen_lohnt_sich() -> void: abschnitt("Die Technik zu lesen zahlt sich aus") + _ohne_momente() var id := "mensch_0" Game.known.clear() for t in TechDB.LIST: