update
This commit is contained in:
17
README.md
17
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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()):
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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
24
tests/parse.gd
Normal 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
6
tests/parse.tscn
Normal 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
20
tests/run.sh
Executable 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
|
||||
197
tests/verify.gd
197
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:
|
||||
|
||||
Reference in New Issue
Block a user