This commit is contained in:
Team3
2026-09-16 16:15:32 +02:00
parent e813b5f7aa
commit 7c4edaa439
19 changed files with 1244 additions and 636 deletions

64
tests/flimmer.gd Normal file
View File

@@ -0,0 +1,64 @@
extends Node
## Flimmer-Probe: Szene anhalten, Kamera um Millimeter rücken, Bilder vergleichen.
## Deckungsgleiche Flächen (Z-Fighting) kippen dabei großflächig, echte Kanten
## wandern nur um Bruchteile eines Pixels.
const GRENZE := 0.4 # Prozent veränderter Pixel, ab der es flimmert
var main: Node3D
func _ready() -> void:
main = load("res://scenes/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
main.ui.schliessen()
Game.time_h = 12.0
var hs := Orte.center(DreamDB.PLACES, "hoersaal")
main.player.global_position = hs + Vector3(1.0, 1.0, 2.0)
var traum := await _messen()
main.player.global_position = Vector3(0, 2.0, 20.0)
var dorf := await _messen()
main._erde()
await get_tree().process_frame
main.player.global_position = Vector3(0, 0.3, 12.0)
var park := await _messen()
var heim := ""
for m in main.welt.humans:
heim = str(m.data["heim"])
break
main.player.global_position = Orte.center(HumanDB.PLACES, heim) + Vector3(0, 0.3, 0)
var wohnung := await _messen()
var schlimmste: float = [traum, dorf, park, wohnung].max()
print("FLIMMERN %s hoersaal=%.2f%% dorf=%.2f%% park=%.2f%% wohnung=%.2f%%" % [
"OK" if schlimmste <= GRENZE else "ZU VIEL", traum, dorf, park, wohnung])
get_tree().quit(0 if schlimmste <= GRENZE else 1)
## Prozent der Pixel, die sich zwischen zwei Bildern bei winziger Kamerabewegung stark ändern.
func _messen() -> float:
for i in range(20):
await get_tree().process_frame
get_tree().paused = true
var cam: Camera3D = main.player.cam
var bilder: Array = []
for i in range(4):
cam.position.x += 0.002
await RenderingServer.frame_post_draw
await RenderingServer.frame_post_draw
var img := get_viewport().get_texture().get_image()
img.resize(img.get_width() / 2, img.get_height() / 2, Image.INTERPOLATE_NEAREST)
bilder.append(img)
cam.position.x -= 0.008
get_tree().paused = false
var schlimm := 0.0
for k in range(1, bilder.size()):
var a: Image = bilder[k - 1]
var b: Image = bilder[k]
var anders := 0
for y in range(0, a.get_height(), 2):
for x in range(0, a.get_width(), 2):
var ca := a.get_pixel(x, y)
var cb := b.get_pixel(x, y)
if absf(ca.r - cb.r) > 0.19 or absf(ca.g - cb.g) > 0.19 or absf(ca.b - cb.b) > 0.19:
anders += 1
var anteil := 100.0 * float(anders) / float((a.get_width() / 2) * (a.get_height() / 2))
schlimm = maxf(schlimm, anteil)
return schlimm

6
tests/flimmer.tscn Normal file
View File

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

37
tests/rauch.gd Normal file
View File

@@ -0,0 +1,37 @@
extends Node
## Rauchtest: das echte Spiel, echte Aushänge-Datei, ein paar Sekunden spielen.
## Jede ERROR-Zeile im Log lässt tests/run.sh durchfallen.
var main: Node3D
func _ready() -> void:
main = load("res://scenes/main.tscn").instantiate()
add_child(main)
await _sekunden(1.5)
main.ui.schliessen()
Input.action_press("vor")
await _sekunden(1.0)
Input.action_release("vor")
main._erde()
await _sekunden(1.5)
var leute: Array = main.welt.humans
if leute.is_empty():
print("RAUCHTEST FEHLT: keine Menschen")
get_tree().quit(1)
return
main.player.anhaengen(leute[0])
await _sekunden(3.0)
main.player.folgt = null
Input.action_press("links")
await _sekunden(1.0)
Input.action_release("links")
main.ui.journal_panel()
await _sekunden(0.5)
main.ui.schliessen()
main._traumwelt()
await _sekunden(1.0)
print("RAUCHTEST OK: %d Menschen, Tag %d %02d:%02d" % [leute.size(), Game.day, int(Game.time_h), int(fmod(Game.time_h, 1.0) * 60.0)])
get_tree().quit()
func _sekunden(s: float) -> void:
await get_tree().create_timer(s).timeout

6
tests/rauch.tscn Normal file
View File

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

View File

@@ -12,6 +12,22 @@ if ! timeout 90 godot --headless --path . tests/parse.tscn > "$log/parse.log" 2>
fi
timeout 600 godot --headless --path . tests/verify.tscn -- "$@" > "$log/suite.log" 2>&1
code=$?
if [ -n "${DISPLAY:-}" ] && [ "${1:-}" != "schnell" ]; then
# Rauchtest: das echte Spiel mit der echten Aushänge-Datei, ein paar Sekunden
timeout 120 godot --path . tests/rauch.tscn > "$log/rauch.log" 2>&1
rauch=$(grep -E "^(SCRIPT )?ERROR:" "$log/rauch.log" | grep -v leaked | sort | uniq -c | sort -rn)
if [ -n "$rauch" ] || ! grep -q "RAUCHTEST OK" "$log/rauch.log"; then
echo "Rauchtest fehlgeschlagen:"; echo "$rauch" | head -5; grep "RAUCHTEST" "$log/rauch.log"; [ $code -eq 0 ] && code=4
else
echo "Rauchtest OK"
fi
# Flimmer-Probe: stehende Szene, Kamera minimal bewegt, Bilder verglichen
timeout 120 godot --path . tests/flimmer.tscn > "$log/flimmer.log" 2>&1
grep "FLIMMERN" "$log/flimmer.log"
if ! grep -q "FLIMMERN OK" "$log/flimmer.log"; then [ $code -eq 0 ] && code=5; fi
else
echo "Rauchtest und Flimmer-Probe übersprungen (kein Display oder Schnelllauf)"
fi
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)

View File

@@ -8,7 +8,7 @@ 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"]
"test_huegel", "test_dorf_organisch", "test_welt_waechst", "test_zufallslauf"]
var schnell := false
var _uhr_abschnitt := 0
var _dauern: Array = []
@@ -46,6 +46,16 @@ func lauf(name: String) -> void:
if schnell and LANGSAM.has(name):
_uebersprungen += 1
return
# Ausgangslage für jeden Abschnitt: Oberfläche zu, niemand verfolgt, kein
# Moment, keine Taste gedrückt -- sonst erbt ein Abschnitt die Fehler des vorigen
ui().schliessen()
main.player.folgt = null
main.player.look_enabled = true
main.player.velocity = Vector3.ZERO
Game.situation = {}
Game.notiz_sperre = 0.0
for t in ["vor", "zurueck", "links", "rechts", "steigen", "drehen"]:
Input.action_release(t)
await Callable(self, name).call()
@@ -71,6 +81,12 @@ func _ready() -> void:
await lauf("test_hoersaal")
await lauf("test_mission_annehmen")
await lauf("test_reise_zur_erde")
await lauf("test_welten_bleiben")
await lauf("test_spuren")
await lauf("test_ankunft")
await lauf("test_akte")
await lauf("test_passanten")
await lauf("test_wesen_helfen")
await lauf("test_bezirk")
await lauf("test_tagesablauf")
await lauf("test_wege_machbar")
@@ -78,6 +94,7 @@ func _ready() -> void:
await lauf("test_verfolgung")
await lauf("test_charakter")
await lauf("test_kamera_und_etagen")
await lauf("test_zufallslauf")
await lauf("test_gehen_bei_tempo")
await lauf("test_diagnose")
await lauf("test_notieren_im_spiel")
@@ -135,7 +152,7 @@ func schritte(n: int, dt: float = 0.05, zeit: float = -1.0) -> void:
func _panel_text() -> String:
var out: Array = []
for c in ui()._content.get_children():
for c in _alle_kinder(ui()._content):
if c is RichTextLabel:
out.append((c as RichTextLabel).get_parsed_text())
return "\n".join(out)
@@ -193,21 +210,21 @@ func _hingehen(ziel: Node3D) -> void:
## Drückt den ersten aktiven Knopf, dessen Beschriftung so beginnt.
func _knopf_texte() -> String:
var out: Array = []
for c in ui()._content.get_children():
for c in _alle_kinder(ui()._content):
if c is Button:
out.append((c as Button).text)
return "\n".join(out)
func _knopf_da(anfang: String) -> bool:
for c in ui()._content.get_children():
for c in _alle_kinder(ui()._content):
if c is Button and (c as Button).text.begins_with(anfang):
return true
return false
func _druecke(anfang: String) -> bool:
for c in ui()._content.get_children():
for c in _alle_kinder(ui()._content):
if c is Button and not (c as Button).disabled and (c as Button).text.begins_with(anfang):
(c as Button).pressed.emit()
return true
@@ -420,6 +437,10 @@ func test_traumdorf() -> void:
if w.place == r[0]:
zuhause += 1
p(zuhause == 4, "Um 01:00 sind alle vier an ihrem Nachtplatz (%d)" % zuhause)
var ohne_netz := 0
for w in welt().bewohner:
ohne_netz += int(w.nav_fehl)
p(ohne_netz == 0, "Auch sie laufen auf dem Navigationsnetz der Insel (%d Rückfälle)" % ohne_netz)
# Ausgangslage wiederherstellen: das Dorf ist wieder leer
Game.completed.clear()
@@ -583,7 +604,7 @@ func test_hoersaal() -> void:
basis_da = false
p(basis_da, "Die %d Basiswege werden gelehrt" % 1)
var lernbar := 0
for c in ui()._content.get_children():
for c in _alle_kinder(ui()._content):
if c is Button and (c as Button).text.begins_with("Diesen Weg"):
lernbar += 1
p(lernbar <= 1, "Und höchstens einer davon ist zu lernen (%d)" % lernbar)
@@ -726,6 +747,237 @@ func test_reise_zur_erde() -> void:
"%d Menschen leben in der Stadt" % welt().humans.size())
## Beide Welten bleiben bestehen: wer zurückkehrt, findet dieselbe Stadt, und
## die Menschen haben derweil ihren Tag weitergelebt.
func test_welten_bleiben() -> void:
abschnitt("Die Welten bleiben bestehen")
var erde_vorher: Node3D = welt()
var mensch: Bewohner = welt().humans[0]
Game.time_h = 9.5
schritte(20, 0.05, 9.5)
var stand: Vector3 = mensch.global_position
main._traumwelt()
await get_tree().process_frame
p(welt() is DreamWorld and is_instance_valid(erde_vorher) and not erde_vorher.visible,
"Im Traum bleibt die Erde bestehen, nur unsichtbar")
var himmel := 0
for we in get_tree().root.find_children("*", "WorldEnvironment", true, false):
if (we as WorldEnvironment).environment != null:
himmel += 1
p(himmel == 1, "Genau ein Himmel ist aktiv (%d)" % himmel)
for i in range(400):
Game.time_h = 12.5
for m in erde_vorher.humans:
m._process(0.05)
main._erde()
await get_tree().process_frame
p(welt() == erde_vorher, "Zurück auf der Erde ist es dieselbe Stadt")
p(mensch.global_position.distance_to(stand) > 3.0,
"%s hat derweil ihren Tag weitergelebt (%.0f m weiter)" % [mensch.data["name"],
mensch.global_position.distance_to(stand)])
p(main.player.collision_mask == 2, "Der Geist stößt nur an die Erde, nicht an die unsichtbare Insel")
## Spuren in der Wohnung: wer hineingeht und hinsieht, kennt die Ursache und
## weiß, welche Stunden im Tag sich lohnen.
func test_spuren() -> void:
abschnitt("Spuren in der Wohnung")
if not main.auf_erde:
main._erde()
await get_tree().process_frame
ui().schliessen()
var sp: Player = main.player
sp.folgt = null
var mensch: Bewohner = welt().humans[0]
var id := str(mensch.data["id"])
var heim := str(mensch.data["heim"])
var ursachen: Array = mensch.data["ursachen"]
Game.active_mission = id
Game.spuren.erase(id)
var spuren: Array = []
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind", "") == "spur" and str(n.get_meta("human")) == id:
spuren.append(n)
p(spuren.size() >= 1 and spuren.size() <= ursachen.size(),
"In %s liegen %d Spuren, je Ursache höchstens eine (%d Ursachen)" %
[HumanDB.PLACES[heim]["name"], spuren.size(), ursachen.size()])
var c := Orte.center(HumanDB.PLACES, heim)
var g: Array = HumanDB.PLACES[heim]["size"]
var drinnen := 0
for n in spuren:
var q: Vector3 = (n as Node3D).global_position
if absf(q.x - c.x) < float(g[0]) * 0.5 and absf(q.z - c.z) < float(g[1]) * 0.5 \
and absf(q.y) < 2.0:
drinnen += 1
p(drinnen == spuren.size(), "Alle liegen in seiner Wohnung")
if spuren.is_empty():
Game.active_mission = ""
return
var spur: Node3D = spuren[0]
var u := str(spur.get_meta("arg"))
p(not Game.ursache_bekannt(id, u), "Vorher ist die Ursache '%s' unbekannt" % Ursachen.LISTE[u]["name"])
var verdaechtig_vorher := 0
for nr in range(Game.slots(id).size()):
if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG:
verdaechtig_vorher += 1
sp.global_position = spur.global_position + Vector3(0.8, 0.3, 0.8)
sp.velocity = Vector3.ZERO
main._process(0.05)
p(main._fokus == spur and ui()._hinweis.text.contains("Spur"),
"Vor der Spur zeigt der Hinweis sie an: %s" % ui()._hinweis.text)
main._interagieren()
p(Game.spur_bekannt(id, u) and Game.ursache_bekannt(id, u),
"E liest die Spur: die Ursache '%s' steht im Journal" % Ursachen.LISTE[u]["name"])
var verdaechtig := 0
for nr in range(Game.slots(id).size()):
if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG:
verdaechtig += 1
p(verdaechtig_vorher == 0 and verdaechtig >= 1,
"Die Stunden dieser Ursache sind jetzt verdächtig (%d), gesehen sind sie nicht" % verdaechtig)
main._process(0.05)
ui()._ablauf_aktualisieren()
p(ui()._ablauf_titel.text.contains("verdächtig"), "Die Zeitleiste sagt es: %s" % ui()._ablauf_titel.text)
p(not Game.spur_lesen(id, u), "Noch einmal lesen bringt nichts Neues")
Game.spuren.erase(id)
Game.active_mission = ""
## Wer mit Mission zur Erde reist, landet vor der Tür seines Ziels; ein Knopf
## bringt ihn jederzeit dorthin.
func test_ankunft() -> void:
abschnitt("Ankunft beim Ziel")
var ziel: Bewohner = welt().humans[0]
var id := str(ziel.data["id"])
Game.active_mission = id
main._traumwelt()
await get_tree().process_frame
main._erde()
await get_tree().process_frame
var nah: float = _xz(main.player.global_position).distance_to(_xz(ziel.global_position))
var tuer := Orte.door(HumanDB.PLACES, ziel.place) if HumanDB.PLACES.has(ziel.place) \
and float(HumanDB.PLACES[ziel.place]["size"][0]) > 0.0 else ziel.global_position
var vor_tuer: float = _xz(main.player.global_position).distance_to(_xz(tuer))
p(minf(nah, vor_tuer) < 4.0 and main.player.global_position.y > -0.5,
"Mit Mission landet man vor der Tür von %s (%.1f m)" % [ziel.data["name"], minf(nah, vor_tuer)])
var andere: Bewohner = welt().humans[3]
main.hinfahren(str(andere.data["id"]))
var d2: float = _xz(main.player.global_position).distance_to(_xz(andere.global_position))
p(d2 < 12.0 and main.auf_erde, "„Hinfahren“ setzt einen zu %s (%.1f m)" % [andere.data["name"], d2])
main._traumwelt()
await get_tree().process_frame
main.hinfahren(id)
p(main.auf_erde, "Aus dem Traum heraus geht es ebenfalls, über das Tor")
Game.active_mission = ""
## Die Akte: ein Blatt je Mensch mit Person, Traum, Befund, Tag und Notizen.
func test_akte() -> void:
abschnitt("Die Akte")
var ziel: Bewohner = welt().humans[0]
var id := str(ziel.data["id"])
var h := HumanDB.by_id(id)
Game.active_mission = id
Game.progress[id] = Game.progress.get(id, 0.0)
Game.spuren.erase(id)
Game.notizen.erase(id)
ui().journal_panel("missionen")
p(_knopf_da(str(h["name"])), "Im Journal liegt eine Akte je Mensch mit Mission")
p(_druecke(str(h["name"])) and ui()._kind == "akte", "Sie lässt sich aufschlagen")
var text := _panel_text()
p(text.contains(str(h["name"])) and text.contains(str(h["traum"])) and text.contains(str(h["problem"])),
"Oben stehen Person, Traum und was im Weg steht")
p(text.contains("BEFUND") and text.contains("TAG") and text.contains("NOTIZEN"),
"Darunter Befund, Tag und Notizen")
p(text.contains("noch keine") and text.contains("noch nichts gefunden"),
"Ohne Spuren und Beobachtungen ist der Befund leer")
p(_knopf_da("Hinfahren"), "Ein Knopf bringt einen vor die Tür")
var u := str((h["ursachen"] as Array)[0])
Game.spur_lesen(id, u)
ui().akte_panel(id)
text = _panel_text()
p(text.contains(str(Ursachen.LISTE[u]["name"])) and text.contains("Spur gelesen"),
"Eine gelesene Spur steht als Befund: %s" % Ursachen.LISTE[u]["name"])
p(text.contains("Uhr —") and text.contains("Spur:"), "Und als Notiz mit Uhrzeit")
ui().schliessen()
main._traumwelt()
await get_tree().process_frame
ui().akte_panel(id)
p(_druecke("Hinfahren") and main.auf_erde and not ui().offen,
"„Hinfahren“ führt aus dem Traum vor die Tür von %s" % h["name"])
Game.active_mission = ""
Game.spuren.erase(id)
Game.notizen.erase(id)
## Passanten beleben die Straßen: ohne Namen, ohne Schild, ohne Mission.
func test_passanten() -> void:
abschnitt("Passanten beleben die Straßen")
if not main.auf_erde:
main._erde()
await get_tree().process_frame
p(welt().passanten.size() == HumanDB.PASSANTEN and welt().humans.size() == HumanDB.LIST.size(),
"%d Passanten neben den %d Menschen mit Geschichte" % [welt().passanten.size(), welt().humans.size()])
var unterwegs := 0
var proben := 0
for i in range(1200):
schritte(1, 0.05, 9.0 + float(i) * 0.004)
if i % 100 == 99:
proben += 1
for pz in welt().passanten:
if not pz._path.is_empty():
unterwegs += 1
p(unterwegs >= proben, "Zu jeder Zeit ist im Schnitt mindestens einer unterwegs (%d in %d Proben)" %
[unterwegs, proben])
var schilder := 0
for pz in welt().passanten:
if (pz._label as Label3D).visible:
schilder += 1
p(schilder == 0, "Passanten tragen kein Schild")
var pz0: Bewohner = welt().passanten[0]
main.player.global_position = pz0.global_position + Vector3(0, 0.3, 0)
main._process(0.05)
p(main._fokus_mensch != pz0, "Und kommen nicht in den Fokus")
## Die Traumwesen helfen: ein Bewohner flüstert, was in der Wohnung des
## Menschen liegt, um den es gerade geht.
func test_wesen_helfen() -> void:
abschnitt("Die Traumwesen helfen")
var merker: Array = Game.completed.duplicate()
Game.completed.clear()
Game.completed.append("mensch_1")
Game.welt_erzeugen()
main._traumwelt()
await get_tree().process_frame
p(welt().bewohner.size() == 1, "Mit einem erfüllten Traum wohnt ein Wesen hier")
var wesen: Bewohner = welt().bewohner[0]
p(str(wesen.data["rolle"]).contains("Traum"), "Es trägt, aus wessen Traum es kam: %s" % wesen.data["rolle"])
var marke: Node3D = null
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind", "") == "wesen":
marke = n
p(marke != null, "Man kann es ansprechen")
Game.active_mission = "mensch_0"
Game.spuren.erase("mensch_0")
main.player.set_mode(Player.Mode.TRAUM)
main.player.global_position = wesen.global_position + Vector3(0.5, 0.3, 0.5)
main.player.velocity = Vector3.ZERO
main._process(0.05)
p(main._fokus == marke and ui()._hinweis.text.contains("zuhören"), "Davor steht: zuhören")
main._interagieren()
var bekannt := 0
for u in Game.ursachen_von("mensch_0"):
if Game.spur_bekannt("mensch_0", str(u)):
bekannt += 1
p(bekannt == 1, "Es flüstert eine Spur des Menschen, um den es geht (%d bekannt)" % bekannt)
Game.spuren.erase("mensch_0")
Game.active_mission = ""
Game.completed.assign(merker)
Game.welt_erzeugen()
main._erde() # die folgenden Abschnitte spielen auf der Erde
await get_tree().process_frame
func test_tagesablauf() -> void:
abschnitt("Tagesablauf der Menschen")
var mensch: Bewohner = null
@@ -734,7 +986,10 @@ func test_tagesablauf() -> void:
mensch = m
p(mensch != null, "Der erste Mensch lebt auf der Erde: %s" % mensch.data["name"])
schritte(30, 0.05, 2.0)
for i in range(3000): # die Welt bleibt bestehen: er kann weit weg sein
schritte(1, 0.05, 2.0)
if mensch.place == str(mensch.data["heim"]) and mensch._path.is_empty():
break
p(mensch.activity == "schlaeft", "Um 02:00 schläft er (%s)" % mensch.activity)
p(_xz(mensch.global_position).distance_to(
_xz(Orte.center(HumanDB.PLACES, str(mensch.data["heim"])))) < 5.0,
@@ -791,12 +1046,11 @@ func test_tagesablauf() -> void:
func test_wege_machbar() -> void:
abschnitt("Wege passen in die Zeitfenster")
_wege_pruefen("Erde", HumanDB.PLACES, HumanDB.LIST, HumanDB.NETZ, HumanDB.ANKER)
_wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST, DreamDB.NETZ, DreamDB.ANKER)
_wege_pruefen("Erde", HumanDB.PLACES, HumanDB.LIST, main.erde.nav_map)
_wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST, main.traum.nav_map)
func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array,
netz: Netz = null, anker: Dictionary = {}) -> void:
func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array, karte: RID) -> void:
var sek_pro_stunde := Game.DAY_SECONDS / 24.0
var alles_ok := true
var engste := 999.0
@@ -808,8 +1062,7 @@ func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array,
var nach: String = String(r[i][1])
if von == nach:
continue
var dauer := _weg_laenge(places, von, nach, m.get("etage", {}), netz, anker) \
/ Bewohner.GEHTEMPO
var dauer := _weg_laenge(places, von, nach, karte) / Bewohner.GEHTEMPO
var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() \
else (24.0 - float(r[i][0]))
var fenster := slot_h * sek_pro_stunde
@@ -824,21 +1077,28 @@ func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array,
p(alles_ok, "%s — jeder Weg passt, engster Fall: %s" % [welt_name, engste_text])
func _weg_laenge(places: Dictionary, von: String, nach: String, etagen: Dictionary,
netz: Netz = null, anker: Dictionary = {}) -> float:
var pts: Array = Orte.route_voll(places, von, nach, Vector3.ZERO, etagen, netz, anker)
## Weglänge über das Navigationsnetz der Welt -- so, wie die Menschen wirklich laufen.
func _weg_laenge(places: Dictionary, von: String, nach: String, karte: RID) -> float:
var a := Orte.center(places, von)
var b := Orte.center(places, nach)
# freie Plätze: neben der Mitte stehen, wie die Bewohner auch (in der Mitte steht der Traumbaum)
if float(places[von]["size"][0]) <= 0.0:
a += Vector3(2.2, 0, 0)
if float(places[nach]["size"][0]) <= 0.0:
b += Vector3(2.2, 0, 0)
var pts := NavigationServer3D.map_get_path(karte, a, b, true)
if pts.size() < 2:
return 9999.0 # kein Weg: fällt als "passt nicht" auf
var l := 0.0
var pos := Orte.center(places, von) + Vector3(0, Orte.hoehe(int(etagen.get(von, 0))), 0)
for pt in pts:
l += pos.distance_to(pt)
pos = pt
for i in range(1, pts.size()):
l += pts[i - 1].distance_to(pts[i])
return l
func test_geist() -> void:
abschnitt("Geist-Bewegung")
p(main.player.mode == Player.Mode.GEIST, "Spieler ist auf der Erde ein Geist")
p(main.player.collision_mask == 1, "Der Geist stößt an Wände wie ein Mensch")
p(main.player.collision_mask != 0, "Der Geist stößt an Wände wie ein Mensch")
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3(6, 0, 0)
for i in range(60):
@@ -848,9 +1108,14 @@ func test_geist() -> void:
"Die Schwerkraft holt ihn auf den Boden (y = %.2f)" % main.player.global_position.y)
var lena: Bewohner = welt().humans[0]
for i in range(1500): # erst, wenn sie irgendwo angekommen ist
if lena._path.is_empty():
break
schritte(1)
var davor: Vector3 = lena.global_position
var uhr_geist := Game.time_h
main.player.global_position = lena.global_position + Vector3(0, 1, 0.5)
schritte(10)
schritte(10, 0.05, uhr_geist)
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
@@ -954,7 +1219,7 @@ func _xz(v: Vector3) -> Vector2:
func test_kamera_und_etagen() -> void:
abschnitt("Feste Kamera, kleine Menschen, Stadt mit Stockwerken")
abschnitt("Feste Kamera, kleine Menschen, geschlossene Häuser")
ui().schliessen()
var sp: Player = main.player
sp.set_mode(Player.Mode.GEIST)
@@ -994,40 +1259,23 @@ func test_kamera_und_etagen() -> void:
p(mensch._visual.scale.x < 0.85 and sp.body.scale.x < 0.7,
"Menschen und Geist sind kleiner (%.2f / %.2f)" % [mensch._visual.scale.x, sp.body.scale.x])
# Stockwerke
var mehr := 0
for key in HumanDB.PLACES:
if Orte.etagen(HumanDB.PLACES, key) > 1:
mehr += 1
p(mehr >= 12, "%d Gebäude haben mehrere Stockwerke" % mehr)
# Geschlossene Häuser: einstöckig, ein Raum mit Tür
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:
if float(sz[0]) > 0.0 and minf(float(sz[0]), float(sz[1])) < 6.0:
eng += 1
p(eng == 0, "Jedes Gebäude hat Platz für Treppenhaus und Decke (%d zu schmal)" % eng)
p(eng == 0, "Jedes Gebäude ist breit genug für Tür und Möbel (%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:
oben = m
break
p(oben != null, "Jemand wohnt über dem Erdgeschoss")
var oben: Bewohner = welt().humans[0]
var heim := str(oben.data["heim"])
var e := oben.etage(heim)
var c := Orte.center(HumanDB.PLACES, heim)
var g: Array = HumanDB.PLACES[heim]["size"]
var t0 := Orte.treppe(HumanDB.PLACES, heim, 0)
var t1 := Orte.treppe(HumanDB.PLACES, heim, 1)
var im_haus: bool = absf(t0.x - c.x) < float(g[0]) * 0.5 and absf(t0.z - c.z) < float(g[1]) * 0.5 \
and absf(t1.x - c.x) < float(g[0]) * 0.5 and absf(t1.z - c.z) < float(g[1]) * 0.5
p(im_haus and is_equal_approx(t1.y, Orte.ETAGE), "Das Treppenhaus steht im Haus, ein Podest je Etage")
var uhr := Game.time_h
var zuhause := false
for i in range(3000):
@@ -1035,80 +1283,34 @@ func test_kamera_und_etagen() -> void:
if oben.place == heim and oben._path.is_empty():
zuhause = true
break
p(zuhause and absf(oben.global_position.y - Orte.hoehe(e)) < 0.2,
"Nachts liegt %s in seiner Wohnung im %d. Stock (y = %.1f)" %
[oben.data["name"], e, oben.global_position.y])
var weg_stunde := -1.0
for r in oben.data["routine"]:
if str(r[1]) != heim:
weg_stunde = float(r[0]) + 0.5
break
var draussen := false
var tief := 99.0
for i in range(3000):
schritte(1, 0.05, weg_stunde)
tief = minf(tief, oben.global_position.y)
if _xz(oben.global_position).distance_to(_xz(c)) > float(g[0]) * 0.5 + 3.0:
draussen = absf(oben.global_position.y) < 0.3
break
p(draussen, "Auf die Straße kommt er über die Treppe wieder nach unten (y = %.1f)" %
oben.global_position.y)
p(zuhause and absf(oben.global_position.y) < 0.3,
"Nachts liegt %s zu Hause, ebenerdig (y = %.1f)" % [oben.data["name"], oben.global_position.y])
# Etagenschnitt
# Schnitt: drinnen verschwinden Dach und die Wände zur Kamera
var bau: Dictionary = {}
for b in welt()._bauten:
if (b["c"] as Vector3).distance_to(c) < 0.1:
bau = b
var n := Orte.etagen(HumanDB.PLACES, heim)
p(not bau.is_empty() and (bau["etagen"] as Array).size() == n,
"Sein Haus steht mit allen %d Etagen da" % n)
sp.global_position = c + Vector3(0, 1.4, 0)
p(not bau.is_empty() and (bau["teile"] as Array).size() == 1 and (bau["waende"] as Array).size() >= 4,
"Sein Haus steht mit Dach und Wänden da")
sp.global_position = c + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
sp.yaw = Player.CAM_YAW
await get_tree().physics_frame
welt()._process(0.05)
var obere_weg := true
for f in range(1, n):
if (bau["etagen"][f] as Node3D).visible:
obere_weg = false
var wand_weg := 0
for w in bau["waende"][0]:
for w in bau["waende"]:
if not (w as Node3D).visible:
wand_weg += 1
p(obere_weg and not (bau["dach"][0] as Node3D).visible,
"Steht der Geist im Erdgeschoss, verschwinden Dach und obere Etagen")
p(wand_weg >= 2 and wand_weg < (bau["waende"][0] as Array).size(),
p(not (bau["teile"][0] as Node3D).visible, "Steht der Geist im Haus, verschwindet das Dach")
p(wand_weg >= 2 and wand_weg < (bau["waende"] as Array).size(),
"Die Wände zur Kamera hin sind weg, die hinteren bleiben (%d von %d Teile)" %
[wand_weg, (bau["waende"][0] as Array).size()])
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)
Input.action_release("vor")
p(hoechste > Orte.ETAGE - 0.3,
"Über die Treppe kommt der Geist in den ersten Stock (y = %.1f)" % hoechste)
sp.global_position = t1 + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
[wand_weg, (bau["waende"] as Array).size()])
sp.global_position = c + Vector3(float(g[0]) + 4.0, 0.3, 0)
await get_tree().physics_frame
welt()._process(0.05)
p((bau["etagen"][0] as Node3D).visible and (bau["etagen"][1] as Node3D).visible
and (n < 3 or not (bau["etagen"][2] as Node3D).visible),
"Im ersten Stock sieht man Erdgeschoss und ersten Stock")
sp.global_position = c + Vector3(float(g[0]) + 4.0, 1.4, 0)
await get_tree().physics_frame
welt()._process(0.05)
var alles := (bau["dach"][0] as Node3D).visible
for f in range(n):
if not (bau["etagen"][f] as Node3D).visible:
alles = false
for w in bau["waende"][0]:
var alles := (bau["teile"][0] as Node3D).visible
for w in bau["waende"]:
if not (w as Node3D).visible:
alles = false
p(alles, "Draußen steht das Haus wieder ganz")
@@ -1124,6 +1326,8 @@ func test_kamera_und_etagen() -> void:
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
for m in welt().humans:
m.nav_fehl = 0 # frühere Tests haben Leute versetzt
var daheim := false
for i in range(3000):
schritte(1, 0.05, 2.0)
@@ -1134,9 +1338,11 @@ func test_kamera_und_etagen() -> void:
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())
await get_tree().physics_frame
await get_tree().physics_frame
p(daheim and sp._leine_pfad.size() >= 4,
"Von weit weg plant das Navigationsnetz den Weg bis in seine Wohnung (%d Punkte)" %
sp._leine_pfad.size())
var angekommen := false
for i in range(2400):
await get_tree().physics_frame
@@ -1156,9 +1362,82 @@ func test_kamera_und_etagen() -> void:
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
var ohne_netz := 0
for m in welt().humans:
ohne_netz += int(m.nav_fehl)
p(ohne_netz == 0, "Alle Wege der Menschen kamen vom Navigationsnetz (%d Rückfälle)" % ohne_netz)
Game.time_h = uhr
## Wie ein Spieler, der einfach losläuft: 90 Sekunden kreuz und quer durch die
## Stadt, bei Wand Richtung wechseln. Dabei darf er nie hängen bleiben, nie
## unter den Boden und nie über die Dächer geraten.
func test_zufallslauf() -> void:
abschnitt("Freies Herumlaufen: nie eingeklemmt, nie unter dem Boden")
if not main.auf_erde:
main._erde()
await get_tree().process_frame
ui().schliessen()
var sp: Player = main.player
sp.set_mode(Player.Mode.GEIST)
sp.look_enabled = true
sp.folgt = null
sp.spruenge = 0
sp.global_position = Vector3(0, 0.3, 12.0)
sp.velocity = Vector3.ZERO
var rng := RandomNumberGenerator.new()
rng.seed = 4711
var richtungen := [["vor"], ["vor", "rechts"], ["rechts"], ["zurueck", "rechts"],
["zurueck"], ["zurueck", "links"], ["links"], ["vor", "links"]]
var kurs := 0
var letzte := sp.global_position
var tiefster := 0.0
var hoechster := 0.0
var still := 0
var laengste_still := 0
var gesamt := 0.0
for runde in range(150):
for t in richtungen[kurs]:
Input.action_press(t)
var gemeldet := false
for i in range(36):
await get_tree().physics_frame
if sp.global_position.y < -0.2 and not gemeldet:
gemeldet = true
var k := sp.get_last_slide_collision()
var txt := "-"
if k != null and k.get_collider() != null:
txt = str((k.get_collider() as Node).get_parent().global_position)
print(" [unter dem Boden] Runde %d, %s, Kollision bei %s" % [runde, str(sp.global_position), txt])
for t in richtungen[kurs]:
Input.action_release(t)
var bewegt := _xz(sp.global_position).distance_to(_xz(letzte))
if bewegt < 0.3 and still >= 2:
print(" [steht] Runde %d bei %s, Kurs %d" % [runde, str(sp.global_position), kurs])
gesamt += bewegt
if bewegt < 0.3:
still += 1
laengste_still = maxi(laengste_still, still)
kurs = (kurs + 3 + rng.randi_range(0, 2)) % 8 # umdrehen, leicht versetzt
else:
still = 0
if rng.randf() < 0.2:
kurs = rng.randi_range(0, 7)
letzte = sp.global_position
tiefster = minf(tiefster, sp.global_position.y)
hoechster = maxf(hoechster, sp.global_position.y)
p(gesamt > 200.0, "Er kommt herum (%.0f m in 90 s)" % gesamt)
p(laengste_still <= 3, "Nie länger als drei Züge ohne Bewegung (längste Pause %d)" % laengste_still)
p(tiefster > -0.5, "Nie unter dem Boden (tiefster y %.2f)" % tiefster)
p(hoechster < 6.0, "Nie über den Dächern (höchster y %.1f)" % hoechster)
p(sp.spruenge == 0, "Nie eingeklemmt -- die Stadt musste ihn nicht befreien (%d)" % sp.spruenge)
var schwebend := 0
for m in welt().humans:
if m._path.is_empty() and m.global_position.y > 0.4:
schwebend += 1
p(schwebend == 0, "Kein stehender Mensch schwebt über dem Boden (%d)" % schwebend)
## Bei hohem Tempo wird der Schritt je Bild größer als die Ankunftsschwelle.
## Wer dann über den Wegpunkt hinausläuft, pendelt um ihn herum statt anzukommen.
func test_gehen_bei_tempo() -> void:
@@ -1671,7 +1950,9 @@ func test_eigene_menschen() -> void:
p(stumm.is_empty(), "Bei allen %d Menschen zeigt sich jede Ursache im Tag%s" %
[HumanDB.LIST.size(), "" if stumm.is_empty() else " — nicht: " + ", ".join(stumm)])
p(Game.machbar(id) or true, "Bereit fürs Brett")
_wege_pruefen("Stadt mit eigenem Menschen", HumanDB.PLACES, HumanDB.LIST)
main._erde()
await get_tree().process_frame
_wege_pruefen("Stadt mit eigenem Menschen", HumanDB.PLACES, HumanDB.LIST, main.erde.nav_map)
# --- Die Wörter verändern, wie er sich verhält
p(Game.verhalten(id, "abwehr") >= 1, "Stur: Abwehr %d" % Game.verhalten(id, "abwehr"))
@@ -2238,10 +2519,26 @@ func _dorf_platz_pruefen() -> void:
for i in range(stufen):
Game.completed.append("stufe_%d" % i)
Game.welt_erzeugen()
_wege_pruefen("Traumdorf mit %d Häusern" % stufen, DreamDB.PLACES, DreamDB.LIST,
DreamDB.NETZ, DreamDB.ANKER)
main._traumwelt()
await get_tree().process_frame
_wege_pruefen("Traumdorf mit %d Häusern" % stufen, DreamDB.PLACES, DreamDB.LIST, main.traum.nav_map)
# Jede Haustür muss zu Fuß erreichbar sein -- keine Klippe vor der Tür
var platz := Orte.center(DreamDB.PLACES, "lichtung") + Vector3(2.2, 0, 0)
var unerreichbar: Array = []
for key in DreamDB.PLACES:
if not str(key).begins_with("haus_"):
continue
var t := Orte.door(DreamDB.PLACES, key)
t.y = DreamDB.hoehe_bei(t.x, t.z)
var pf := NavigationServer3D.map_get_path(main.traum.nav_map, platz, t, true)
if pf.is_empty() or pf[pf.size() - 1].distance_to(t) > 1.5:
unerreichbar.append("%s (Terrasse %.1f)" % [key, DreamDB.terrasse(key)])
p(unerreichbar.is_empty(), "Bei %d Häusern ist jede Haustür vom Dorfplatz aus zu Fuß erreichbar%s" %
[stufen, "" if unerreichbar.is_empty() else " — nicht: " + ", ".join(unerreichbar)])
main._erde()
await get_tree().process_frame
_wege_pruefen("Stadt mit %d Menschen" % HumanDB.LIST.size(), HumanDB.PLACES,
HumanDB.LIST, HumanDB.NETZ, HumanDB.ANKER)
HumanDB.LIST, main.erde.nav_map)
Game.completed.assign(merker)
Game.welt_erzeugen()
@@ -2364,8 +2661,8 @@ func test_steuerung() -> void:
var klotz: Dictionary = {}
var richtung := Vector3.ZERO
for b in welt()._bauten:
if not bool(b.get("voll", false)):
continue
if not (b["waende"] as Array).is_empty():
continue # ein Haus mit Wänden ist kein Klotz
for pz in HumanDB.PARZELLEN:
if (pz["pos"] as Vector3).distance_to(b["c"]) < 0.1:
klotz = b
@@ -2697,7 +2994,8 @@ func test_dorf_organisch() -> void:
sektoren_12 = sekt.size()
if stufe == stufens_probe():
var flaeche := PI * pow(Game.insel_radius(), 2.0)
if flaeche / float(stufe) > 2200.0:
# Terrassen verschiedener Höhe brauchen Böschungen dazwischen: das kostet Fläche
if flaeche / float(stufe) > 3300.0:
dichte = "%.0f m² je Haus" % (flaeche / float(stufe))
p(ueberlappung == "", "Über %d Ausbaustufen steht kein Gebäude im anderen%s" %