755 lines
27 KiB
GDScript
755 lines
27 KiB
GDScript
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 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
|
|
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), 3],
|
|
["cafe", "Café Nadel", Color(0.82, 0.62, 0.45), 2],
|
|
["buero", "Büro Ostwind", Color(0.62, 0.66, 0.72), 4],
|
|
["baeckerei", "Bäckerei Keller", Color(0.85, 0.72, 0.50), 2],
|
|
["werkstatt", "Werkstatt Loos", Color(0.70, 0.58, 0.55), 1],
|
|
]
|
|
|
|
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"]
|
|
|
|
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]
|
|
## Jeder Beruf hat einen Tagestyp -- der bestimmt, wann gearbeitet wird und
|
|
## wo im Tag Platz für Blocker ist. So bekommt jeder Mensch einen eigenen Alltag.
|
|
static var BERUF_TAGESTYP: Dictionary = {
|
|
"Illustratorin": "heim", "Programmiererin": "heim", "Musiker": "heim",
|
|
"Bäcker": "frueh", "Pfleger": "frueh", "Fahrer": "frueh", "Köchin": "frueh",
|
|
"Sachbearbeiter": "buero", "Archivarin": "buero", "Lehrer": "buero",
|
|
"Studentin": "studium", "Tischlerin": "werkstatt",
|
|
}
|
|
|
|
static var TAGESTYP_ARBEIT: Dictionary = {
|
|
"heim": "heim", "frueh": "baeckerei", "buero": "buero", "studium": "uni",
|
|
"werkstatt": "werkstatt",
|
|
}
|
|
|
|
static var TAGESTYPEN: Dictionary = {
|
|
"heim": [
|
|
[0, "heim", "schlaeft", "nacht"], [7, "heim", "morgenroutine", "morgen"],
|
|
[9, "heim", "arbeitet", "tag"], [12, "cafe", "essen", "tag"],
|
|
[14, "heim", "arbeitet", "tag"], [17, "park", "unterwegs", "abend"],
|
|
[19, "heim", "ruht", "abend"], [21, "heim", "ruht", "abend"],
|
|
[23, "heim", "abendroutine", "nacht"],
|
|
],
|
|
"frueh": [
|
|
[0, "heim", "schlaeft", "nacht"], [4, "arbeit", "arbeitet", "morgen"],
|
|
[10, "cafe", "pause", "tag"], [12, "heim", "essen", "tag"],
|
|
[14, "heim", "ruht", "tag"], [16, "park", "unterwegs", "abend"],
|
|
[18, "heim", "ruht", "abend"], [20, "heim", "ruht", "abend"],
|
|
[22, "heim", "abendroutine", "nacht"],
|
|
],
|
|
"buero": [
|
|
[0, "heim", "schlaeft", "nacht"], [6, "heim", "morgenroutine", "morgen"],
|
|
[8, "arbeit", "arbeitet", "tag"], [12, "cafe", "essen", "tag"],
|
|
[14, "arbeit", "arbeitet", "tag"], [18, "arbeit", "arbeitet", "abend"],
|
|
[20, "heim", "essen", "abend"], [22, "heim", "ruht", "abend"],
|
|
[23, "heim", "abendroutine", "nacht"],
|
|
],
|
|
"studium": [
|
|
[0, "heim", "schlaeft", "nacht"], [8, "heim", "morgenroutine", "morgen"],
|
|
[10, "arbeit", "arbeitet", "tag"], [13, "cafe", "essen", "tag"],
|
|
[15, "arbeit", "arbeitet", "tag"], [18, "heim", "ruht", "abend"],
|
|
[20, "park", "unterwegs", "abend"], [22, "heim", "ruht", "abend"],
|
|
[23, "heim", "abendroutine", "nacht"],
|
|
],
|
|
"werkstatt": [
|
|
[0, "heim", "schlaeft", "nacht"], [6, "heim", "morgenroutine", "morgen"],
|
|
[7, "arbeit", "arbeitet", "tag"], [12, "cafe", "essen", "tag"],
|
|
[13, "arbeit", "arbeitet", "tag"], [17, "heim", "ruht", "abend"],
|
|
[19, "park", "unterwegs", "abend"], [21, "heim", "ruht", "abend"],
|
|
[23, "heim", "abendroutine", "nacht"],
|
|
],
|
|
}
|
|
|
|
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()
|
|
_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):
|
|
LIST.append(_mensch(i))
|
|
for k in range(n_eigen):
|
|
LIST.append(mensch_aus_profil(eigene[k], EINWOHNER - n_eigen + k))
|
|
|
|
|
|
# ------------------------------------------------------------ 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 erste := PARZELLEN.size()
|
|
var tiefe := minf(RAND_TIEFE, minf(groesse.x, groesse.y) * 0.34)
|
|
var seiten := [
|
|
["back", Vector3(0, 0, 1), groesse.x, groesse.y],
|
|
["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)
|
|
_gasse_freischneiden(erste, start, ende, flanke)
|
|
var p_breite := (innen.x if laengs else innen.y) * 0.5 - ROJI * 0.5 - 0.8
|
|
if p_breite < PARZELLE_MIN * 0.8:
|
|
return
|
|
# 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"),
|
|
})
|
|
|
|
|
|
## Die Gasse läuft durch die Randreihe in den Block. Eine Parzelle, die auf
|
|
## ihr steht, wird in zwei Hälften links und rechts der Gasse geteilt --
|
|
## sonst stünde ein Haus quer über der Einfahrt.
|
|
static func _gasse_freischneiden(ab: int, start: Vector3, ende: Vector3, flanke: Vector3) -> void:
|
|
var quer_ist := absf(flanke.x) > 0.0 # Gasse läuft in z, Flanke ist x
|
|
var breite := ROJI * 0.5 + 0.8
|
|
var g_mitte: float = start.x if quer_ist else start.z
|
|
var g_von: float = minf(start.z, ende.z) if quer_ist else minf(start.x, ende.x)
|
|
var g_bis: float = maxf(start.z, ende.z) if quer_ist else maxf(start.x, ende.x)
|
|
var neu: Array = []
|
|
var weg: Array = []
|
|
for i in range(ab, PARZELLEN.size()):
|
|
var p: Dictionary = PARZELLEN[i]
|
|
var pos: Vector3 = p["pos"]
|
|
var s: Vector2 = p["size"]
|
|
var pf: float = pos.x if quer_ist else pos.z # Lage entlang der Flanke
|
|
var pl: float = pos.z if quer_ist else pos.x # Lage entlang der Gasse
|
|
var hf: float = (s.x if quer_ist else s.y) * 0.5
|
|
var hl: float = (s.y if quer_ist else s.x) * 0.5
|
|
if pl + hl < g_von or pl - hl > g_bis:
|
|
continue
|
|
if pf + hf < g_mitte - breite or pf - hf > g_mitte + breite:
|
|
continue
|
|
weg.append(i)
|
|
for teil in [[pf - hf, g_mitte - breite], [g_mitte + breite, pf + hf]]:
|
|
var w: float = float(teil[1]) - float(teil[0])
|
|
if w < 3.0:
|
|
continue
|
|
var m: float = (float(teil[0]) + float(teil[1])) * 0.5
|
|
neu.append({
|
|
"pos": Vector3(m, 0, pl) if quer_ist else Vector3(pl, 0, m),
|
|
"size": Vector2(w, s.y) if quer_ist else Vector2(s.x, w),
|
|
"dir": p["dir"],
|
|
})
|
|
weg.reverse()
|
|
for i in weg:
|
|
PARZELLEN.remove_at(i)
|
|
PARZELLEN.append_array(neu)
|
|
|
|
|
|
## 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:
|
|
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]) == "werkstatt":
|
|
wunsch = Vector3(0, 0, 30.0) # kurze Mittagspause: das Café muss nah sein
|
|
elif 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.
|
|
const WOHNUNG_MIN := 7.0 # schmaler passt neben die Tür kein Bett mehr
|
|
|
|
static func _wohnhaeuser_verteilen() -> void:
|
|
var frei: Array = []
|
|
for i in range(PARZELLEN.size()):
|
|
var g: Vector2 = PARZELLEN[i]["size"]
|
|
if not PARZELLEN[i].has("belegt") and minf(g.x, g.y) >= WOHNUNG_MIN:
|
|
frei.append(i)
|
|
if frei.is_empty():
|
|
return
|
|
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:
|
|
return _tag_belegen(GRUNDTAG, heim, arbeit, [], ursachen_von(i), i)
|
|
|
|
|
|
## Legt Blocker in den Tag. Zuerst bekommt jede Ursache einen -- sonst wäre sie
|
|
## nie zu erkennen und bremste den Traum für immer. Danach kommen weitere
|
|
## gewünschte dazu, bis der Tag voll ist. Jeder Blocker nur in sein Zeitfenster.
|
|
static func _tag_belegen(vorlage: Array, heim: String, arbeit: String,
|
|
gewuenscht: Array, ursachen: Array, i: int) -> Array:
|
|
var slots: Array = []
|
|
for e in vorlage:
|
|
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 gesetzt: Array = []
|
|
|
|
# 1. je Ursache ein Blocker -- aus den gewünschten, sonst aus dem Katalog
|
|
var nr := 0
|
|
for u in ursachen:
|
|
var kandidaten: Array = []
|
|
for b in gewuenscht:
|
|
if Ursachen.ursache_von(str(b)) == str(u):
|
|
kandidaten.append(str(b))
|
|
for b in Ursachen.alle_blocker(str(u)):
|
|
if not kandidaten.has(str(b)):
|
|
kandidaten.append(str(b))
|
|
for k in range(kandidaten.size()):
|
|
var b: String = str(kandidaten[(k + i + nr) % kandidaten.size()])
|
|
if _blocker_setzen(slots, belegt, b):
|
|
gesetzt.append(b)
|
|
break
|
|
nr += 1
|
|
|
|
# 2. weitere gewünschte, solange Platz ist
|
|
for b in gewuenscht:
|
|
if gesetzt.size() >= BLOCKER_MAX:
|
|
break
|
|
if gesetzt.has(str(b)) or not Ursachen.BLOCKER.has(b):
|
|
continue
|
|
if _blocker_setzen(slots, belegt, str(b)):
|
|
gesetzt.append(str(b))
|
|
|
|
var out: Array = []
|
|
for e in slots:
|
|
out.append([e[0], e[1], e[2]])
|
|
return out
|
|
|
|
|
|
## Ein Blocker in den ersten freien Slot seines Zeitfensters. Nie in den Schlaf.
|
|
static func _blocker_setzen(slots: Array, belegt: Dictionary, b: String) -> bool:
|
|
var fenster := str(Ursachen.BLOCKER[b]["zeit"])
|
|
for k in range(slots.size()):
|
|
if belegt.has(k) or str(slots[k][2]) == "schlaeft" or str(slots[k][3]) != fenster:
|
|
continue
|
|
slots[k][2] = b
|
|
belegt[k] = true
|
|
return true
|
|
return false
|
|
|
|
|
|
## Ein Mensch, den jemand selbst geschrieben hat. Name, Beruf, Traum und
|
|
## Geschichte kommen vom Aushang; Ursachen, Blocker, Klemmen und Verhalten
|
|
## schließt das Spiel aus den angekreuzten Wörtern.
|
|
static func mensch_aus_profil(pr: Dictionary, i: int) -> Dictionary:
|
|
var bild := Woerter.profil(pr.get("woerter", []))
|
|
var beruf := str(pr.get("beruf", ""))
|
|
var typ := str(BERUF_TAGESTYP.get(beruf, "buero"))
|
|
var heim := heim_key(i)
|
|
var arbeit := str(TAGESTYP_ARBEIT.get(typ, "buero"))
|
|
if arbeit == "heim":
|
|
arbeit = heim
|
|
elif not PLACES.has(arbeit):
|
|
arbeit = _naechster_arbeitsplatz(heim, i)
|
|
return {
|
|
"id": str(pr.get("id", "eigen_%d" % i)),
|
|
"name": str(pr.get("name", "Unbekannt")),
|
|
"alter": int(pr.get("alter", 30)),
|
|
"beruf": beruf,
|
|
"traum": str(pr.get("traum", "")),
|
|
"problem": str(pr.get("geschichte", "")),
|
|
"tags": [],
|
|
"ursachen": bild["ursachen"],
|
|
"probleme": bild["probleme"],
|
|
"verhalten": bild["verhalten"],
|
|
"woerter": Array(pr.get("woerter", [])),
|
|
"eigen": true,
|
|
"mission": true,
|
|
"heim": heim,
|
|
"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),
|
|
}
|
|
|
|
|
|
## Der Zufall-Knopf: füllt einen Aushang so, wie der Generator Menschen macht.
|
|
static func zufallsprofil() -> Dictionary:
|
|
var i := randi() % 1000
|
|
var anliegen: Dictionary = ANLIEGEN[i % ANLIEGEN.size()]
|
|
var alle: Array = Woerter.ids()
|
|
alle.shuffle()
|
|
var woerter: Array = alle.slice(0, Woerter.MINDESTENS + randi() % 4)
|
|
return {
|
|
"name": "%s %s" % [_vorname(i), _nachname(i)],
|
|
"alter": 20 + randi() % 45,
|
|
"beruf": str(BERUFE[i % BERUFE.size()]),
|
|
"traum": str(anliegen["traum"]),
|
|
"geschichte": str(anliegen["problem"]),
|
|
"woerter": woerter,
|
|
}
|
|
|
|
|
|
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),
|
|
"verhalten": {},
|
|
"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 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:
|
|
return h
|
|
return {}
|