update
This commit is contained in:
63
README.md
63
README.md
@@ -35,7 +35,7 @@ Aushang schreiben. **Esc** geht eine Ebene zurück, nicht gleich ganz zu.
|
||||
|
||||
Oben links stehen nur Zeit und Mission. Alles Weitere steht im Journal.
|
||||
In der Traumwelt springt **Leertaste**. Auf der Erde gibt es keinen Sprung:
|
||||
der Geist nimmt die Treppe.
|
||||
der Geist geht zu Fuß.
|
||||
|
||||
## Spielschleife
|
||||
|
||||
@@ -45,6 +45,11 @@ Das Journal führt Buch — es lehrt und vergibt nichts.
|
||||
|
||||
* **Techniken** — was du gelernt hast, mit Stufe und Erfahrung. Besser werden
|
||||
sie nur draußen.
|
||||
* **Akten** — je Mensch mit Mission ein Blatt Papier: Person und Traum, was im
|
||||
Weg steht, der Befund aus Spuren und Beobachtungen, der Tag, die Notizen mit
|
||||
Uhrzeit, ein Stempel, wenn der Traum erfüllt ist. Und der Knopf
|
||||
**Hinfahren**, der dich vor seine Tür setzt. Keine Patientenakte: die Leute
|
||||
sind nicht krank, sie haben Gewohnheiten.
|
||||
* **Missionen** — die Menschen, für die du dich entschieden hast, jeweils mit
|
||||
ihrem **Tagesablauf**: der Tag von 0 bis 23 Uhr, in Abschnitte geteilt. Es
|
||||
gibt keine Wochentage; jeder Tag ist derselbe. Zu Beginn steht in jedem
|
||||
@@ -90,7 +95,7 @@ so, wie der Generator Menschen macht. Eigene Aushänge liegen unter
|
||||
`user://aushaenge.cfg`, überleben den Neustart und ersetzen die letzten
|
||||
erzeugten Einwohner; ein Aushang lässt sich wieder abnehmen.
|
||||
|
||||
**Diagnose und Systeme** — der Kern des Spiels
|
||||
**Diagnose und Routinen** — der Kern des Spiels (im Code heißen Routinen „Systeme“)
|
||||
|
||||
Ein Traum scheitert an Ursachen: *kein Anfang*, *keine Zeit*, *keine Kraft*,
|
||||
*kein Halt*, *kein Grund*. Jeder Mensch hat zwei bis drei davon, nie alle. Die
|
||||
@@ -105,7 +110,7 @@ Gang kommt, räumt auf statt anzufangen, recherchiert oder schreibt Später-List
|
||||
Wer keine Kraft hat, schaut abends eine Serie und bleibt danach wach. Jede
|
||||
dieser Tätigkeiten hat einen **Auslöser** und eine **Belohnung**.
|
||||
|
||||
Gegen eine erkannte Ursache baust du ein System in den Tag:
|
||||
Gegen eine erkannte Ursache baust du eine Routine in den Tag:
|
||||
|
||||
Einführen Ein System, das fehlt — in einen freien Slot.
|
||||
Ändern Eine Tätigkeit, die schadet — mit gleicher Belohnung ersetzt.
|
||||
@@ -154,6 +159,10 @@ nicht immer — Übung entscheidet.
|
||||
Dorfplatz in erträglicher Nähe — und meiden das Tor. Ein Weg führt nie
|
||||
durch ein Haus; findet die Suche keinen, wird feiner gesucht und notfalls
|
||||
woanders angedockt.
|
||||
* **Die Wesen helfen.** Jeder erfüllte Traum bringt ein Wesen ins Dorf, und es
|
||||
trägt, aus wessen Traum es kam. Sprich es an (**E**): es flüstert, was in der
|
||||
Wohnung des Menschen liegt, um den es gerade geht — eine Spur, ohne dass du
|
||||
hingehen musst.
|
||||
* **Schwarzes Brett** am Dorfplatz — hier hängen die Menschen aus, die nicht
|
||||
weiterkommen. Mission annehmen.
|
||||
|
||||
@@ -166,18 +175,26 @@ nicht immer — Übung entscheidet.
|
||||
* **Hier wächst nichts.** Alle 16 Menschen sind von Anfang an da und leben
|
||||
ihren Tag: schlafen, arbeiten, grübeln, essen, spazieren. Wer wo wohnt,
|
||||
arbeitet in der Nähe; das Café liegt in der Mitte.
|
||||
* **Die Stadt ist verschachtelt.** Wohnhäuser und Sonderbauten sind
|
||||
geschlossene Häuser mit zwei bis vier Stockwerken, Treppenhaus neben der
|
||||
Tür, Fenstern und Möbeln. Jeder Mensch hat seine Wohnung in einer Etage,
|
||||
im Büro und an der Hochschule sitzt jeder auf seinem Stockwerk. Die
|
||||
Bewohner gehen die Treppe hoch und runter. Alles andere steht als
|
||||
geschlossener Klotz da.
|
||||
* **Wege kommen vom Navigationsnetz.** Beim ersten Betreten wird aus allen
|
||||
Kollisionsformen ein Navigationsnetz gebacken (unter einer Sekunde): Straßen,
|
||||
Türlücken, Möbel, Klötze — auf der Insel genauso.
|
||||
Menschen und der Geist fragen es nach Pfaden; Türen und Treppen finden sie
|
||||
von allein. Das Straßennetz zeichnet nur noch die Wege.
|
||||
* **Beide Welten bleiben bestehen.** Wer aus dem Traum zurückkehrt, findet
|
||||
dieselbe Stadt; die Menschen haben derweil ihren Tag weitergelebt. Jede Welt
|
||||
liegt auf ihrer eigenen Kollisionsebene mit eigener Navigationskarte.
|
||||
* **Passanten.** Zwölf Leute ohne Geschichte laufen stündlich zu einer anderen
|
||||
Straßenecke oder in den Park: kein Name, kein Schild, keine Mission, nur
|
||||
damit die Straßen nicht leer sind.
|
||||
* **Die Stadt ist einstöckig.** Wohnhäuser und Sonderbauten sind geschlossene
|
||||
Häuser: ein Raum mit Tür, Fenstern, Möbeln und den Spuren seines Bewohners.
|
||||
Alles andere steht als Klotz da, unterschiedlich hoch, damit die Stadt eine
|
||||
Silhouette hat.
|
||||
* Du bist ein unsichtbarer Geist, der eine Handbreit über dem Boden schwebt,
|
||||
sich aber bewegt wie ein Mensch: Wände halten, Türen und Treppen sind die
|
||||
Wege, Schwerkraft gilt. Bist du in einem Haus, greift der **Etagenschnitt**: Dach, Etagen über dir und
|
||||
die Wände zur Kamera hin verschwinden, wie bei den Sims. Ein Haus, das
|
||||
zwischen Kamera und dir steht, wird durchsichtig. Wer auf einer anderen
|
||||
Etage ist, ist nicht im Fokus — durch die Decke sieht man nichts.
|
||||
sich aber bewegt wie ein Mensch: Wände halten, Türen sind die Wege,
|
||||
Schwerkraft gilt. Bist du in einem Haus, verschwinden Dach und die Wände zur
|
||||
Kamera hin, wie bei den Sims. Ein Haus, das zwischen Kamera und dir steht,
|
||||
wird durchsichtig.
|
||||
* **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. Steht er weit weg, planst du
|
||||
@@ -187,6 +204,11 @@ nicht immer — Übung entscheidet.
|
||||
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.
|
||||
* **Spuren in der Wohnung.** Von jeder Ursache liegt etwas in der Wohnung des
|
||||
Menschen herum: das Handy auf dem Boden, der vollgeschriebene Kalender, leere
|
||||
Dosen am Bett, ungeöffnete Briefe, der verstaubte Koffer. Geh hinein, sieh
|
||||
hin, **E** liest die Spur. Die Ursache steht dann im Journal und ihre Stunden
|
||||
werden in der Zeitleiste **verdächtig** — du weißt, wann du dabei sein musst.
|
||||
* **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
|
||||
@@ -194,6 +216,9 @@ nicht immer — Übung entscheidet.
|
||||
Zusehen allein deckt nichts auf — das Bemerken ist der Zug.
|
||||
* Nah herangehen, **E** drücken, Technik wählen. Das Panel nennt für jede
|
||||
Technik nur, ob Thema und Moment passen — keine Zahl.
|
||||
* Wer mit Mission zur Erde reist, landet vor der Tür seines Ziels. Der Fußweg
|
||||
quer durch den Bezirk war Leerlauf; das Gehen zählt in der Wohnung und beim
|
||||
Folgen. Aus der Akte heraus bringt **Hinfahren** dich jederzeit dorthin.
|
||||
* Die Säule im Park bringt dich zurück in die Traumwelt.
|
||||
|
||||
## Ob es gelingt
|
||||
@@ -245,7 +270,15 @@ eintauschen könnte. Ein Erfolg bringt 2 EP, ein Fehlversuch 1 — auch der zäh
|
||||
|
||||
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.
|
||||
misst sie jeden Abschnitt und nennt am Ende die langsamsten. Jede `ERROR:`-
|
||||
Zeile im Log lässt den Lauf durchfallen.
|
||||
|
||||
Mit Display laufen zusätzlich der **Rauchtest** (das echte Spiel samt eigener
|
||||
Aushänge-Datei, ein paar Sekunden spielen, Log muss sauber sein) und die
|
||||
**Flimmer-Probe** (stehende Szene, Kamera um Millimeter gerückt, Bilder
|
||||
verglichen: deckungsgleiche Flächen fallen so auf). Der **Zufallslauf** in der
|
||||
Suite läuft 90 Sekunden kreuz und quer durch die Stadt und prüft, dass der
|
||||
Geist nie hängen bleibt, nie unter den Boden und nie über die Dächer gerät.
|
||||
godot --path . tests/shots.tscn # 28 Screenshots nach user://shots/
|
||||
|
||||
`verify` fährt das echte Spiel: leeres Traumdorf, einziehende Wesen mit ihren
|
||||
|
||||
@@ -12,8 +12,6 @@ var place: String = ""
|
||||
var activity: String = ""
|
||||
var resist_left: float = 0.0
|
||||
var places: Dictionary = {}
|
||||
var netz: Netz = null
|
||||
var anker: Dictionary = {}
|
||||
var mit_mission := false
|
||||
|
||||
var _visual: Node3D
|
||||
@@ -28,18 +26,17 @@ var _path: Array = []
|
||||
var _letzte_aktivitaet: String = ""
|
||||
var boden: Callable = Callable() # Höhe des Bodens (x, z) -> y; leer heißt eben
|
||||
var slot: int = 0
|
||||
var spur: Array = [] # letzte Positionen -- der Geist folgt ihr durch Türen
|
||||
var nav_map: RID # auf der Erde: Pfade vom Navigationsnetz
|
||||
var nav_fehl := 0 # wie oft das Netz keinen Pfad wusste (Rückfall aufs Wegenetz)
|
||||
var nav_fehl_info := ""
|
||||
var _bob: float = 0.0
|
||||
var _offset: Vector3
|
||||
|
||||
|
||||
func setup(d: Dictionary, index: int, orte: Dictionary,
|
||||
wegenetz: Netz = null, wegeanker: Dictionary = {}) -> void:
|
||||
func setup(d: Dictionary, index: int, orte: Dictionary) -> void:
|
||||
data = d
|
||||
idx = index
|
||||
places = orte
|
||||
netz = wegenetz
|
||||
anker = wegeanker
|
||||
mit_mission = bool(d.get("mission", false))
|
||||
name = "Bewohner_" + str(d["id"])
|
||||
var winkel := float(index) * TAU / 4.0
|
||||
@@ -85,20 +82,25 @@ func setup(d: Dictionary, index: int, orte: Dictionary,
|
||||
place = start[0]
|
||||
activity = start[1]
|
||||
_letzte_aktivitaet = activity
|
||||
global_position = Orte.center(places, place) + _offset + Vector3(0, Orte.hoehe(etage(place)), 0)
|
||||
global_position = Orte.center(places, place) + _offset
|
||||
|
||||
|
||||
## Punkt auf der Spur, der mindestens `abstand` hinter dem Menschen liegt.
|
||||
func spur_punkt(abstand: float) -> Vector3:
|
||||
for i in range(spur.size() - 1, -1, -1):
|
||||
if (spur[i] as Vector3).distance_to(global_position) >= abstand:
|
||||
return spur[i]
|
||||
return spur[0] if not spur.is_empty() else global_position
|
||||
|
||||
|
||||
## In welcher Etage dieser Mensch an einem Ort ist -- 0, wenn er dort nur unten ist.
|
||||
func etage(ort: String) -> int:
|
||||
return int((data.get("etage", {}) as Dictionary).get(ort, 0))
|
||||
## Pfad über das Navigationsnetz zu einem Ort -- leer, wenn es keins gibt.
|
||||
func _nav_route(nach: String) -> Array:
|
||||
if not nav_map.is_valid():
|
||||
return []
|
||||
var ziel := Orte.center(places, nach) + _offset
|
||||
var pts := NavigationServer3D.map_get_path(nav_map, global_position, ziel, true)
|
||||
if pts.size() < 2:
|
||||
nav_fehl += 1
|
||||
nav_fehl_info = "%s: %s -> %s von %s nach %s (%d Punkte)" % [data["name"], place, nach,
|
||||
str(global_position), str(ziel), pts.size()]
|
||||
print(" [kein Weg] " + nav_fehl_info)
|
||||
return []
|
||||
var out: Array = []
|
||||
for i in range(1, pts.size()):
|
||||
out.append(pts[i])
|
||||
return out
|
||||
|
||||
|
||||
## Kopfbedeckung nach Tagestyp des Berufs -- so unterscheiden sich die Leute
|
||||
@@ -166,11 +168,9 @@ func _process(roh: float) -> void:
|
||||
var soll := _routine_at(Game.time_h)
|
||||
slot = int(soll[2])
|
||||
if soll[0] != place:
|
||||
var war_unterwegs := not _path.is_empty()
|
||||
_path = Orte.route_voll(places, place, soll[0], _offset,
|
||||
data.get("etage", {}), netz, anker, war_unterwegs)
|
||||
if war_unterwegs and _path.size() > 1:
|
||||
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
|
||||
_path = _nav_route(soll[0])
|
||||
if _path.is_empty(): # kein Netz oder kein Weg: Luftlinie, damit er ankommt
|
||||
_path = [Orte.center(places, soll[0]) + _offset]
|
||||
place = soll[0]
|
||||
activity = "unterwegs"
|
||||
elif _path.is_empty() and activity != soll[1]:
|
||||
@@ -200,10 +200,6 @@ func _process(roh: float) -> void:
|
||||
_haltung(delta, not _path.is_empty())
|
||||
if boden.is_valid():
|
||||
global_position.y = float(boden.call(global_position.x, global_position.z))
|
||||
if spur.is_empty() or (spur.back() as Vector3).distance_to(global_position) > 0.4:
|
||||
spur.append(global_position)
|
||||
if spur.size() > 120:
|
||||
spur.pop_front()
|
||||
|
||||
_visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0,
|
||||
1.0 - pow(0.01, delta))
|
||||
@@ -224,7 +220,8 @@ func _process(roh: float) -> void:
|
||||
_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.visible = (not mit_mission and not bool(data.get("stumm", false))) \
|
||||
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))
|
||||
|
||||
|
||||
@@ -254,7 +254,15 @@ static func _platz_frei(p: Vector3, groesse: Vector2) -> bool:
|
||||
var c := Orte.center(PLACES, key)
|
||||
var dx: float = absf(p.x - c.x) - (groesse.x + bx) * 0.5
|
||||
var dz: float = absf(p.z - c.z) - (groesse.y + bz) * 0.5
|
||||
if dx < HAUS_ABSTAND and dz < HAUS_ABSTAND:
|
||||
# Zwei Terrassen verschiedener Höhe brauchen Platz für eine gehbare Böschung
|
||||
# dazwischen -- sonst steht vor der Tür eine Klippe, und niemand kommt hinauf.
|
||||
# Randstreifen beider plus eine Böschung dazwischen; über 4 m Höhenunterschied
|
||||
# wird die Böschung zu steil (40°), dann muss der Abstand mitwachsen.
|
||||
var dh := absf(hang(p.length()) - terrasse(key))
|
||||
var luft := HAUS_ABSTAND
|
||||
if dh > 0.6:
|
||||
luft += TERRASSE_RAND + BOESCHUNG + maxf(0.0, dh - 4.0) * 1.5
|
||||
if dx < luft and dz < luft:
|
||||
return false
|
||||
for k in NETZ.kanten:
|
||||
if Netz.trifft_rechteck(NETZ.knoten[k[0]], NETZ.knoten[k[1]], p, groesse, WEG_ABSTAND):
|
||||
@@ -511,10 +519,16 @@ static func _bewohner(i: int) -> Dictionary:
|
||||
var ablauf: Array = []
|
||||
for e in ABLAEUFE[i % ABLAEUFE.size()]:
|
||||
ablauf.append([e[0], haus if e[1] == "haus" else e[1], e[2]])
|
||||
var von := str(Game.completed[i]) if i < Game.completed.size() else ""
|
||||
var mensch := HumanDB.by_id(von)
|
||||
var rolle := _rolle(i)
|
||||
if not mensch.is_empty():
|
||||
rolle += " · aus %s Traum" % _besitz(str(mensch["name"]).get_slice(" ", 0))
|
||||
return {
|
||||
"id": "wesen_%d" % i,
|
||||
"name": _name(i),
|
||||
"rolle": _rolle(i),
|
||||
"rolle": rolle,
|
||||
"von": von,
|
||||
"haus": haus,
|
||||
"leuchtet": true,
|
||||
"farbe": Color.from_hsv(fmod(0.72 + float(i) * 0.161, 1.0), 0.55, 0.95),
|
||||
|
||||
@@ -7,10 +7,18 @@ class_name DreamWorld
|
||||
var spawn := Vector3(0, 2.0, 20.0)
|
||||
var bewohner: Array = []
|
||||
var spieler: Node3D = null # setzt main, für den Etagenschnitt
|
||||
var nav_map: RID # Navigationsnetz über Insel, Wege und Häuser
|
||||
var _bauten: Array = []
|
||||
|
||||
var _lichter: Array = []
|
||||
var _freihalten: Array[Vector3] = []
|
||||
var _we: WorldEnvironment
|
||||
var _env: Environment
|
||||
|
||||
|
||||
## Nur die aktive Welt stellt den Himmel; zwei WorldEnvironments vertragen sich nicht.
|
||||
func aktiv_setzen(an: bool) -> void:
|
||||
_we.environment = _env if an else null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -23,13 +31,39 @@ func _ready() -> void:
|
||||
_lichtung()
|
||||
_tor_zur_erde()
|
||||
_gruen()
|
||||
_navigation()
|
||||
_wesen()
|
||||
|
||||
|
||||
## Navigationsnetz aus den Kollisionsformen: Insel, Häuser, Bäume, Laternen.
|
||||
func _navigation() -> void:
|
||||
var region := NavigationRegion3D.new()
|
||||
add_child(region)
|
||||
var nm := NavigationMesh.new()
|
||||
nm.geometry_parsed_geometry_type = NavigationMesh.PARSED_GEOMETRY_STATIC_COLLIDERS
|
||||
nm.agent_radius = 0.4
|
||||
nm.agent_height = 1.6
|
||||
nm.agent_max_climb = 0.35
|
||||
nm.agent_max_slope = 40.0
|
||||
nm.cell_height = 0.1
|
||||
var quelle := NavigationMeshSourceGeometryData3D.new()
|
||||
NavigationServer3D.parse_source_geometry_data(nm, quelle, self)
|
||||
NavigationServer3D.bake_from_source_geometry_data(nm, quelle)
|
||||
nav_map = NavigationServer3D.map_create()
|
||||
NavigationServer3D.map_set_active(nav_map, true)
|
||||
NavigationServer3D.map_set_cell_height(nav_map, 0.1)
|
||||
region.set_navigation_map(nav_map)
|
||||
region.navigation_mesh = nm
|
||||
NavigationServer3D.map_set_use_async_iterations(nav_map, false)
|
||||
NavigationServer3D.map_force_update(nav_map)
|
||||
|
||||
|
||||
## Dämmerung: tiefes Blau oben, warmes Lavendel am Horizont, Nebel im Tal.
|
||||
func _environment() -> void:
|
||||
var we := WorldEnvironment.new()
|
||||
var env := Environment.new()
|
||||
_we = WorldEnvironment.new()
|
||||
var we := _we
|
||||
_env = Environment.new()
|
||||
var env := _env
|
||||
env.background_mode = Environment.BG_SKY
|
||||
var sky := Sky.new()
|
||||
var mat := ProceduralSkyMaterial.new()
|
||||
@@ -182,7 +216,7 @@ func _wege() -> void:
|
||||
for k in DreamDB.NETZ.knoten:
|
||||
var p: Vector3 = k
|
||||
p.y = DreamDB.hoehe_bei(p.x, p.z)
|
||||
Prims.cyl(self, p + Vector3(0, 0.04, 0), 1.3, 0.06, farbe, false)
|
||||
Prims.cyl(self, p + Vector3(0, 0.05, 0), 1.3, 0.06, farbe, false) # knapp über den Bändern
|
||||
if nr % 2 == 0 and Vector2(p.x, p.z).length() > 12.0:
|
||||
_laterne(p + Vector3(1.6, 0, 0))
|
||||
nr += 1
|
||||
@@ -212,7 +246,7 @@ func _haeuser() -> void:
|
||||
Orte.DIR_VEC[pl["dir"]], 3.4, true)
|
||||
var dach := Prims.dach(self, c, groesse, farbe)
|
||||
_bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": groesse.y + 3.0,
|
||||
"etagen": [], "dach": [dach], "waende": [waende]})
|
||||
"teile": [dach], "waende": waende})
|
||||
Prims.label(self, c + Vector3(0, groesse.y + 4.2, 0), pl["name"], 0.6, Color(1, 0.95, 0.85))
|
||||
_einrichtung(c, groesse, farbe, key)
|
||||
|
||||
@@ -463,8 +497,17 @@ func _wesen() -> void:
|
||||
var w := Bewohner.new()
|
||||
add_child(w)
|
||||
w.boden = Callable(DreamDB, "hoehe_bei")
|
||||
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES, DreamDB.NETZ, DreamDB.ANKER)
|
||||
w.nav_map = nav_map
|
||||
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES)
|
||||
bewohner.append(w)
|
||||
# ansprechbar: ein Wesen flüstert, was in der Wohnung des nächsten Menschen liegt
|
||||
var n := Node3D.new()
|
||||
n.position.y = 1.0
|
||||
w.add_child(n)
|
||||
n.add_to_group("interactable")
|
||||
n.set_meta("kind", "wesen")
|
||||
n.set_meta("arg", str(i))
|
||||
n.set_meta("titel", str(DreamDB.LIST[i]["name"]))
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
|
||||
@@ -4,12 +4,17 @@ class_name EarthWorld
|
||||
|
||||
var spawn := Vector3(0, 1.4, 12.0)
|
||||
var humans: Array = []
|
||||
var passanten: Array = [] # Statisten ohne Geschichte
|
||||
var spieler: Node3D = null # setzt main, für den Etagenschnitt
|
||||
var nav_map: RID # Navigationsnetz: Straßen, Türen, Möbel, Klötze
|
||||
const HOEHE := 3.0 # Raumhöhe; Klötze sind Vielfache davon
|
||||
const NAV_ZELLE_HOCH := 0.1
|
||||
var _bauten: Array = [] # alles, was der Etagenschnitt aufschneidet
|
||||
|
||||
var _sun: DirectionalLight3D
|
||||
var _sky: ProceduralSkyMaterial
|
||||
var _env: Environment
|
||||
var _we: WorldEnvironment
|
||||
var _nachtlichter: Array = []
|
||||
|
||||
|
||||
@@ -20,12 +25,41 @@ func _ready() -> void:
|
||||
_orte()
|
||||
_park()
|
||||
_saeule()
|
||||
_navigation()
|
||||
_menschen()
|
||||
_himmel_aktualisieren()
|
||||
|
||||
|
||||
## Das Navigationsnetz wird aus allen Kollisionsformen gebacken: Boden,
|
||||
## Straßen, Wände mit Türlücken, Möbel, Klötze. Wer darauf läuft, findet die
|
||||
## Türen von allein.
|
||||
func _navigation() -> void:
|
||||
var region := NavigationRegion3D.new()
|
||||
add_child(region)
|
||||
var nm := NavigationMesh.new()
|
||||
nm.geometry_parsed_geometry_type = NavigationMesh.PARSED_GEOMETRY_STATIC_COLLIDERS
|
||||
nm.agent_radius = 0.4
|
||||
nm.agent_height = 1.6
|
||||
nm.agent_max_climb = 0.35
|
||||
nm.agent_max_slope = 40.0
|
||||
nm.cell_height = NAV_ZELLE_HOCH # fein, sonst schweben die Leute 25 cm über der Straße
|
||||
var quelle := NavigationMeshSourceGeometryData3D.new()
|
||||
NavigationServer3D.parse_source_geometry_data(nm, quelle, self)
|
||||
NavigationServer3D.bake_from_source_geometry_data(nm, quelle)
|
||||
# eigene Karte: die Traumwelt liegt im selben Raum, ihre Polygone dürfen hier nicht auftauchen
|
||||
nav_map = NavigationServer3D.map_create()
|
||||
NavigationServer3D.map_set_active(nav_map, true)
|
||||
NavigationServer3D.map_set_cell_height(nav_map, NAV_ZELLE_HOCH)
|
||||
region.set_navigation_map(nav_map)
|
||||
region.navigation_mesh = nm
|
||||
# synchron: sonst kommen die ersten Pfade leer zurück
|
||||
NavigationServer3D.map_set_use_async_iterations(nav_map, false)
|
||||
NavigationServer3D.map_force_update(nav_map)
|
||||
|
||||
|
||||
func _environment() -> void:
|
||||
var we := WorldEnvironment.new()
|
||||
_we = WorldEnvironment.new()
|
||||
var we := _we
|
||||
_env = Environment.new()
|
||||
_env.background_mode = Environment.BG_SKY
|
||||
var sky := Sky.new()
|
||||
@@ -46,9 +80,22 @@ func _environment() -> void:
|
||||
add_child(_sun)
|
||||
|
||||
|
||||
const STADTRAND := 140.0 # unsichtbare Wand: hinter dem Bezirk endet die Welt
|
||||
|
||||
func _boden() -> void:
|
||||
Prims.box(self, Vector3(0, -0.53, 0), Vector3(300, 1, 300), Color(0.42, 0.48, 0.40))
|
||||
_strassen()
|
||||
# Stadtrand: wer immer weiter läuft, fiele sonst vom Rand der Welt
|
||||
for d: Vector3 in [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]:
|
||||
var wand := StaticBody3D.new()
|
||||
var cs := CollisionShape3D.new()
|
||||
var form := BoxShape3D.new()
|
||||
form.size = Vector3(4.0, 30.0, STADTRAND * 2.0 + 8.0) if absf(d.x) > 0.0 \
|
||||
else Vector3(STADTRAND * 2.0 + 8.0, 30.0, 4.0)
|
||||
cs.shape = form
|
||||
wand.add_child(cs)
|
||||
wand.position = d * (STADTRAND + 2.0) + Vector3(0, 15.0, 0)
|
||||
add_child(wand)
|
||||
|
||||
|
||||
## Arterien breit, roji schmal -- beide als flaches Band mit Belag.
|
||||
@@ -61,7 +108,9 @@ func _strassen() -> void:
|
||||
continue
|
||||
var breit := float(st["breite"])
|
||||
var farbe := Color(0.62, 0.60, 0.58) if breit > 5.0 else Color(0.55, 0.53, 0.52)
|
||||
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, 0.02, 0),
|
||||
# gestaffelt: Kreuzungen und Gasseneinfahrten flimmern sonst (Z-Fighting)
|
||||
var hoch := 0.02 if breit > 5.0 and absf(d.z) > 0.0 else (0.03 if breit > 5.0 else 0.04)
|
||||
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, hoch, 0),
|
||||
Vector3(breit, 0.06, d.length() + breit), farbe, false)
|
||||
band.rotation.y = atan2(d.x, d.z)
|
||||
|
||||
@@ -84,7 +133,7 @@ func _bebauung() -> void:
|
||||
if minf(g.x, g.y) < 3.0:
|
||||
continue
|
||||
var n := 1 + rng.randi_range(0, 3)
|
||||
var hoehe := Orte.hoehe(n)
|
||||
var hoehe := HOEHE * float(n)
|
||||
var ton := Color.from_hsv(fmod(0.07 + rng.randf() * 0.12, 1.0),
|
||||
0.10 + rng.randf() * 0.14, 0.62 + rng.randf() * 0.26)
|
||||
var bau := Node3D.new()
|
||||
@@ -93,18 +142,17 @@ func _bebauung() -> void:
|
||||
# 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)
|
||||
_fenster(bau, c + Vector3(0, HOEHE * float(f), 0), g, ton)
|
||||
Prims.box(bau, c + Vector3(0, hoehe + 0.2, 0),
|
||||
Vector3(g.x + 0.5, 0.4, g.y + 0.5), ton.darkened(0.25))
|
||||
if rng.randf() < 0.35:
|
||||
Prims.box(bau, c + Vector3(0, hoehe + 0.9, 0),
|
||||
Vector3(g.x * 0.4, 1.0, g.y * 0.4), ton.darkened(0.1))
|
||||
_bauten.append({"c": c, "size": g, "hoch": hoehe, "etagen": [bau], "dach": [],
|
||||
"waende": [], "voll": true})
|
||||
_bauten.append({"c": c, "size": g, "hoch": hoehe, "teile": [bau], "waende": []})
|
||||
|
||||
|
||||
## Wohnungen und Sonderbauten: geschlossene Häuser mit Stockwerken, Treppenhaus
|
||||
## und Möbeln. Der Geist gleitet hinein, der Etagenschnitt öffnet sie ihm.
|
||||
## Wohnungen und Sonderbauten: geschlossene Häuser, ein Raum mit Tür, Möbeln
|
||||
## und Spuren. Der Geist geht hinein, dann verschwinden Dach und Vorderwände.
|
||||
func _orte() -> void:
|
||||
for key in HumanDB.PLACES:
|
||||
var pl: Dictionary = HumanDB.PLACES[key]
|
||||
@@ -112,42 +160,32 @@ func _orte() -> void:
|
||||
if float(s[0]) <= 0.0:
|
||||
continue
|
||||
var c := Orte.center(HumanDB.PLACES, key)
|
||||
var groesse := Vector3(float(s[0]), Orte.ETAGE, float(s[1]))
|
||||
var groesse := Vector3(float(s[0]), HOEHE, float(s[1]))
|
||||
var farbe: Color = pl["color"]
|
||||
var tuer: float = maxf(1.6, minf(3.0, minf(groesse.x, groesse.z) - 3.0))
|
||||
var n := Orte.etagen(HumanDB.PLACES, key)
|
||||
var etagen: Array = []
|
||||
var waende: Array = []
|
||||
for f in range(n):
|
||||
var eb := Node3D.new()
|
||||
add_child(eb)
|
||||
var cf := c + Vector3(0, Orte.hoehe(f), 0)
|
||||
var ton := farbe.lightened(0.06 * float(f))
|
||||
waende.append(Prims.room(eb, cf, groesse, ton, farbe.darkened(0.35),
|
||||
Orte.DIR_VEC[pl["dir"]] if f == 0 else Vector3.ZERO, tuer, f == 0))
|
||||
if f > 0:
|
||||
_decke(eb, key, cf, groesse, farbe.darkened(0.35))
|
||||
_fenster(eb, cf, Vector2(groesse.x, groesse.z), ton)
|
||||
_einrichtung(eb, cf, groesse, farbe, key if f == 0 or HumanDB.ETAGEN_ARBEIT.has(key)
|
||||
else "heim_", key)
|
||||
if f + 1 < n:
|
||||
_treppe(eb, key, f)
|
||||
var licht := OmniLight3D.new()
|
||||
licht.position = cf + Vector3(0, 2.2, 0)
|
||||
licht.omni_range = 12.0
|
||||
licht.light_color = Color(1.0, 0.85, 0.6)
|
||||
licht.light_energy = 0.0
|
||||
eb.add_child(licht)
|
||||
_nachtlichter.append(licht)
|
||||
etagen.append(eb)
|
||||
var raum := Node3D.new()
|
||||
add_child(raum)
|
||||
var waende: Array = Prims.room(raum, c, groesse, farbe, farbe.darkened(0.35),
|
||||
Orte.DIR_VEC[pl["dir"]], tuer, true)
|
||||
_fenster(raum, c, Vector2(groesse.x, groesse.z), farbe)
|
||||
_einrichtung(raum, c, groesse, farbe, key)
|
||||
var wer := HumanDB.bewohner_von(key)
|
||||
if not wer.is_empty():
|
||||
_spuren(raum, key, c, groesse, wer)
|
||||
var licht := OmniLight3D.new()
|
||||
licht.position = c + Vector3(0, 2.2, 0)
|
||||
licht.omni_range = 12.0
|
||||
licht.light_color = Color(1.0, 0.85, 0.6)
|
||||
licht.light_energy = 0.0
|
||||
raum.add_child(licht)
|
||||
_nachtlichter.append(licht)
|
||||
var dach := Node3D.new()
|
||||
add_child(dach)
|
||||
Prims.box(dach, c + Vector3(0, Orte.hoehe(n) + 0.1, 0),
|
||||
Prims.box(dach, c + Vector3(0, HOEHE + 0.1, 0),
|
||||
Vector3(groesse.x + 0.4, 0.2, groesse.z + 0.4), farbe.darkened(0.3))
|
||||
Prims.label(self, c + Vector3(0, Orte.hoehe(n) + 1.6, 0), str(pl["name"]), 0.55,
|
||||
Color(1, 1, 0.9))
|
||||
_bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": Orte.hoehe(n),
|
||||
"etagen": etagen, "dach": [dach], "waende": waende})
|
||||
Prims.label(self, c + Vector3(0, HOEHE + 1.6, 0), str(pl["name"]), 0.55, Color(1, 1, 0.9))
|
||||
_bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": HOEHE,
|
||||
"teile": [dach], "waende": waende})
|
||||
|
||||
|
||||
## Ein Fensterband je Wand, damit man die Stockwerke von außen zählt.
|
||||
@@ -163,51 +201,21 @@ func _fenster(parent: Node, cf: Vector3, g: Vector2, ton: Color) -> void:
|
||||
glas, false)
|
||||
|
||||
|
||||
## Ein Lauf von Podest zu Podest -- die Treppe, die Bewohner und Geist
|
||||
## hochgehen. Sie trägt, deshalb mit Kollision.
|
||||
func _treppe(parent: Node, key: String, f: int) -> void:
|
||||
var lauf := Orte.treppen_lauf(HumanDB.PLACES, key, f)
|
||||
var von: Vector3 = lauf[0]
|
||||
var bis: Vector3 = lauf[1]
|
||||
var d := bis - von
|
||||
var stufe := Prims.box(parent, (von + bis) * 0.5, Vector3(1.6, 0.18, d.length()),
|
||||
Color(0.55, 0.50, 0.46))
|
||||
stufe.basis = Basis.looking_at(d.normalized(), Vector3.UP)
|
||||
var r := Orte.treppen_rahmen(HumanDB.PLACES, key)
|
||||
var podest := Orte.treppe(HumanDB.PLACES, key, f + 1)
|
||||
var mass := Orte._xz(r["a"], Orte.PODEST * 2.0, Orte.TREPPE_TIEFE - 0.8)
|
||||
Prims.box(parent, podest + Vector3(0, -0.09, 0), Vector3(mass.x, 0.18, mass.y),
|
||||
Color(0.55, 0.50, 0.46))
|
||||
|
||||
|
||||
## Der Boden einer oberen Etage lässt über dem Treppenhaus ein Loch frei --
|
||||
## sonst stieße, wer die Treppe hochkommt, mit dem Kopf an die Decke.
|
||||
func _decke(parent: Node, key: String, cf: Vector3, _groesse: Vector3, farbe: Color) -> void:
|
||||
for platte in Orte.treppen_decke(HumanDB.PLACES, key):
|
||||
var mitte: Vector3 = platte[0]
|
||||
var mass: Vector2 = platte[1]
|
||||
mitte.y = cf.y - 0.1
|
||||
Prims.box(parent, mitte, Vector3(mass.x, 0.2, mass.y), farbe)
|
||||
|
||||
|
||||
## Ein paar Möbel, damit man sieht, wofür ein Raum da ist. 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
|
||||
## Rahmen der Tür (quer / zur Tür hin); was in der Türzone stünde, bleibt weg.
|
||||
func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
|
||||
var ort := key
|
||||
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_"):
|
||||
# an der Türwand, links und rechts der Tür -- die Rückwand gehört der Treppe
|
||||
# an der Türwand, links und rechts der Tür
|
||||
_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))
|
||||
if _moebel(parent, ort, c, -(hq - 1.15), hd - 1.6, 0.75, Vector3(1.5, 0.15, 2.2),
|
||||
farbe.darkened(0.5)):
|
||||
farbe.darkened(0.5)) != null:
|
||||
_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":
|
||||
@@ -236,38 +244,64 @@ func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key:
|
||||
Color(0.52, 0.48, 0.46))
|
||||
|
||||
|
||||
## Spuren: was von den Ursachen eines Menschen in seiner Wohnung herumliegt.
|
||||
## Je Ursache eine, an den Seitenwänden hinter Bett und Tisch, ansprechbar mit E.
|
||||
func _spuren(parent: Node, key: String, c: Vector3, groesse: Vector3, wer: Dictionary) -> void:
|
||||
var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[key]["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 plaetze: Array = [[hq - 0.7, hd - 3.4], [-(hq - 0.7), hd - 3.4], [hq - 0.7, hd - 4.4],
|
||||
[-(hq - 0.7), hd - 4.4], [hq - 0.7, hd - 5.4], [-(hq - 0.7), hd - 5.4], [0.0, hd - 3.9]]
|
||||
var formen: Dictionary = {
|
||||
"anfang": [Vector3(0.28, 0.08, 0.5), Color(0.25, 0.28, 0.36)],
|
||||
"zeit": [Vector3(0.08, 0.6, 0.7), Color(0.92, 0.88, 0.78)],
|
||||
"kraft": [Vector3(0.3, 0.28, 0.55), Color(0.75, 0.55, 0.35)],
|
||||
"halt": [Vector3(0.32, 0.18, 0.42), Color(0.9, 0.86, 0.74)],
|
||||
"grund": [Vector3(0.48, 0.28, 0.75), Color(0.45, 0.35, 0.28)],
|
||||
}
|
||||
var naechster := 0
|
||||
for u in wer.get("ursachen", []):
|
||||
var us := str(u)
|
||||
if not formen.has(us):
|
||||
continue
|
||||
var mass: Vector3 = formen[us][0]
|
||||
var farbe: Color = formen[us][1]
|
||||
while naechster < plaetze.size():
|
||||
var pl: Array = plaetze[naechster]
|
||||
naechster += 1
|
||||
var y: float = 1.5 if us == "zeit" else mass.y * 0.5
|
||||
var stueck := _moebel(parent, key, c, float(pl[0]), float(pl[1]), y, mass, farbe, false, 0.9, true)
|
||||
if stueck == null:
|
||||
continue
|
||||
var marke := Node3D.new()
|
||||
stueck.add_child(marke)
|
||||
marke.add_to_group("interactable")
|
||||
marke.set_meta("kind", "spur")
|
||||
marke.set_meta("arg", us)
|
||||
marke.set_meta("human", str(wer["id"]))
|
||||
marke.set_meta("titel", str(Ursachen.LISTE[us]["spur"]))
|
||||
break
|
||||
|
||||
|
||||
## 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.
|
||||
## als (quer, hoch, tief). Steht es im Weg, wird es nicht gestellt (null).
|
||||
func _moebel(parent: Node, ort: String, c: Vector3, aq: float, ad: float, y: float,
|
||||
mass: Vector3, farbe: Color, rund: bool = false) -> bool:
|
||||
mass: Vector3, farbe: Color, rund: bool = false, glanz: float = 0.0,
|
||||
klein: bool = false) -> Node3D:
|
||||
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
|
||||
# Türzone: 2,2 tief hinter der Tür, 4 breit -- Kleinkram darf näher heran
|
||||
var rand := 1.7 if klein else 2.0
|
||||
if ad + mass.z * 0.5 > hd - 2.2 and absf(aq) - mass.x * 0.5 < rand:
|
||||
return null
|
||||
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
|
||||
return Prims.cyl(parent, pos, mass.x * 0.5, mass.y, farbe, true, glanz)
|
||||
return Prims.box(parent, pos, welt_mass, farbe, true, glanz)
|
||||
|
||||
|
||||
## Der mittlere Block bleibt frei: Rasen, Teich, Bäume, Bänke. Der Park ist
|
||||
@@ -319,8 +353,20 @@ func _menschen() -> void:
|
||||
for i in range(HumanDB.LIST.size()):
|
||||
var m := Bewohner.new()
|
||||
add_child(m)
|
||||
m.setup(HumanDB.LIST[i], i, HumanDB.PLACES, HumanDB.NETZ, HumanDB.ANKER)
|
||||
m.setup(HumanDB.LIST[i], i, HumanDB.PLACES)
|
||||
m.nav_map = nav_map
|
||||
humans.append(m)
|
||||
for i in range(HumanDB.PASSANTEN_LISTE.size()):
|
||||
var pz := Bewohner.new()
|
||||
add_child(pz)
|
||||
pz.setup(HumanDB.PASSANTEN_LISTE[i], i + 7, HumanDB.PLACES)
|
||||
pz.nav_map = nav_map
|
||||
passanten.append(pz)
|
||||
|
||||
|
||||
## Nur die aktive Welt stellt den Himmel; zwei WorldEnvironments vertragen sich nicht.
|
||||
func aktiv_setzen(an: bool) -> void:
|
||||
_we.environment = _env if an else null
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
|
||||
@@ -33,6 +33,9 @@ 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 welt_version := 0 # zählt hoch, wenn Dorf oder Stadt neu erzeugt wurden
|
||||
var spuren: Dictionary = {} # human_id -> {ursache -> true}: in der Wohnung gelesen
|
||||
var notizen: Dictionary = {} # human_id -> [Zeilen mit Tag und Uhrzeit], für die Akte
|
||||
var time_h: float = 7.0
|
||||
var day: int = 1
|
||||
var teleport_lock: float = 0.0
|
||||
@@ -151,6 +154,7 @@ func insel_radius() -> float:
|
||||
func welt_erzeugen() -> void:
|
||||
DreamDB.erzeuge(haeuser())
|
||||
HumanDB.erzeuge()
|
||||
welt_version += 1
|
||||
|
||||
|
||||
## Das Traumdorf beginnt leer. Mit jedem erfüllten Traum zieht ein Wesen ein.
|
||||
@@ -352,11 +356,15 @@ func sehen(human_id: String, nr: int) -> void:
|
||||
|
||||
## Der Zustand einer Stunde. HUD und Panel zeichnen denselben -- zwei
|
||||
## Auslegungen würden mit der Zeit auseinanderlaufen.
|
||||
enum Stunde { UNBEKANNT, GESEHEN, SCHLAF, UNGEKLAERT, BENANNT, GEBAUT }
|
||||
enum Stunde { UNBEKANNT, GESEHEN, SCHLAF, UNGEKLAERT, BENANNT, GEBAUT, VERDAECHTIG }
|
||||
|
||||
|
||||
func slot_zustand(human_id: String, nr: int) -> int:
|
||||
if not gesehen_slot(human_id, nr):
|
||||
# eine gelesene Spur macht die Stunden ihrer Ursache verdächtig: dort lohnt es, dabei zu sein
|
||||
var g := slot_grund(human_id, nr)
|
||||
if Ursachen.ist_blocker(g) and spur_bekannt(human_id, Ursachen.ursache_von(g)):
|
||||
return Stunde.VERDAECHTIG
|
||||
return Stunde.UNBEKANNT
|
||||
if not system_in(human_id, nr).is_empty():
|
||||
return Stunde.GEBAUT
|
||||
@@ -405,6 +413,7 @@ func notieren(human_id: String, nr: int) -> String:
|
||||
if not Ursachen.ist_blocker(grund):
|
||||
notiz_sperre = NOTIZ_SPERRE
|
||||
notified.emit("Nichts schief: %s" % Orte.text(grund), Color(1, 0.75, 0.55))
|
||||
notiz(human_id, "Beobachtet: %s — nichts schief" % Orte.text(grund))
|
||||
return "daneben"
|
||||
if erkannt(human_id, nr):
|
||||
notified.emit("Schon notiert: %s" % Orte.text(grund), Color(0.8, 0.85, 1))
|
||||
@@ -414,13 +423,60 @@ func notieren(human_id: String, nr: int) -> String:
|
||||
erkannt_slots[human_id][nr] = true
|
||||
notified.emit("Erkannt: %s — %s" % [Ursachen.BLOCKER[grund]["name"],
|
||||
Ursachen.LISTE[Ursachen.ursache_von(grund)]["name"]], Color(0.8, 0.95, 1))
|
||||
notiz(human_id, "Erkannt: %s (%s)" % [Ursachen.BLOCKER[grund]["name"],
|
||||
Ursachen.LISTE[Ursachen.ursache_von(grund)]["name"]])
|
||||
state_changed.emit()
|
||||
return "treffer"
|
||||
|
||||
|
||||
## Eine Ursache gilt als erkannt, sobald eine ihrer Tätigkeiten aufgedeckt ist.
|
||||
## Nur eine Quelle -- sonst driften Slot und Ursache auseinander.
|
||||
## Ein Traumwesen flüstert eine Spur des Menschen, um den es gerade geht --
|
||||
## das Dorf hilft dem, der ihm seine Bewohner gebracht hat.
|
||||
func wesen_fluestert(index: int) -> String:
|
||||
var wesen: Dictionary = DreamDB.LIST[index] if index < DreamDB.LIST.size() else {"name": "Ein Wesen"}
|
||||
var wer := active_mission
|
||||
if wer == "":
|
||||
var offen := offene_missionen()
|
||||
wer = str(offen[0]["id"]) if not offen.is_empty() else ""
|
||||
if wer == "":
|
||||
return "%s lächelt nur. Nimm dir am Schwarzen Brett jemanden vor." % wesen["name"]
|
||||
var mensch := HumanDB.by_id(wer)
|
||||
for u in ursachen_von(wer):
|
||||
if spur_lesen(wer, str(u)):
|
||||
return "%s flüstert: Bei %s liegt etwas -- %s" % [wesen["name"], mensch["name"],
|
||||
Ursachen.LISTE[u]["spur_text"]]
|
||||
return "%s flüstert: Was bei %s herumliegt, kennst du schon. Geh hin und sieh, wann es geschieht." % \
|
||||
[wesen["name"], mensch["name"]]
|
||||
|
||||
|
||||
## Eine Zeile in die Akte: was wann festgestellt wurde.
|
||||
func notiz(human_id: String, text: String) -> void:
|
||||
if not notizen.has(human_id):
|
||||
notizen[human_id] = []
|
||||
notizen[human_id].append("Tag %d, %02d Uhr — %s" % [day, int(time_h), text])
|
||||
|
||||
|
||||
## Eine Spur in der Wohnung lesen: die Ursache ist damit bekannt, ihre
|
||||
## Stunden im Tag werden verdächtig. Liefert false, wenn sie schon bekannt war.
|
||||
func spur_lesen(human_id: String, ursache: String) -> bool:
|
||||
if spur_bekannt(human_id, ursache):
|
||||
return false
|
||||
if not spuren.has(human_id):
|
||||
spuren[human_id] = {}
|
||||
spuren[human_id][ursache] = true
|
||||
notiz(human_id, "Spur: %s" % Ursachen.LISTE[ursache]["spur"])
|
||||
state_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func spur_bekannt(human_id: String, ursache: String) -> bool:
|
||||
return spuren.get(human_id, {}).has(ursache)
|
||||
|
||||
|
||||
func ursache_bekannt(human_id: String, ursache: String) -> bool:
|
||||
if spur_bekannt(human_id, ursache):
|
||||
return true
|
||||
for nr in erkannt_slots.get(human_id, {}):
|
||||
if Ursachen.ursache_von(slot_grund(human_id, int(nr))) == ursache:
|
||||
return true
|
||||
@@ -507,7 +563,8 @@ func system_setzen(human_id: String, nr: int, tech_id: String, widerstand: bool)
|
||||
var guete := float(pruef["guete"]) * (1.0 + 0.1 * float(verhalten(human_id, "guete")))
|
||||
systeme[human_id][nr] = {"tech": tech_id, "guete": guete}
|
||||
add_xp(tech_id, 2)
|
||||
notified.emit("System steht: %s um %02d:00" %
|
||||
notiz(human_id, "Routine gesetzt: %s" % TechDB.by_id(tech_id)["name"])
|
||||
notified.emit("Neue Routine steht: %s um %02d:00" %
|
||||
[TechDB.by_id(tech_id)["name"], int(slots(human_id)[nr][0])], Color(0.6, 1, 0.7))
|
||||
state_changed.emit()
|
||||
return {"ok": true, "erfolg": true, "guete": float(pruef["guete"])}
|
||||
@@ -699,7 +756,7 @@ func situation_anwenden(tech_id: String) -> Dictionary:
|
||||
ueberwunden[id] = {}
|
||||
ueberwunden[id][problem] = true
|
||||
res["ueberwunden"] = true
|
||||
notified.emit("%s überwunden" % Probleme.name_von(problem), Color(1, 0.85, 0.4))
|
||||
notified.emit("%s — ein Schritt weiter" % Probleme.name_von(problem), Color(1, 0.85, 0.4))
|
||||
state_changed.emit()
|
||||
return res
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ class_name HumanDB
|
||||
## an da und leben ihren Tag.
|
||||
|
||||
const EINWOHNER := 16
|
||||
const PASSANTEN := 12 # Leute ohne Geschichte, die die Straßen beleben
|
||||
const BLOCKER_MAX := 4 # mehr Blocker, und der Tag wäre nur noch kaputt
|
||||
const ACHSEN := [-72.0, -24.0, 24.0, 72.0] # Arterien in x wie in z
|
||||
const ARTERIE := 7.0 # Breite einer Arterie
|
||||
@@ -32,12 +33,10 @@ static var SONDER: Array = [
|
||||
["baeckerei", "Bäckerei Keller", Color(0.85, 0.72, 0.50), 2],
|
||||
["werkstatt", "Werkstatt Loos", Color(0.70, 0.58, 0.55), 1],
|
||||
]
|
||||
## Arbeitsorte, an denen die Leute auf verschiedenen Etagen sitzen. Café,
|
||||
## Bäckerei und Werkstatt spielen sich unten ab.
|
||||
static var ETAGEN_ARBEIT: Array = ["buero", "uni"]
|
||||
|
||||
static var PLACES: Dictionary = {}
|
||||
static var LIST: Array = []
|
||||
static var PASSANTEN_LISTE: Array = []
|
||||
|
||||
## Arbeitsplätze, die ein erzeugter Mensch bekommen kann.
|
||||
static var ARBEIT: Array = ["uni", "cafe", "buero", "baeckerei", "werkstatt"]
|
||||
@@ -200,7 +199,11 @@ static func erzeuge(_ungenutzt: int = 0) -> void:
|
||||
_park_eintragen()
|
||||
_sonderbauten_verteilen()
|
||||
_wohnhaeuser_verteilen()
|
||||
_ecken_eintragen()
|
||||
_anker_setzen()
|
||||
PASSANTEN_LISTE = []
|
||||
for i in range(PASSANTEN):
|
||||
PASSANTEN_LISTE.append(_passant(i))
|
||||
var eigene := Aushaenge.laden()
|
||||
var n_eigen := mini(eigene.size(), EINWOHNER)
|
||||
for i in range(EINWOHNER - n_eigen):
|
||||
@@ -408,6 +411,48 @@ static func _gasse_freischneiden(ab: int, start: Vector3, ende: Vector3, flanke:
|
||||
PARZELLEN.append_array(neu)
|
||||
|
||||
|
||||
## Straßenecken: die Kreuzungen des Netzes, Ziele für Passanten.
|
||||
static func _ecken_eintragen() -> void:
|
||||
var ecken: Array = []
|
||||
for i in range(NETZ.knoten.size()):
|
||||
if NETZ.nachbarn(i).size() >= 3:
|
||||
ecken.append(i)
|
||||
if ecken.size() < 4:
|
||||
for i in range(NETZ.knoten.size()):
|
||||
if not ecken.has(i):
|
||||
ecken.append(i)
|
||||
for k in range(mini(8, ecken.size())):
|
||||
var p: Vector3 = NETZ.knoten[int(ecken[(k * 5) % ecken.size()])]
|
||||
PLACES["ecke_%d" % k] = {
|
||||
"name": "Straßenecke", "pos": [p.x, p.z], "size": [0.0, 0.0],
|
||||
"dir": "none", "color": Color(0.5, 0.5, 0.5),
|
||||
}
|
||||
|
||||
|
||||
## Ein Passant: stündlich eine andere Ecke, dazwischen Park -- kein Name,
|
||||
## kein Schild, keine Mission. Er ist da, damit die Straßen nicht leer sind.
|
||||
static func _passant(i: int) -> Dictionary:
|
||||
var ecken: Array = []
|
||||
for key in PLACES:
|
||||
if str(key).begins_with("ecke_"):
|
||||
ecken.append(str(key))
|
||||
var routine: Array = []
|
||||
var letzte := ""
|
||||
for h in range(24):
|
||||
var ort := "park" if (h + i) % 7 == 3 else str(ecken[(i * 3 + h * 5) % ecken.size()])
|
||||
if ort == letzte:
|
||||
ort = str(ecken[(i * 3 + h * 5 + 1) % ecken.size()])
|
||||
routine.append([h, ort, "unterwegs" if h % 2 == 0 else "wartet"])
|
||||
letzte = ort
|
||||
return {
|
||||
"id": "passant_%d" % i,
|
||||
"name": "Passant",
|
||||
"stumm": true,
|
||||
"farbe": Color.from_hsv(0.58 + float(i % 4) * 0.05, 0.12, 0.55 + float(i % 3) * 0.1),
|
||||
"routine": routine,
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Belegung
|
||||
|
||||
static func _park_eintragen() -> void:
|
||||
@@ -455,13 +500,12 @@ static func _sonderbauten_verteilen() -> void:
|
||||
"size": [p["size"].x, p["size"].y],
|
||||
"dir": p["dir"],
|
||||
"color": SONDER[s][2],
|
||||
"etagen": int(SONDER[s][3]),
|
||||
}
|
||||
|
||||
|
||||
## 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
|
||||
const WOHNUNG_MIN := 7.0 # schmaler passt neben die Tür kein Bett mehr
|
||||
|
||||
static func _wohnhaeuser_verteilen() -> void:
|
||||
var frei: Array = []
|
||||
@@ -487,21 +531,9 @@ static func _wohnhaeuser_verteilen() -> void:
|
||||
"size": [p["size"].x, p["size"].y],
|
||||
"dir": p["dir"],
|
||||
"color": Color.from_hsv(fmod(0.08 + float(i) * 0.113, 1.0), 0.28, 0.78),
|
||||
"etagen": 2 + (i * 5 + 1) % 3,
|
||||
}
|
||||
|
||||
|
||||
## In welcher Etage jemand wohnt und arbeitet. Wohnhäuser sind Mehrfamilien-
|
||||
## häuser: die eigene Wohnung liegt irgendwo im Haus.
|
||||
static func etagen_fuer(i: int, heim: String, arbeit: String) -> Dictionary:
|
||||
var out: Dictionary = {}
|
||||
if PLACES.has(heim):
|
||||
out[heim] = (i * 7 + 3) % Orte.etagen(PLACES, heim)
|
||||
if PLACES.has(arbeit) and ETAGEN_ARBEIT.has(arbeit):
|
||||
out[arbeit] = i % Orte.etagen(PLACES, arbeit)
|
||||
return out
|
||||
|
||||
|
||||
static func _anker_setzen() -> void:
|
||||
for key in PLACES:
|
||||
var s: Array = PLACES[key]["size"]
|
||||
@@ -635,7 +667,6 @@ static func mensch_aus_profil(pr: Dictionary, i: int) -> Dictionary:
|
||||
"eigen": true,
|
||||
"mission": true,
|
||||
"heim": heim,
|
||||
"etage": etagen_fuer(i, heim, arbeit),
|
||||
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
|
||||
"routine": _tag_belegen(TAGESTYPEN[typ], heim, arbeit, bild["blocker"],
|
||||
bild["ursachen"], i),
|
||||
@@ -678,7 +709,6 @@ static func _mensch(i: int) -> Dictionary:
|
||||
"verhalten": {},
|
||||
"mission": true,
|
||||
"heim": heim,
|
||||
"etage": etagen_fuer(i, heim, arbeit),
|
||||
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
|
||||
"routine": _tagesablauf(i, heim, arbeit),
|
||||
}
|
||||
@@ -710,6 +740,13 @@ static func _nachname(i: int) -> String:
|
||||
return str(NACHNAMEN[(i / VORNAMEN.size() + i) % NACHNAMEN.size()])
|
||||
|
||||
|
||||
static func bewohner_von(heim: String) -> Dictionary:
|
||||
for m in LIST:
|
||||
if str(m.get("heim", "")) == heim:
|
||||
return m
|
||||
return {}
|
||||
|
||||
|
||||
static func by_id(id: String) -> Dictionary:
|
||||
for h in LIST:
|
||||
if h["id"] == id:
|
||||
|
||||
104
scripts/main.gd
104
scripts/main.gd
@@ -7,7 +7,11 @@ const REICHWEITE_MENSCH := 3.6
|
||||
|
||||
var player: Player
|
||||
var ui: UI
|
||||
var welt: Node3D
|
||||
var welt: Node3D # die aktive Welt
|
||||
var traum: DreamWorld = null # beide Welten bleiben bestehen und leben weiter
|
||||
var erde: EarthWorld = null
|
||||
var _traum_version := -1
|
||||
var _erde_version := -1
|
||||
var auf_erde := false
|
||||
var _fokus: Node3D = null
|
||||
var _fokus_mensch: Node3D = null
|
||||
@@ -20,6 +24,7 @@ func _ready() -> void:
|
||||
player.look_enabled = false)
|
||||
ui.panel_closed.connect(func() -> void:
|
||||
player.look_enabled = true)
|
||||
ui.hinfahren.connect(hinfahren)
|
||||
|
||||
player = Player.new()
|
||||
add_child(player)
|
||||
@@ -34,28 +39,86 @@ func _ready() -> void:
|
||||
|
||||
func _traumwelt() -> void:
|
||||
auf_erde = false
|
||||
_setze_welt(DreamWorld.new())
|
||||
if traum == null or _traum_version != Game.welt_version:
|
||||
traum = _neu_bauen(traum, DreamWorld.new(), 1) as DreamWorld
|
||||
_traum_version = Game.welt_version
|
||||
_aktivieren(traum)
|
||||
player.set_mode(Player.Mode.TRAUM)
|
||||
player.global_position = (welt as DreamWorld).spawn
|
||||
player.collision_mask = 1
|
||||
player.global_position = traum.spawn
|
||||
player.yaw = Player.CAM_YAW
|
||||
|
||||
|
||||
func _erde() -> void:
|
||||
auf_erde = true
|
||||
_setze_welt(EarthWorld.new())
|
||||
if erde == null or _erde_version != Game.welt_version:
|
||||
erde = _neu_bauen(erde, EarthWorld.new(), 2) as EarthWorld
|
||||
_erde_version = Game.welt_version
|
||||
_aktivieren(erde)
|
||||
player.set_mode(Player.Mode.GEIST)
|
||||
player.global_position = (welt as EarthWorld).spawn
|
||||
player.collision_mask = 2
|
||||
player.global_position = erde.spawn
|
||||
player.yaw = Player.CAM_YAW
|
||||
if Game.active_mission != "":
|
||||
zum_ziel(Game.active_mission, false) # wer mit Mission reist, landet vor der Tür seines Ziels
|
||||
|
||||
|
||||
func _setze_welt(neu: Node3D) -> void:
|
||||
if welt != null:
|
||||
remove_child(welt)
|
||||
welt.queue_free()
|
||||
## Vor die Tür eines Menschen setzen -- steht er draußen, neben ihn. Der Fußweg
|
||||
## quer durch den Bezirk war Leerlauf; das Gehen zählt in der Wohnung und beim Folgen.
|
||||
func zum_ziel(id: String, melden: bool = true) -> void:
|
||||
var wer: Bewohner = null
|
||||
for h in erde.humans:
|
||||
if str(h.data["id"]) == id:
|
||||
wer = h
|
||||
if wer == null:
|
||||
return
|
||||
var p: Vector3
|
||||
var drinnen: bool = HumanDB.PLACES.has(wer.place) \
|
||||
and float(HumanDB.PLACES[wer.place]["size"][0]) > 0.0 and wer._path.is_empty()
|
||||
if drinnen:
|
||||
p = Orte.door(HumanDB.PLACES, wer.place)
|
||||
else:
|
||||
p = wer.global_position + Vector3(2.0, 0, 0)
|
||||
p = NavigationServer3D.map_get_closest_point(erde.nav_map, p)
|
||||
player.global_position = p + Vector3(0, 0.3, 0)
|
||||
player.velocity = Vector3.ZERO
|
||||
player.folgt = null
|
||||
Game.teleport_lock = 1.0
|
||||
if melden:
|
||||
Game.notified.emit("Du stehst %s %s." % ["vor der Tür von" if drinnen else "bei",
|
||||
wer.data["name"]], Color(0.7, 0.9, 1))
|
||||
|
||||
|
||||
## Knopf in der Akte: zur Erde und vor die Tür.
|
||||
func hinfahren(id: String) -> void:
|
||||
if not auf_erde:
|
||||
_erde()
|
||||
zum_ziel(id)
|
||||
|
||||
|
||||
## Eine Welt neu bauen -- nur wenn Dorf oder Stadt sich geändert haben. Jede
|
||||
## Welt liegt auf ihrer eigenen Kollisionsebene, denn beide teilen sich den Raum.
|
||||
func _neu_bauen(alt: Node3D, neu: Node3D, ebene: int) -> Node3D:
|
||||
if alt != null:
|
||||
remove_child(alt)
|
||||
alt.queue_free()
|
||||
add_child(neu)
|
||||
move_child(neu, 0)
|
||||
for k in neu.find_children("*", "CollisionObject3D", true, false):
|
||||
(k as CollisionObject3D).collision_layer = 1 << (ebene - 1)
|
||||
return neu
|
||||
|
||||
|
||||
## Die andere Welt bleibt da und lebt weiter, nur unsichtbar und ohne Himmel.
|
||||
func _aktivieren(neu: Node3D) -> void:
|
||||
for w in [traum, erde]:
|
||||
if w == null:
|
||||
continue
|
||||
var an: bool = w == neu
|
||||
w.visible = an
|
||||
w.set("spieler", player if an else null)
|
||||
w.call("aktiv_setzen", an)
|
||||
welt = neu
|
||||
add_child(welt)
|
||||
move_child(welt, 0)
|
||||
welt.set("spieler", player) # für den Etagenschnitt
|
||||
Game.teleport_lock = 1.0
|
||||
_fokus = null
|
||||
_fokus_mensch = null
|
||||
@@ -115,6 +178,8 @@ func _fokus_suchen() -> void:
|
||||
var p := player.global_position
|
||||
var best := REICHWEITE_OBJEKT
|
||||
for n in get_tree().get_nodes_in_group("interactable"):
|
||||
if not welt.is_ancestor_of(n):
|
||||
continue # die andere Welt liegt im selben Raum
|
||||
var d: float = p.distance_to((n as Node3D).global_position)
|
||||
if d < best:
|
||||
best = d
|
||||
@@ -123,8 +188,6 @@ func _fokus_suchen() -> void:
|
||||
var bestm := REICHWEITE_MENSCH
|
||||
for m in (welt as EarthWorld).humans:
|
||||
var mp: Vector3 = (m as Node3D).global_position + Vector3(0, 1.0, 0)
|
||||
if absf(mp.y - p.y) > 2.0:
|
||||
continue # andere Etage: durch die Decke sieht man nichts
|
||||
var d: float = p.distance_to(mp)
|
||||
if d < bestm:
|
||||
bestm = d
|
||||
@@ -156,6 +219,10 @@ func _hinweis_setzen() -> void:
|
||||
ui.setze_hinweis("[E] %s — Techniken lernen" % titel)
|
||||
"brett":
|
||||
ui.setze_hinweis("[E] Schwarzes Brett — Mission annehmen")
|
||||
"spur":
|
||||
ui.setze_hinweis("[E] Spur: %s" % titel)
|
||||
"wesen":
|
||||
ui.setze_hinweis("[E] %s — zuhören" % titel)
|
||||
_:
|
||||
ui.setze_hinweis("[E] %s" % titel)
|
||||
else:
|
||||
@@ -298,3 +365,12 @@ func _interagieren() -> void:
|
||||
ui.lehre_panel(str(_fokus.get_meta("arg")))
|
||||
"brett":
|
||||
ui.brett_panel()
|
||||
"wesen":
|
||||
Game.notified.emit(Game.wesen_fluestert(int(str(_fokus.get_meta("arg")))), Color(0.85, 0.8, 1))
|
||||
"spur":
|
||||
var wer := str(_fokus.get_meta("human"))
|
||||
var u := str(_fokus.get_meta("arg"))
|
||||
var neu := Game.spur_lesen(wer, u)
|
||||
Game.notified.emit("%s%s — %s" % ["" if neu else "Schon gesehen: ",
|
||||
Ursachen.LISTE[u]["spur"], Ursachen.LISTE[u]["spur_text"]],
|
||||
Color(1, 0.9, 0.6) if neu else Color(0.8, 0.85, 1))
|
||||
|
||||
242
scripts/orte.gd
242
scripts/orte.gd
@@ -48,220 +48,6 @@ static func kennt(a: String) -> bool:
|
||||
return ACTIVITY_TEXT.has(a) or Ursachen.ist_blocker(a)
|
||||
|
||||
|
||||
const ETAGE := 3.0 # Höhe eines Stockwerks
|
||||
|
||||
|
||||
static func etagen(places: Dictionary, key: String) -> int:
|
||||
return maxi(1, int(places[key].get("etagen", 1)))
|
||||
|
||||
|
||||
static func hoehe(e: int) -> float:
|
||||
return float(e) * ETAGE
|
||||
|
||||
|
||||
const TREPPE_LAUF := 6.0 # Abstand der Wendepodeste entlang der Wand
|
||||
const TREPPE_TIEFE := 4.4 # Streifen an der Treppenwand: zwei Läufe nebeneinander
|
||||
const PODEST := 0.9 # halbe Podestlänge
|
||||
|
||||
## Rahmen des Treppenhauses: an der längeren Wand, die nicht die Türwand ist.
|
||||
## n zeigt von der Mitte zur Treppenwand, a läuft an ihr entlang.
|
||||
static func treppen_rahmen(places: Dictionary, key: String) -> Dictionary:
|
||||
var pl: Dictionary = places[key]
|
||||
var c := center(places, key)
|
||||
var d: Vector3 = DIR_VEC[pl["dir"]]
|
||||
var size: Array = pl["size"]
|
||||
var quer_ist := absf(d.z) > 0.0
|
||||
var halb_d: float = (float(size[1]) if quer_ist else float(size[0])) * 0.5
|
||||
var halb_q: float = (float(size[0]) if quer_ist else float(size[1])) * 0.5
|
||||
var quer := Vector3(-d.z, 0, d.x)
|
||||
if halb_q >= halb_d:
|
||||
return {"c": c, "n": -d, "a": quer, "halb_n": halb_d, "halb_a": halb_q}
|
||||
return {"c": c, "n": -quer, "a": -d, "halb_n": halb_q, "halb_a": halb_d}
|
||||
|
||||
|
||||
static func _lauf(r: Dictionary) -> float:
|
||||
return minf(TREPPE_LAUF, float(r["halb_a"]) * 2.0 - 2.8)
|
||||
|
||||
|
||||
static func _podest_a(r: Dictionary, e: int) -> float:
|
||||
var p0: float = float(r["halb_a"]) - 1.4
|
||||
return p0 if e % 2 == 0 else p0 - _lauf(r)
|
||||
|
||||
|
||||
## Wendepodest der Etage e: zwei Läufe liegen nebeneinander, das Podest
|
||||
## verbindet sie. Der obere Lauf ist so nie die Decke des unteren.
|
||||
static func treppe(places: Dictionary, key: String, e: int) -> Vector3:
|
||||
var r := treppen_rahmen(places, key)
|
||||
var p: Vector3 = r["c"] + r["n"] * (float(r["halb_n"]) - TREPPE_TIEFE * 0.5) \
|
||||
+ r["a"] * _podest_a(r, e)
|
||||
p.y = hoehe(e)
|
||||
return p
|
||||
|
||||
|
||||
## Ein Lauf: [Anfang auf Etage f, Ende auf Etage f + 1].
|
||||
static func treppen_lauf(places: Dictionary, key: String, f: int) -> Array:
|
||||
var r := treppen_rahmen(places, key)
|
||||
var tiefe: float = float(r["halb_n"]) - (1.4 if f % 2 == 0 else 3.0)
|
||||
var richtung := -1.0 if f % 2 == 0 else 1.0
|
||||
var von: Vector3 = r["c"] + r["n"] * tiefe + r["a"] * (_podest_a(r, f) + richtung * PODEST)
|
||||
var bis: Vector3 = r["c"] + r["n"] * tiefe + r["a"] * (_podest_a(r, f + 1) - richtung * PODEST)
|
||||
von.y = hoehe(f)
|
||||
bis.y = hoehe(f + 1)
|
||||
return [von, bis]
|
||||
|
||||
|
||||
## Wegpunkte durchs Treppenhaus von Etage a nach Etage b, ohne a selbst:
|
||||
## Laufanfang, Laufende, Podest -- je Etage.
|
||||
static func treppen_weg(places: Dictionary, key: String, a: int, b: int) -> Array:
|
||||
var out: Array = []
|
||||
if b > a:
|
||||
for f in range(a, b):
|
||||
var l := treppen_lauf(places, key, f)
|
||||
out.append(l[0])
|
||||
out.append(l[1])
|
||||
out.append(treppe(places, key, f + 1))
|
||||
else:
|
||||
for f in range(a - 1, b - 1, -1):
|
||||
var l := treppen_lauf(places, key, f)
|
||||
out.append(l[1])
|
||||
out.append(l[0])
|
||||
out.append(treppe(places, key, f))
|
||||
return out
|
||||
|
||||
|
||||
## Deckenplatten einer oberen Etage: alles außer dem Treppenloch.
|
||||
## Liefert [[mitte, groesse_xz], ...] ohne Höhe.
|
||||
static func treppen_decke(places: Dictionary, key: String) -> Array:
|
||||
var r := treppen_rahmen(places, key)
|
||||
var c: Vector3 = r["c"]
|
||||
var n: Vector3 = r["n"]
|
||||
var a: Vector3 = r["a"]
|
||||
var halb_n := float(r["halb_n"])
|
||||
var halb_a := float(r["halb_a"])
|
||||
var out: Array = []
|
||||
# Hauptplatte: alles außer dem Streifen an der Treppenwand
|
||||
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
|
||||
if rest > 0.3:
|
||||
out.append([c + n * (halb_n - TREPPE_TIEFE * 0.5) + a * ((ende - halb_a) * 0.5),
|
||||
_xz(a, rest, TREPPE_TIEFE)])
|
||||
return out
|
||||
|
||||
|
||||
## Kastenmaß in Weltachsen: laenge_a entlang a, laenge_n quer dazu.
|
||||
static func _xz(a: Vector3, laenge_a: float, laenge_n: float) -> Vector2:
|
||||
return Vector2(laenge_a, laenge_n) if absf(a.x) > 0.0 else Vector2(laenge_n, laenge_a)
|
||||
|
||||
|
||||
## Rückt einen Punkt aus dem Treppenstreifen heraus (für Möbel).
|
||||
static func frei_von_treppe(places: Dictionary, key: String, pos: Vector3, halb: float) -> Vector3:
|
||||
var r := treppen_rahmen(places, key)
|
||||
var n: Vector3 = r["n"]
|
||||
var pn: float = (pos - r["c"]).dot(n)
|
||||
var grenze: float = float(r["halb_n"]) - TREPPE_TIEFE - halb - 0.2
|
||||
if pn > grenze:
|
||||
pos -= n * (pn - grenze)
|
||||
return pos
|
||||
|
||||
|
||||
## Punkt gleich hinter der Tür, im Haus.
|
||||
static func innen_tuer(places: Dictionary, key: String) -> Vector3:
|
||||
var pl: Dictionary = places[key]
|
||||
var c := center(places, key)
|
||||
var d: Vector3 = DIR_VEC[pl["dir"]]
|
||||
var size: Array = pl["size"]
|
||||
var halb: float = (float(size[1]) if absf(d.z) > 0.0 else float(size[0])) * 0.5
|
||||
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 \
|
||||
and DIR_VEC[places[key]["dir"]] != Vector3.ZERO
|
||||
|
||||
|
||||
## Route mit allem, was ein Körper braucht: aus der Etage runter, gerade durch
|
||||
## die Tür, über das Netz, durch die nächste Tür und die Treppe hinauf.
|
||||
## `etagen` sagt, in welcher Etage der Mensch an welchem Ort ist.
|
||||
static func route_voll(places: Dictionary, a: String, b: String, offset: Vector3,
|
||||
etagen: Dictionary, netz: Netz = null, anker: Dictionary = {},
|
||||
war_unterwegs: bool = false) -> Array:
|
||||
var pts := route(places, a, b, offset, netz, anker)
|
||||
if pts.is_empty():
|
||||
return pts
|
||||
if gebaeude(places, a) and not war_unterwegs:
|
||||
var e_von := int(etagen.get(a, 0))
|
||||
var runter: Array = []
|
||||
if e_von > 0:
|
||||
runter.append(treppe(places, a, e_von))
|
||||
runter.append_array(treppen_weg(places, a, e_von, 0))
|
||||
runter.append(innen_tuer(places, a))
|
||||
runter.append(door(places, a))
|
||||
runter.append_array(pts)
|
||||
pts = runter
|
||||
if gebaeude(places, b):
|
||||
var e_nach := int(etagen.get(b, 0))
|
||||
var ziel: Vector3 = pts.pop_back()
|
||||
pts.append(door(places, b))
|
||||
pts.append(innen_tuer(places, b))
|
||||
if e_nach > 0:
|
||||
pts.append(treppe(places, b, 0))
|
||||
pts.append_array(treppen_weg(places, b, 0, e_nach))
|
||||
ziel.y = hoehe(e_nach)
|
||||
pts.append(ziel)
|
||||
return pts
|
||||
|
||||
|
||||
static func center(places: Dictionary, key: String) -> Vector3:
|
||||
var p: Array = places[key]["pos"]
|
||||
return Vector3(p[0], 0.0, p[1])
|
||||
@@ -277,31 +63,3 @@ static func door(places: Dictionary, key: String) -> Vector3:
|
||||
var size: Array = pl["size"]
|
||||
var half: float = size[1] * 0.5 if absf(d.z) > 0.0 else size[0] * 0.5
|
||||
return c + d * (half + 2.5)
|
||||
|
||||
|
||||
## Route von Ort a nach Ort b. Gibt es ein Wegenetz, wird darauf gelaufen --
|
||||
## sonst über den freien Platz in der Mitte.
|
||||
static func route(places: Dictionary, a: String, b: String, offset: Vector3,
|
||||
netz: Netz = null, anker: Dictionary = {}) -> Array:
|
||||
var pts: Array = []
|
||||
if a == b:
|
||||
return pts
|
||||
if netz != null and anker.has(a) and anker.has(b):
|
||||
var weg := netz.pfad(int(anker[a]), int(anker[b]))
|
||||
if not weg.is_empty():
|
||||
for idx in weg:
|
||||
var k: Vector3 = netz.knoten[idx]
|
||||
if pts.is_empty() or pts[pts.size() - 1].distance_to(k) > 0.6:
|
||||
pts.append(k)
|
||||
pts.append(center(places, b) + offset)
|
||||
return pts
|
||||
var da := door(places, a)
|
||||
var db := door(places, b)
|
||||
pts.append(da)
|
||||
if da.length() > 17.0:
|
||||
pts.append(da.normalized() * 15.0)
|
||||
if db.length() > 17.0:
|
||||
pts.append(db.normalized() * 15.0)
|
||||
pts.append(db)
|
||||
pts.append(center(places, b) + offset)
|
||||
return pts
|
||||
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
@@ -29,9 +28,12 @@ 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 _leine_pfad: Array = [] # Pfad zum Verfolgten, alle 0,4 s neu
|
||||
var _leine_uhr := 0.0
|
||||
var _verloren := 0.0 # Sekunden zu weit weg vom Verfolgten
|
||||
var _stockt := 0.0 # Sekunden ohne Vorankommen (eingeklemmt)
|
||||
var _vorher := Vector3.ZERO # Position im letzten Physikbild
|
||||
var spruenge := 0 # wie oft Leine oder Stadt nachziehen mussten
|
||||
var body: Node3D
|
||||
var _schleier: MeshInstance3D
|
||||
var _kern: MeshInstance3D
|
||||
@@ -111,8 +113,7 @@ func _ready() -> void:
|
||||
func set_mode(m: int) -> void:
|
||||
mode = m
|
||||
folgt = null
|
||||
collision_layer = 1
|
||||
collision_mask = 1
|
||||
collision_layer = 1 # die Maske stellt main je Welt: jede Welt hat ihre Ebene
|
||||
if m == Mode.GEIST:
|
||||
body.scale = Vector3.ONE * 0.6
|
||||
_schleier_mat.albedo_color.a = 0.3
|
||||
@@ -177,7 +178,11 @@ func _physics_process(delta: float) -> void:
|
||||
## 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:
|
||||
# gemessen am Weg, nicht an der Geschwindigkeit: in einer Ecke schiebt man mit
|
||||
# voller Geschwindigkeit gegen die Wand und kommt doch nicht vom Fleck
|
||||
var weg := Vector2(global_position.x - _vorher.x, global_position.z - _vorher.z).length()
|
||||
_vorher = global_position
|
||||
if mode != Mode.GEIST or not will or weg > 0.3 * delta:
|
||||
_stockt = 0.0
|
||||
return
|
||||
_stockt += delta
|
||||
@@ -210,37 +215,39 @@ 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.
|
||||
## An einen Menschen hängen. Den Weg zu ihm findet das Navigationsnetz.
|
||||
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)
|
||||
_leine_pfad = []
|
||||
_leine_uhr = 0.0
|
||||
_verloren = 0.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.
|
||||
## Folgen heißt: ihm nachgehen, durch dieselben Türen und Treppen. Der Pfad
|
||||
## kommt vom Navigationsnetz und wird laufend neu geplant. 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
|
||||
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]
|
||||
_leine_uhr -= delta
|
||||
if _leine_uhr <= 0.0 or _leine_pfad.is_empty():
|
||||
_leine_uhr = 0.4
|
||||
_leine_pfad = []
|
||||
if mensch.nav_map.is_valid():
|
||||
var pts := NavigationServer3D.map_get_path(mensch.nav_map, global_position,
|
||||
mensch.global_position, true)
|
||||
for i in range(1, pts.size()):
|
||||
_leine_pfad.append(pts[i])
|
||||
if pts.size() < 2:
|
||||
print(" [kein Weg] Geist von %s zu %s bei %s" % [str(global_position),
|
||||
mensch.data["name"], str(mensch.global_position)])
|
||||
var abstand := global_position.distance_to(mensch.global_position)
|
||||
var ziel: Vector3 = _leine_pfad[0] if not _leine_pfad.is_empty() else mensch.global_position
|
||||
var d := ziel - global_position
|
||||
d.y = 0.0
|
||||
if not _leine_pfad.is_empty() and d.length() < 0.5 and absf(ziel.y - global_position.y) < 1.6:
|
||||
_leine_pfad.pop_front()
|
||||
var tempo := maxf(SPEED_GEIST, Bewohner.GEHTEMPO * Game.zeit_faktor() * 1.4)
|
||||
if d.length() > 0.3:
|
||||
if abstand > FOLGE_ABSTAND and d.length() > 0.05:
|
||||
var v := d.normalized() * minf(tempo, d.length() / delta)
|
||||
velocity.x = v.x
|
||||
velocity.z = v.z
|
||||
@@ -250,16 +257,13 @@ func _an_der_leine(delta: float) -> void:
|
||||
velocity.z = 0.0
|
||||
_fallen(delta)
|
||||
move_and_slide()
|
||||
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
|
||||
# Verloren? Nur wenn das Netz keinen Weg weiß und er zu weit weg ist,
|
||||
# wird er nachgezogen -- ein Notnagel, kein Spielzug.
|
||||
_verloren = _verloren + delta if abstand > LEINE and _leine_pfad.is_empty() else 0.0
|
||||
if _verloren > 3.0:
|
||||
global_position = mensch.global_position + Vector3(0, 0.3, 0)
|
||||
velocity = Vector3.ZERO
|
||||
_stockt = 0.0
|
||||
_verloren = 0.0
|
||||
spruenge += 1
|
||||
|
||||
|
||||
|
||||
@@ -139,14 +139,11 @@ static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color,
|
||||
|
||||
static func _meshes_von(b: Dictionary) -> Array:
|
||||
var out: Array = []
|
||||
for n in b["etagen"]:
|
||||
for n in b["teile"]:
|
||||
_sammle(n, out)
|
||||
for n in b["dach"]:
|
||||
_sammle(n, out)
|
||||
for liste in b["waende"]:
|
||||
for w in liste:
|
||||
if not out.has(w):
|
||||
out.append(w)
|
||||
for w in b["waende"]:
|
||||
if not out.has(w):
|
||||
out.append(w)
|
||||
return out
|
||||
|
||||
|
||||
@@ -162,12 +159,10 @@ static func _wand(mi: MeshInstance3D, normale: Vector3) -> MeshInstance3D:
|
||||
return mi
|
||||
|
||||
|
||||
## Etagenschnitt wie bei den Sims: steht der Spieler in einem Bau, verschwinden
|
||||
## das Dach, die Etagen über ihm und die beiden Wände, die zur Kamera zeigen.
|
||||
## Ein Bau: {c, size, hoch, etagen, dach, waende, voll}. "voll" heißt: ein
|
||||
## geschlossener Klotz, der ganz verschwindet.
|
||||
## Ein Bau zwischen Kamera und Spieler wird durchsichtig, sonst verschwände
|
||||
## der Spieler hinter jedem Nachbarhaus.
|
||||
## Schnitt wie bei den Sims: steht der Spieler in einem Bau, verschwinden Dach
|
||||
## (bei Klötzen der ganze Klotz) und die beiden Wände, die zur Kamera zeigen.
|
||||
## Ein Bau: {c, size, hoch, teile, waende}. Ein Bau zwischen Kamera und
|
||||
## Spieler wird durchsichtig, sonst verschwände der Spieler hinter jedem Nachbarhaus.
|
||||
static func schnitt(bauten: Array, spieler: Vector3, kamera: Vector3) -> void:
|
||||
for b in bauten:
|
||||
var c: Vector3 = b["c"]
|
||||
@@ -188,17 +183,11 @@ static func schnitt(bauten: Array, spieler: Vector3, kamera: Vector3) -> void:
|
||||
(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()):
|
||||
(etagen[f] as Node3D).visible = not drin or (f <= pf and not bool(b.get("voll", false)))
|
||||
for d in b["dach"]:
|
||||
(d as Node3D).visible = not drin
|
||||
var waende: Array = b["waende"]
|
||||
for f in range(waende.size()):
|
||||
for w in waende[f]:
|
||||
var n: Vector3 = w.get_meta("normal")
|
||||
(w as Node3D).visible = not (drin and f == pf and n.dot(kamera - c) > 0.0)
|
||||
for t in b["teile"]:
|
||||
(t as Node3D).visible = not drin
|
||||
for w in b["waende"]:
|
||||
var n: Vector3 = w.get_meta("normal")
|
||||
(w as Node3D).visible = not (drin and n.dot(kamera - c) > 0.0)
|
||||
|
||||
|
||||
## Ein Satteldach: ein geschlossenes Prisma über den Wänden. Der First läuft
|
||||
|
||||
185
scripts/ui.gd
185
scripts/ui.gd
@@ -6,6 +6,7 @@ class_name UI
|
||||
## der Mitte für Orte. Die Zeitleiste unten ist der Tag des Menschen -- alles
|
||||
## im Spiel hängt daran.
|
||||
|
||||
signal hinfahren(id: String)
|
||||
signal panel_opened
|
||||
signal panel_closed
|
||||
|
||||
@@ -25,8 +26,15 @@ const C_UNBEKANNT := Color(0.22, 0.23, 0.30)
|
||||
const C_GESEHEN := Color(0.46, 0.50, 0.62)
|
||||
const C_SCHLAF := Color(0.30, 0.33, 0.44)
|
||||
const C_WARN := Color(0.85, 0.66, 0.32)
|
||||
const C_VERDAECHTIG := Color(0.58, 0.46, 0.26) # Spur gelesen, Stunde noch nicht gesehen
|
||||
const C_BLOCKER := Color(0.84, 0.42, 0.35)
|
||||
const C_SYSTEM := Color(0.44, 0.78, 0.50)
|
||||
const C_PAPIER := Color(0.95, 0.92, 0.85) # die Akte: Papier, dunkle Tinte
|
||||
const C_PAPIER_ZEILE := Color(0.90, 0.86, 0.78)
|
||||
const TINTE := "#2b2620"
|
||||
const TINTE_HELL := "#6b6257"
|
||||
const TINTE_ROT := "#a83a2e"
|
||||
const TINTE_GRUEN := "#3d7a45"
|
||||
|
||||
var offen := false
|
||||
var _kind := ""
|
||||
@@ -299,6 +307,8 @@ func _ablauf_farbe(human_id: String, nr: int) -> Color:
|
||||
return C_BLOCKER
|
||||
Game.Stunde.SCHLAF:
|
||||
return C_SCHLAF
|
||||
Game.Stunde.VERDAECHTIG:
|
||||
return C_VERDAECHTIG
|
||||
return C_GESEHEN
|
||||
|
||||
|
||||
@@ -310,20 +320,24 @@ func _ablauf_aktualisieren() -> void:
|
||||
return
|
||||
var jetzt := int(Game.time_h)
|
||||
var offen_std := 0
|
||||
var verdaechtig := 0
|
||||
var puls := 0.5 + 0.5 * sin(Time.get_ticks_msec() * 0.008)
|
||||
for stunde in range(24):
|
||||
var nr := Game.slot_bei_stunde(id, stunde)
|
||||
var f := _ablauf_farbe(id, nr)
|
||||
if not Game.gesehen_slot(id, nr):
|
||||
offen_std += 1
|
||||
if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG:
|
||||
verdaechtig += 1
|
||||
var zelle: ColorRect = _zellen[stunde]
|
||||
if stunde == jetzt:
|
||||
f = f.lightened(0.55 if not Game.situation_bei(id) else 0.3 + 0.4 * puls)
|
||||
zelle.color = f
|
||||
zelle.custom_minimum_size.y = 24 if stunde == jetzt else 16
|
||||
zelle.tooltip_text = "%02d Uhr — %s" % [stunde, _stunden_text(id, nr)]
|
||||
_ablauf_titel.text = "Ihr Tag 0 – 23 Uhr%s" % \
|
||||
("" if offen_std == 0 else " · %d Stunden unbekannt" % offen_std)
|
||||
_ablauf_titel.text = "Ihr Tag 0 – 23 Uhr%s%s" % [
|
||||
"" if offen_std == 0 else " · %d Stunden unbekannt" % offen_std,
|
||||
"" if verdaechtig == 0 else " · %d verdächtig" % verdaechtig]
|
||||
|
||||
var urs: Array = []
|
||||
for u in Game.ursachen_von(id):
|
||||
@@ -414,7 +428,7 @@ func _baue_wirte() -> void:
|
||||
|
||||
func _wirt_fuer(kind: String) -> PanelContainer:
|
||||
match kind:
|
||||
"journal", "technik", "systeme":
|
||||
"journal", "technik", "akte":
|
||||
return _drawer
|
||||
"slot", "situation", "anwenden":
|
||||
return _karte_host
|
||||
@@ -609,7 +623,7 @@ func _reiter() -> void:
|
||||
_content.add_child(hb)
|
||||
var seiten := [
|
||||
["techniken", "Techniken (%d)" % Game.known.size()],
|
||||
["missionen", "Missionen (%d)" % Game.progress.size()],
|
||||
["missionen", "Akten (%d)" % Game.progress.size()],
|
||||
]
|
||||
for eintrag in seiten:
|
||||
var b := Button.new()
|
||||
@@ -675,44 +689,145 @@ func _journal_missionen() -> void:
|
||||
stand = "aktiv · %d %%" % int(Game.mission_progress(id))
|
||||
farbe = C_AKZENT
|
||||
_karte("%s · %s" % [h["name"], stand], farbe,
|
||||
func() -> void: systeme_panel(id))
|
||||
func() -> void: akte_panel(id))
|
||||
|
||||
|
||||
# ---------------------------------------------- Diagnose und Systeme
|
||||
# ---------------------------------------------- Die Akte
|
||||
|
||||
## Alles zu einem Menschen auf einem Blatt: Person und Traum, was im Weg steht,
|
||||
## der Befund aus Spuren und Beobachtungen, der Tag, die Routinen, die Notizen.
|
||||
## Keine Patientenakte -- die Leute sind nicht krank, sie haben Gewohnheiten.
|
||||
var _blatt: VBoxContainer = null
|
||||
|
||||
|
||||
## Der Traum scheitert an Ursachen. Die stehen nicht im Panel -- man findet sie,
|
||||
## indem man dabeisteht. Gegen jede gefundene Ursache baut man ein System in
|
||||
## den Tag. Je mehr abgedeckt ist, desto schneller kommt der Traum voran.
|
||||
func systeme_panel(human_id: String) -> void:
|
||||
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "systeme" else 0
|
||||
var h := HumanDB.by_id(human_id)
|
||||
_leeren("%s — Tagesablauf" % h["name"])
|
||||
_text("%d %% · +%d %% je Tag · [color=#aaaaaa]%s[/color]" %
|
||||
[int(Game.mission_progress(human_id)), int(round(Game.traum_tempo(human_id))),
|
||||
h["traum"]], 16)
|
||||
_trenner()
|
||||
akte_panel(human_id)
|
||||
|
||||
_text("[b]Woran es scheitert[/b]", 17)
|
||||
var offene := 0
|
||||
|
||||
func akte_panel(human_id: String) -> void:
|
||||
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "akte" else 0
|
||||
var h := HumanDB.by_id(human_id)
|
||||
_leeren("Akte")
|
||||
_papier()
|
||||
var erfuellt := Game.completed.has(human_id)
|
||||
_akte_text("[b]%s[/b] [color=%s]%d · %s[/color]" % [h["name"], TINTE_HELL,
|
||||
int(h.get("alter", 0)), h.get("beruf", "")], 20)
|
||||
_akte_text("[color=%s]Traum[/color] „%s“" % [TINTE_HELL, h["traum"]], 16)
|
||||
if erfuellt:
|
||||
_stempel("TRAUM ERFÜLLT")
|
||||
else:
|
||||
_akte_text("[color=%s]Stand[/color] %d %% · +%d %% je Tag" % [TINTE_HELL,
|
||||
int(Game.mission_progress(human_id)), int(round(Game.traum_tempo(human_id)))], 16)
|
||||
_knopf("Hinfahren — vor die Tür", func() -> void:
|
||||
schliessen()
|
||||
hinfahren.emit(human_id), true, _blatt)
|
||||
|
||||
_akte_abschnitt("Was im Weg steht")
|
||||
_akte_text(str(h["problem"]), 15)
|
||||
|
||||
_akte_abschnitt("Befund")
|
||||
for u in Game.ursachen_von(human_id):
|
||||
var name: String = str(u)
|
||||
if not Game.ursache_bekannt(human_id, name):
|
||||
_text("[color=#8e93a8]? — noch nicht erkannt[/color]", 15)
|
||||
offene += 1
|
||||
var stand: String
|
||||
if Game.ursache_geloest(human_id, name) > 0.0:
|
||||
stand = "[color=%s]Routine steht[/color]" % TINTE_GRUEN
|
||||
elif Game.ursache_bekannt(human_id, name):
|
||||
var woher := "Spur gelesen" if Game.spur_bekannt(human_id, name) else "beobachtet"
|
||||
stand = "[color=%s]%s · bremst auf %d %%[/color]" % [TINTE_ROT, woher,
|
||||
int(float(Ursachen.LISTE[name]["faktor"]) * 100.0)]
|
||||
else:
|
||||
_akte_text("[color=%s]? noch nichts gefunden — Spuren in der Wohnung, Momente im Tag[/color]" %
|
||||
TINTE_HELL, 15)
|
||||
continue
|
||||
var g := Game.ursache_geloest(human_id, name)
|
||||
var stand := "[color=#88ff99]gelöst[/color]" if g > 0.0 else \
|
||||
"[color=#ff9977]offen · bremst auf %d %%[/color]" % \
|
||||
int(float(Ursachen.LISTE[name]["faktor"]) * 100.0)
|
||||
_text("[b]%s[/b] %s" % [Ursachen.LISTE[name]["name"], stand], 15)
|
||||
if offene > 0: _trenner()
|
||||
_akte_text("[b]%s[/b] %s" % [Ursachen.LISTE[name]["name"], stand], 15)
|
||||
var probleme: Array = []
|
||||
for pr in Game.probleme_von(human_id):
|
||||
var key := str(pr)
|
||||
if not Game.problem_bekannt(human_id, key):
|
||||
probleme.append("[color=%s]?[/color]" % TINTE_HELL)
|
||||
elif Game.problem_ueberwunden(human_id, key):
|
||||
probleme.append("[color=%s]%s ✓[/color]" % [TINTE_GRUEN, Probleme.name_von(key)])
|
||||
else:
|
||||
probleme.append("%s %d/%d" % [Probleme.name_von(key),
|
||||
Game.problem_fortschritt(human_id, key), Game.PROBLEM_UEBERWINDEN])
|
||||
if not probleme.is_empty():
|
||||
_akte_text("[color=%s]In Momenten[/color] %s" % [TINTE_HELL, " · ".join(probleme)], 15)
|
||||
|
||||
_akte_abschnitt("Tag")
|
||||
_tagesablauf(human_id)
|
||||
_zeigen("systeme", journal_panel.bind("missionen"))
|
||||
|
||||
_akte_abschnitt("Notizen")
|
||||
var zeilen: Array = Game.notizen.get(human_id, [])
|
||||
if zeilen.is_empty():
|
||||
_akte_text("[color=%s]noch keine[/color]" % TINTE_HELL, 14)
|
||||
for i in range(maxi(0, zeilen.size() - 8), zeilen.size()):
|
||||
_akte_text("[color=%s]%s[/color]" % [TINTE, zeilen[i]], 14)
|
||||
|
||||
_zeigen("akte", journal_panel.bind("missionen"))
|
||||
_scroll_wunsch = scroll_alt
|
||||
_scroll_frames = 4
|
||||
|
||||
|
||||
## Ein Blatt Papier im Drawer; alles Weitere der Akte landet darauf.
|
||||
func _papier() -> void:
|
||||
var p := PanelContainer.new()
|
||||
var s := _sb(C_PAPIER, 3, Color(0.78, 0.72, 0.6), 1)
|
||||
s.content_margin_left = 16
|
||||
s.content_margin_right = 16
|
||||
s.content_margin_top = 12
|
||||
s.content_margin_bottom = 14
|
||||
p.add_theme_stylebox_override("panel", s)
|
||||
_content.add_child(p)
|
||||
_blatt = VBoxContainer.new()
|
||||
_blatt.add_theme_constant_override("separation", 6)
|
||||
p.add_child(_blatt)
|
||||
|
||||
|
||||
func _akte_text(bb: String, groesse: int = 15) -> RichTextLabel:
|
||||
var r := _text("[color=%s]%s[/color]" % [TINTE, bb], groesse, _blatt)
|
||||
return r
|
||||
|
||||
|
||||
func _akte_abschnitt(titel: String) -> void:
|
||||
var r := _text("[color=%s]%s[/color]" % [TINTE_HELL, titel.to_upper()], 12, _blatt)
|
||||
r.add_theme_constant_override("line_separation", 2)
|
||||
var linie := HSeparator.new()
|
||||
linie.add_theme_stylebox_override("separator", _sb(Color(0.78, 0.72, 0.6, 0.6), 0))
|
||||
linie.custom_minimum_size.y = 2
|
||||
_blatt.add_child(linie)
|
||||
|
||||
|
||||
func _stempel(text: String) -> void:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.add_theme_font_size_override("font_size", 18)
|
||||
l.add_theme_color_override("font_color", Color(0.66, 0.2, 0.15))
|
||||
l.add_theme_stylebox_override("normal", _sb(Color(0, 0, 0, 0), 2, Color(0.66, 0.2, 0.15), 2))
|
||||
l.rotation_degrees = -4.0
|
||||
_blatt.add_child(l)
|
||||
|
||||
|
||||
## Eine Zeile der Akte, die sich aufschlagen lässt (Stunden des Tages).
|
||||
func _akte_karte(beschriftung: String, farbe: Color, cb: Callable, aktiv: bool = true) -> Button:
|
||||
var b := Button.new()
|
||||
b.text = beschriftung
|
||||
b.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
b.disabled = not aktiv
|
||||
b.custom_minimum_size = Vector2(0, 30)
|
||||
b.add_theme_font_size_override("font_size", 14)
|
||||
b.add_theme_color_override("font_color", Color(0.17, 0.15, 0.13))
|
||||
b.add_theme_color_override("font_hover_color", Color(0.1, 0.08, 0.06))
|
||||
b.add_theme_color_override("font_disabled_color", Color(0.3, 0.27, 0.24))
|
||||
b.add_theme_stylebox_override("normal", _sb(C_PAPIER_ZEILE, 3, farbe, 0, 4))
|
||||
b.add_theme_stylebox_override("hover", _sb(C_PAPIER_ZEILE.darkened(0.06), 3, farbe, 0, 4))
|
||||
b.add_theme_stylebox_override("pressed", _sb(C_PAPIER_ZEILE.darkened(0.1), 3, farbe, 0, 4))
|
||||
b.add_theme_stylebox_override("disabled", _sb(C_PAPIER_ZEILE, 3, farbe.darkened(0.2), 0, 4))
|
||||
_blatt.add_child(b)
|
||||
if aktiv:
|
||||
b.pressed.connect(cb)
|
||||
return b
|
||||
|
||||
|
||||
## Der Ablauf von 0 bis 23 Uhr. Jeder Tag ist gleich -- es gibt keine
|
||||
## Wochentage. Zu Beginn ist jede Stunde leer; wer dabeisteht, füllt sie.
|
||||
func _tagesablauf(human_id: String) -> void:
|
||||
@@ -720,8 +835,8 @@ func _tagesablauf(human_id: String) -> void:
|
||||
for stunde in range(24):
|
||||
if not Game.gesehen_slot(human_id, Game.slot_bei_stunde(human_id, stunde)):
|
||||
offen += 1
|
||||
_text("[b]Ihr Tag[/b]%s" %
|
||||
("" if offen == 0 else " [color=#8e93a8]%d Stunden leer[/color]" % offen), 17)
|
||||
if offen > 0:
|
||||
_akte_text("[color=%s]%d Stunden leer[/color]" % [TINTE_HELL, offen], 14)
|
||||
|
||||
# eine Zeile je Abschnitt des Tages -- auch wenn noch keiner bekannt ist,
|
||||
# steht der Tag vollständig da
|
||||
@@ -756,8 +871,8 @@ func _stunden_zeile(human_id: String, nr: int, von: int, bis: int, text: String)
|
||||
var spanne := "%02d – %02d" % [von, bis + 1 if bis < 23 else 24]
|
||||
var offen: bool = Game.gesehen_slot(human_id, nr) and Game.erkannt(human_id, nr) \
|
||||
and Game.slot_grund(human_id, nr) != "schlaeft"
|
||||
_karte("%s %s" % [spanne, text], _ablauf_farbe(human_id, nr),
|
||||
func() -> void: slot_panel(human_id, nr, systeme_panel.bind(human_id)), offen)
|
||||
_akte_karte("%s %s" % [spanne, text], _ablauf_farbe(human_id, nr),
|
||||
func() -> void: slot_panel(human_id, nr, akte_panel.bind(human_id)), offen)
|
||||
|
||||
|
||||
## Der offene Moment: hier zählt, welche Technik zu diesem Problem passt.
|
||||
@@ -808,13 +923,13 @@ func slot_panel(human_id: String, nr: int, zurueck: Callable = Callable()) -> vo
|
||||
_text("Sie %s.\n[b]Auslöser:[/b] %s\n[b]Belohnung:[/b] %s" %
|
||||
[b["name"], b["ausloeser"], Ursachen.belohnung_text(str(b["belohnung"]))], 16)
|
||||
else:
|
||||
_text("Hier ist Platz. Ein neues System passt in diesen Slot.", 16)
|
||||
_text("Hier ist Platz für eine neue Routine.", 16)
|
||||
_trenner()
|
||||
var sys := Game.system_in(human_id, nr)
|
||||
if not sys.is_empty():
|
||||
_text("[color=#88ff99]Hier steht: %s[/color]" %
|
||||
TechDB.by_id(str(sys["tech"]))["name"], 16)
|
||||
_knopf("System wieder herausnehmen", func() -> void:
|
||||
_knopf("Routine wieder herausnehmen", func() -> void:
|
||||
Game.system_entfernen(human_id, nr)
|
||||
systeme_panel(human_id))
|
||||
_trenner()
|
||||
@@ -1043,7 +1158,7 @@ func _vorschau_aktualisieren() -> void:
|
||||
var wert := int(bild["verhalten"][v])
|
||||
if wert != 0:
|
||||
zuege.append("%s %s" % [namen[v], "+" if wert > 0 else "−"])
|
||||
_vorschau.text = "[color=#ff9a8a]%s[/color]\n[color=#ffcc66]%s[/color] · %d Blocker im Tag\n[color=#8e93a8]%s[/color]" % \
|
||||
_vorschau.text = "[color=#ff9a8a]%s[/color]\n[color=#ffcc66]%s[/color] · %d Gewohnheiten im Tag\n[color=#8e93a8]%s[/color]" % \
|
||||
[" · ".join(urs), " · ".join(prob), (bild["blocker"] as Array).size(),
|
||||
" ".join(zuege) if not zuege.is_empty() else "unauffällig"]
|
||||
|
||||
|
||||
@@ -10,26 +10,38 @@ class_name Ursachen
|
||||
## bemerken, dann ersetzen. Und Duhiggs goldene Regel: Auslöser und Belohnung
|
||||
## bleiben, nur die Handlung dazwischen wird getauscht.
|
||||
|
||||
## spur: was davon in der Wohnung herumliegt. Wer hineingeht und hinsieht,
|
||||
## weiß, wonach er im Tag suchen muss.
|
||||
static var LISTE: Dictionary = {
|
||||
"anfang": {
|
||||
"name": "Kein Anfang", "kurz": "kommt nie in Gang",
|
||||
"faktor": 0.30,
|
||||
"spur": "Handy auf dem Boden",
|
||||
"spur_text": "Das Handy liegt neben dem Tisch, der Bildschirm noch warm. Angefangen wird hier nicht.",
|
||||
},
|
||||
"zeit": {
|
||||
"name": "Keine Zeit", "kurz": "der Tag ist voll",
|
||||
"faktor": 0.30,
|
||||
"spur": "Vollgeschriebener Kalender",
|
||||
"spur_text": "Jede Stunde im Kalender ist belegt. Für den Traum steht keine drin.",
|
||||
},
|
||||
"kraft": {
|
||||
"name": "Keine Kraft", "kurz": "abends ist nichts mehr übrig",
|
||||
"faktor": 0.40,
|
||||
"spur": "Leere Dosen am Bett",
|
||||
"spur_text": "Drei leere Dosen neben dem Bett. Abends ist nichts mehr übrig.",
|
||||
},
|
||||
"halt": {
|
||||
"name": "Kein Halt", "kurz": "fängt an und hört wieder auf",
|
||||
"faktor": 0.50,
|
||||
"spur": "Stapel ungeöffneter Briefe",
|
||||
"spur_text": "Ein Stapel ungeöffneter Briefe. Angefangen und liegen gelassen.",
|
||||
},
|
||||
"grund": {
|
||||
"name": "Kein Grund", "kurz": "glaubt nicht mehr daran",
|
||||
"faktor": 0.40,
|
||||
"spur": "Verstaubter Koffer",
|
||||
"spur_text": "Ein Koffer voller alter Pläne, längst verstaubt. Daran glaubt hier niemand mehr.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
64
tests/flimmer.gd
Normal file
64
tests/flimmer.gd
Normal 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
6
tests/flimmer.tscn
Normal 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
37
tests/rauch.gd
Normal 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
6
tests/rauch.tscn
Normal 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")
|
||||
16
tests/run.sh
16
tests/run.sh
@@ -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)
|
||||
|
||||
524
tests/verify.gd
524
tests/verify.gd
@@ -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" %
|
||||
|
||||
Reference in New Issue
Block a user