class_name HumanDB ## Die Erde: ein Stadtbezirk nach japanischem Vorbild. Weite Arterien in einem ## groben Raster spannen Blöcke auf. Am Blockrand stehen die Häuser direkt an ## der Straße, ohne Vorgarten. Ins Blockinnere führen schmale Sackgassen -- ## roji --, an denen die hinteren Parzellen hängen. So entsteht die typische ## Mischung aus klarer Ordnung außen und Gewinkel innen. ## ## Anders als das Traumdorf wächst hier nichts: alle Menschen sind von Anfang ## an da und leben ihren Tag. const EINWOHNER := 16 const ACHSEN := [-72.0, -24.0, 24.0, 72.0] # Arterien in x wie in z const ARTERIE := 7.0 # Breite einer Arterie const ROJI := 3.2 # Breite einer Gasse const RAND_TIEFE := 11.0 # Tiefe der Parzellen an der Straße const PARZELLE_MIN := 8.0 # schmalste Parzelle const PARZELLE_MAX := 12.0 ## Alle Parzellen des Bezirks: {pos, size, dir, strasse} static var PARZELLEN: Array = [] ## Alle Straßenbänder: {a, b, breite} static var STRASSEN: Array = [] static var NETZ: Netz = Netz.new() static var ANKER: Dictionary = {} ## Die fünf Sonderbauten. Sie bekommen eine Parzelle an einer Arterie. static var SONDER: Array = [ ["uni", "Hochschule", Color(0.72, 0.70, 0.66)], ["cafe", "Café Nadel", Color(0.82, 0.62, 0.45)], ["buero", "Büro Ostwind", Color(0.62, 0.66, 0.72)], ["baeckerei", "Bäckerei Keller", Color(0.85, 0.72, 0.50)], ["werkstatt", "Werkstatt Loos", Color(0.70, 0.58, 0.55)], ] static var PLACES: Dictionary = {} static var LIST: Array = [] ## Arbeitsplätze, die ein erzeugter Mensch bekommen kann. static var ARBEIT: Array = ["uni", "cafe", "buero", "baeckerei", "werkstatt"] static var VORNAMEN: Array = ["Lena", "Bruno", "Mira", "Tobias", "Halim", "Jorunn", "Pia", "Anselm", "Ceyda", "Rune", "Malte", "Ida", "Vito", "Nour", "Greta", "Kasper"] static var NACHNAMEN: Array = ["Vogt", "Keller", "Sahin", "Frank", "Berg", "Ostermann", "Ruiz", "Lind", "Aslan", "Weiss", "Petit", "Novak"] static var BERUFE: Array = ["Illustratorin", "Bäcker", "Studentin", "Sachbearbeiter", "Pfleger", "Tischlerin", "Lehrer", "Programmiererin", "Fahrer", "Archivarin", "Köchin", "Musiker"] ## Anliegen sind echte Klemmen, keine erzeugten Sätze — sie tragen den Sinn ## des Spiels. Erzeugt werden Person, Wohnort und Tagesablauf drumherum. static var ANLIEGEN: Array = [ { "traum": "Ihr Bilderbuch endlich zu Ende zeichnen.", "problem": "Sie schiebt die letzten zwölf Seiten seit acht Monaten vor sich her. Jeder Entwurf kommt ihr zu schlecht vor, also fängt sie lieber gar nicht erst an.", "tags": ["prokrastination", "selbstzweifel", "ablenkung"], }, { "traum": "Wieder Klavier spielen wie mit zwanzig.", "problem": "Das Klavier steht zugestellt im Flur. Nach der Frühschicht ist er zu müde, um auch nur den Deckel zu öffnen.", "tags": ["gewohnheit", "erschoepfung", "ablenkung"], }, { "traum": "Die Abschlussarbeit vortragen, ohne zu erstarren.", "problem": "Die Arbeit ist längst fertig. Aber vor jedem Vortrag wird ihr schwarz vor Augen, und sie meldet sich wieder ab.", "tags": ["angst", "ueberforderung", "selbstzweifel"], }, { "traum": "Abends wieder Zeit für die Tochter haben.", "problem": "Er sagt zu jeder Bitte ja und sitzt dafür jeden Abend bis acht im Büro. Zu Hause ist er nur noch müde.", "tags": ["stress", "grenzen", "erschoepfung"], }, { "traum": "Endlich wieder durchschlafen.", "problem": "Der Feierabend franst aus, das Handy leuchtet bis zwei. Morgens steht er zerschlagen auf und trinkt sich wach.", "tags": ["erschoepfung", "gesundheit", "gewohnheit"], }, { "traum": "Den alten Freundeskreis zurückholen.", "problem": "Sie wartet seit Monaten, dass sich jemand meldet, und meldet sich selbst nie. Jede Einladung erscheint ihr aufdringlich.", "tags": ["einsamkeit", "selbstzweifel"], }, { "traum": "Die Werkstatt in der Garage wieder aufmachen.", "problem": "Alles liegt bereit, aber der erste Handgriff wirkt jedes Mal wie ein Berg. Am Ende räumt er lieber auf.", "tags": ["prokrastination", "ueberforderung"], }, { "traum": "Vor Publikum sprechen, ohne dass die Hände zittern.", "problem": "Schon Tage vorher liegt ihr der Termin im Magen. Kurz davor wird die Luft knapp und der Kopf leer.", "tags": ["angst", "stress"], }, { "traum": "Nach dem Rückschlag noch einmal anfangen.", "problem": "Seit die Prüfung schiefging, redet sie mit sich, wie sie mit niemandem reden würde. Jeder neue Versuch fühlt sich sinnlos an.", "tags": ["rueckschlag", "selbstzweifel"], }, { "traum": "Drei Abende in der Woche für sich behalten.", "problem": "Der Kalender füllt sich mit fremden Anliegen, weil ein Nein unhöflich wirkt. Am Ende bleibt nichts übrig.", "tags": ["grenzen", "stress", "ueberforderung"], }, ] ## Der Grundtag. Blocker werden später in passende Fenster gesetzt. ## [Stunde, Ort, Tätigkeit, Fenster] static var GRUNDTAG: Array = [ [0, "heim", "schlaeft", "nacht"], [7, "heim", "morgenroutine", "morgen"], [9, "arbeit", "arbeitet", "tag"], [12, "cafe", "essen", "tag"], [14, "arbeit", "arbeitet", "tag"], [17, "park", "unterwegs", "abend"], [19, "heim", "ruht", "abend"], [21, "heim", "ruht", "abend"], [23, "heim", "abendroutine", "nacht"], ] static var PASSEND: Array = [ [0, 0], # Pomodoro -> aufschieben, zögert [6, 4], # Zwei-Minuten -> Werkstatt, räumt auf [4, 3], # Wenn-Dann -> Gewohnheit, Morgenroutine [1, 1], # Gewohnheiten -> Klavier, isst / Abendroutine [9, 0], # Zerlegen -> Überforderung, grübelt [0, 4], # Umgebung -> Ablenkung, räumt auf [8, 0], # Umdeuten -> Selbstzweifel, grübelt [7, 2], # Box-Atmung -> Angst, angespannt [8, 2], # Selbstmitgefühl -> Rückschlag, grübelt [4, 1], # Schlaffenster -> Erschöpfung, ruht [5, 2], # Hilfe bitten -> Einsamkeit, wartet [3, 3], # Nein sagen -> Grenzen, arbeitet ] static func heim_key(i: int) -> String: return "heim_%d" % i ## Baut den ganzen Bezirk neu auf: Straßen, Parzellen, Sonderbauten, Wohnhäuser. ## Die Zahl der Menschen liegt fest -- sie sind alle von Anfang an da. static func erzeuge(_ungenutzt: int = 0) -> void: _bezirk() PLACES = {} LIST = [] _park_eintragen() _sonderbauten_verteilen() _wohnhaeuser_verteilen() _anker_setzen() for i in range(EINWOHNER): LIST.append(_mensch(i)) # ------------------------------------------------------------ Bezirk ## Arterien im Raster, Blöcke dazwischen, roji ins Blockinnere, Parzellen ## an jeder Straße. Danach steht das Straßennetz, auf dem alle laufen. static func _bezirk() -> void: PARZELLEN = [] STRASSEN = [] NETZ = Netz.new() ANKER = {} var rng := RandomNumberGenerator.new() rng.seed = 31337 # Arterien: die Achsen leicht versetzt, damit kein reines Schachbrett steht var xs: Array = [] var zs: Array = [] for i in range(ACHSEN.size()): xs.append(float(ACHSEN[i]) + rng.randf_range(-3.0, 3.0)) zs.append(float(ACHSEN[i]) + rng.randf_range(-3.0, 3.0)) var von: float = float(ACHSEN[0]) - 6.0 var bis: float = float(ACHSEN[ACHSEN.size() - 1]) + 6.0 for x in xs: _strasse(Vector3(x, 0, von), Vector3(x, 0, bis), ARTERIE) for z in zs: _strasse(Vector3(von, 0, z), Vector3(bis, 0, z), ARTERIE) # Blöcke zwischen den Arterien for ix in range(xs.size() - 1): for iz in range(zs.size() - 1): var x0: float = float(xs[ix]) + ARTERIE * 0.5 var x1: float = float(xs[ix + 1]) - ARTERIE * 0.5 var z0: float = float(zs[iz]) + ARTERIE * 0.5 var z1: float = float(zs[iz + 1]) - ARTERIE * 0.5 if ix == 1 and iz == 1: continue # die Mitte bleibt Park _block(Vector3((x0 + x1) * 0.5, 0, (z0 + z1) * 0.5), Vector2(x1 - x0, z1 - z0), rng) _netz_bauen() static func _strasse(a: Vector3, b: Vector3, breite: float) -> void: STRASSEN.append({"a": a, "b": b, "breite": breite}) ## Aus den Straßenbändern ein zusammenhängendes Netz machen: an jeder Kreuzung ## sitzt ein Knoten, dazwischen alle 16 Meter einer. static func _netz_bauen() -> void: for i in range(STRASSEN.size()): var a: Vector3 = STRASSEN[i]["a"] var b: Vector3 = STRASSEN[i]["b"] var laenge := a.distance_to(b) if laenge < 0.5: continue var stellen: Array = [0.0, 1.0] for j in range(STRASSEN.size()): if j == i: continue var t := _kreuzung(a, b, STRASSEN[j]["a"], STRASSEN[j]["b"]) if t >= 0.0: stellen.append(t) stellen.sort() var fein: Array = [float(stellen[0])] for k in range(1, stellen.size()): var von: float = float(fein[fein.size() - 1]) var bis: float = float(stellen[k]) if bis - von < 0.001: continue var n := maxi(1, int((bis - von) * laenge / 16.0)) for m in range(1, n + 1): fein.append(von + (bis - von) * float(m) / float(n)) var vorher := -1 for t2 in fein: var idx := NETZ.fuege_knoten(a.lerp(b, float(t2))) if vorher >= 0: NETZ.verbinde(vorher, idx) vorher = idx ## Parameter t auf der Strecke a-b, an dem sie c-d schneidet. -1 wenn nicht. static func _kreuzung(a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> float: var r := b - a var s2 := d - c var nenner := r.x * s2.z - r.z * s2.x if absf(nenner) < 0.0001: return -1.0 var t := ((c.x - a.x) * s2.z - (c.z - a.z) * s2.x) / nenner var u := ((c.x - a.x) * r.z - (c.z - a.z) * r.x) / nenner if t < 0.0 or t > 1.0 or u < 0.0 or u > 1.0: return -1.0 return t ## Ein Block: außen ein Kranz von Parzellen an den Arterien, innen eine Gasse ## mit den hinteren Parzellen daran. Die Nord- und Südreihe laufen über die ## volle Breite, die Ost- und Westreihe nur dazwischen -- sonst überlappen die ## Ecken. static func _block(mitte: Vector3, groesse: Vector2, rng: RandomNumberGenerator) -> void: var tiefe := minf(RAND_TIEFE, minf(groesse.x, groesse.y) * 0.34) var seiten := [ ["back", Vector3(0, 0, 1), groesse.x, groesse.y], ["forward", Vector3(0, 0, -1), groesse.x, groesse.y], ["right", Vector3(1, 0, 0), groesse.y - tiefe * 2.0, groesse.x], ["left", Vector3(-1, 0, 0), groesse.y - tiefe * 2.0, groesse.x], ] for eintrag in seiten: var dir: String = eintrag[0] var n: Vector3 = eintrag[1] var laenge: float = eintrag[2] var quer_mass: float = eintrag[3] if laenge < PARZELLE_MIN: continue var quer := Vector3(absf(n.z), 0, absf(n.x)) var anzahl := maxi(1, int(round(laenge / rng.randf_range(PARZELLE_MIN + 1.0, PARZELLE_MAX)))) var breite := laenge / float(anzahl) var rand := mitte + n * (quer_mass * 0.5 - tiefe * 0.5) for k in range(anzahl): var versatz := (float(k) + 0.5 - float(anzahl) * 0.5) * breite var quer_ist := absf(n.z) > 0.0 PARZELLEN.append({ "pos": rand + quer * versatz, "size": Vector2(breite - 0.8, tiefe - 0.8) if quer_ist else Vector2(tiefe - 0.8, breite - 0.8), "dir": dir, }) # Blockinneres: eine Gasse von der längeren Seite hinein var innen := groesse - Vector2(tiefe, tiefe) * 2.0 if innen.x < 12.0 or innen.y < 12.0: return var laengs := innen.y >= innen.x var richtung := Vector3(0, 0, -1) if laengs else Vector3(-1, 0, 0) # Die Gasse setzt an der Arterie an, sonst hinge sie in der Luft var aussen: float = (groesse.y if laengs else groesse.x) * 0.5 + ARTERIE * 0.5 var start := mitte - richtung * aussen var ende := mitte + richtung * ((innen.y if laengs else innen.x) * 0.5 - 2.0) _strasse(start, ende, ROJI) var flanke := Vector3(1, 0, 0) if laengs else Vector3(0, 0, 1) var p_breite := (innen.x if laengs else innen.y) * 0.5 - ROJI * 0.5 - 0.8 if p_breite < PARZELLE_MIN * 0.8: return # Nur der Teil im Blockinneren trägt Parzellen var innen_start := mitte - richtung * ((innen.y if laengs else innen.x) * 0.5) var laenge_g := innen_start.distance_to(ende) var anzahl_g := maxi(1, int(round(laenge_g / 13.0))) var tief_g := laenge_g / float(anzahl_g) - 0.8 for seite: float in [1.0, -1.0]: for k in range(anzahl_g): var t := (float(k) + 0.5) / float(anzahl_g) var pos := innen_start.lerp(ende, t) + flanke * (seite * (ROJI * 0.5 + p_breite * 0.5)) PARZELLEN.append({ "pos": pos, "size": Vector2(p_breite, tief_g) if laengs else Vector2(tief_g, p_breite), "dir": ("left" if seite > 0.0 else "right") if laengs else ("forward" if seite > 0.0 else "back"), }) # ------------------------------------------------------------ Belegung static func _park_eintragen() -> void: PLACES["park"] = { "name": "Stadtpark", "pos": [0.0, 0.0], "size": [0.0, 0.0], "dir": "none", "color": Color(0.45, 0.65, 0.38), } ## Die Sonderbauten bekommen Parzellen an einer Arterie, gut verteilt. static func _sonderbauten_verteilen() -> void: var kandidaten: Array = [] for i in range(PARZELLEN.size()): var p: Dictionary = PARZELLEN[i] if minf(p["size"].x, p["size"].y) >= 10.0: kandidaten.append(i) for s in range(SONDER.size()): if kandidaten.is_empty(): break # Das Café gehört in die Mitte -- jeder Tagesablauf führt dorthin. # Die übrigen Sonderbauten verteilen sich rundum. var wunsch := Vector3.ZERO if str(SONDER[s][0]) != "cafe": var w := TAU * float(s) / float(SONDER.size() - 1) wunsch = Vector3(cos(w), 0, sin(w)) * 42.0 var idx: int = -1 var beste := INF for k in kandidaten: var q: Dictionary = PARZELLEN[k] if q.has("belegt"): continue var d: float = (q["pos"] as Vector3).distance_to(wunsch) if d < beste: beste = d idx = int(k) if idx < 0: continue var p: Dictionary = PARZELLEN[idx] p["belegt"] = true PLACES[str(SONDER[s][0])] = { "name": str(SONDER[s][1]), "pos": [p["pos"].x, p["pos"].z], "size": [p["size"].x, p["size"].y], "dir": p["dir"], "color": SONDER[s][2], } ## Die Wohnhäuser: gleichmäßig über den Bezirk verteilt, damit die Wege ## nicht alle in derselben Ecke enden. static func _wohnhaeuser_verteilen() -> void: var frei: Array = [] for i in range(PARZELLEN.size()): if not PARZELLEN[i].has("belegt"): frei.append(i) if frei.is_empty(): return for i in range(EINWOHNER): var idx: int = int(frei[(i * 11 + 5) % frei.size()]) var p: Dictionary = PARZELLEN[idx] if p.has("belegt"): for versuch in range(frei.size()): idx = int(frei[(i * 11 + 5 + versuch) % frei.size()]) p = PARZELLEN[idx] if not p.has("belegt"): break p["belegt"] = true PLACES[heim_key(i)] = { "name": "%s %d" % [_nachname(i), i + 1], "pos": [p["pos"].x, p["pos"].z], "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), } static func _anker_setzen() -> void: for key in PLACES: var s: Array = PLACES[key]["size"] var tuer := Orte.door(PLACES, key) if float(s[0]) <= 0.0: tuer = Orte.center(PLACES, key) ANKER[key] = int(NETZ.andocken(tuer)["knoten"]) ## Was diesen Menschen nicht im Tagesablauf aufhält, sondern in Momenten. ## Ein bis zwei Probleme -- die zweite Ebene neben den Ursachen. static func probleme_von(i: int) -> Array: var alle: Array = Probleme.LISTE.keys() var out: Array = [str(alle[i % alle.size()])] if i % 2 == 1: var zweites: String = str(alle[(i + 2) % alle.size()]) if zweites != out[0]: out.append(zweites) return out ## Woran der Traum dieses Menschen scheitert. Zwei bis drei Ursachen, nie alle. ## Die erste hängt am Weg gleicher Nummer -- so ist der neueste Mensch immer ## mit dem neuesten Weg anzugehen. static func ursachen_von(i: int) -> Array: var alle: Array = Ursachen.LISTE.keys() var erste: String = str(alle[i % alle.size()]) if i < TechDB.LIST.size(): erste = str(TechDB.LIST[i]["ursache"]) var out: Array = [erste] var zweite: String = str(alle[(alle.find(erste) + 1 + i % 3) % alle.size()]) if zweite != erste: out.append(zweite) if i % 3 == 2: var dritte: String = str(alle[(alle.find(erste) + 3) % alle.size()]) if not out.has(dritte): out.append(dritte) return out ## Der Tagesablauf: Grundtag, in den je Ursache ein Blocker gesetzt wird. static func _tagesablauf(i: int, heim: String, arbeit: String) -> Array: var slots: Array = [] for e in GRUNDTAG: var ort: String = str(e[1]) if ort == "heim": ort = heim elif ort == "arbeit": ort = arbeit slots.append([int(e[0]), ort, str(e[2]), str(e[3])]) var belegt: Dictionary = {} var nr := 0 for u in ursachen_von(i): for fenster in ["tag", "abend", "morgen", "nacht"]: var kandidaten: Array = Ursachen.blocker_fuer(str(u), fenster) if kandidaten.is_empty(): continue var blocker: String = str(kandidaten[(i + nr) % kandidaten.size()]) var gesetzt := false for k in range(slots.size()): if belegt.has(k) or str(slots[k][2]) == "schlaeft": continue if str(slots[k][3]) != fenster: continue slots[k][2] = blocker belegt[k] = true gesetzt = true break if gesetzt: break nr += 1 var out: Array = [] for e in slots: out.append([e[0], e[1], e[2]]) return out static func _mensch(i: int) -> Dictionary: var anliegen: Dictionary = ANLIEGEN[i % ANLIEGEN.size()] if i < PASSEND.size(): anliegen = ANLIEGEN[int(PASSEND[i][0])] var heim := heim_key(i) var arbeit := _naechster_arbeitsplatz(heim, i) return { "id": "mensch_%d" % i, "name": "%s %s" % [_vorname(i), _nachname(i)], "alter": 22 + (i * 7) % 38, "beruf": str(BERUFE[i % BERUFE.size()]), "traum": str(anliegen["traum"]), "problem": str(anliegen["problem"]), "tags": (anliegen["tags"] as Array).duplicate(), "ursachen": ursachen_von(i), "probleme": probleme_von(i), "mission": true, "heim": heim, "farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8), "routine": _tagesablauf(i, heim, arbeit), } ## Wer wo wohnt, arbeitet in der Nähe. Sonst laufen alle quer durch den Bezirk ## und kommen nie an. static func _naechster_arbeitsplatz(heim: String, i: int) -> String: if not PLACES.has(heim): return str(ARBEIT[i % ARBEIT.size()]) var von := Orte.center(PLACES, heim) var beste := INF var ziel := str(ARBEIT[i % ARBEIT.size()]) for a in ARBEIT: if not PLACES.has(a): continue var d := Orte.center(PLACES, str(a)).distance_to(von) if d < beste: beste = d ziel = str(a) return ziel static func _vorname(i: int) -> String: return str(VORNAMEN[i % VORNAMEN.size()]) static func _nachname(i: int) -> String: return str(NACHNAMEN[(i / VORNAMEN.size() + i) % NACHNAMEN.size()]) static func by_id(id: String) -> Dictionary: for h in LIST: if h["id"] == id: return h return {}