class_name DreamDB ## Die Traumwelt: ein Dorf, das mit jedem erfüllten Traum wächst. ## Fest sind nur Hörsaal, Dorfplatz und Tor zur Erde. Jedes weitere Haus sucht ## sich seinen Platz selbst: es wird dort gebaut, wo es Nachbarn, einen Weg und ## den Dorfplatz in erträglicher Nähe hat. Der Weg dorthin dockt an der ## nächsten Stelle des bestehenden Netzes an. Weil Wiederverwendung gratis ist, ## gabeln sich die Wege und werden verwinkelt, statt sternförmig zu laufen. ## Vorbild: Emilien et al., "Procedural Generation of Villages" (2012). const PLATZ_FREI := 22.0 # Radius um den Dorfplatz, der leer bleibt const HAUS_ABSTAND := 4.0 # Mindestluft zwischen zwei Gebäuden const WEG_ABSTAND := 3.0 # Mindestluft zwischen Haus und Weg const KANDIDATEN := 64 # Probepunkte je Haus const SPINE_SCHRITT := 8.0 # Abstand der Knoten auf der Hauptachse ## Die Insel ist ein Hügel. Innen eben -- Dorfplatz, Hörsaal, Tor --, ab FLACH ## steigt sie an. Wer später einzieht, wohnt weiter außen und damit höher: ## das Dorf wächst den Hügel hinauf, und vom Platz aus sieht man, wie weit. const FLACH := 24.0 const STEIGUNG := 0.30 const GIPFEL := 36.0 static var FEST: Dictionary = { "hoersaal": { "name": "Hörsaal der Wege", "pos": [0.0, 34.0], "size": [16.0, 12.0], "dir": "forward", "color": Color(0.66, 0.72, 0.94), }, "lichtung": { "name": "Der Dorfplatz", "pos": [0.0, 2.0], "size": [0.0, 0.0], "dir": "none", "color": Color(0.60, 0.50, 0.85), }, "tor": { "name": "Tor zur Erde", "pos": [0.0, -34.0], "size": [0.0, 0.0], "dir": "none", "color": Color(0.50, 0.80, 1.0), }, } ## Wird von erzeuge() gefüllt und wächst mit dem Dorf. static var PLACES: Dictionary = FEST.duplicate(true) static var LIST: Array = [] static var NETZ: Netz = Netz.new() static var ANKER: Dictionary = {} static var FESTE_NAMEN: Array = ["Ailen", "Morr", "Ivo", "Sela"] static var FESTE_ROLLEN: Array = ["Traumweberin", "Erinnerungssammler", "Bote", "Wächterin des Tors"] static var SILBE_A: Array = ["Nu", "Ta", "Es", "Wre", "O", "Ma", "Si", "Te"] static var SILBE_B: Array = ["un", "lia", "kar", "n", "vin", "lu", "rin", "nne"] static var ROLLEN: Array = ["Lichtgießer", "Kartenzeichnerin", "Schwellenhüter", "Fadensucherin", "Stillenzähler", "Wegbereiterin", "Klangsammler", "Nachtgärtnerin"] ## Vier Tagesabläufe, alle mit reichlich Zeit zwischen den Stationen — ## auch ein Haus am äußeren Ring ist so bequem zu erreichen. static var ABLAEUFE: Array = [ [[0, "haus", "traeumt"], [6, "lichtung", "ruht"], [10, "haus", "webt"], [14, "lichtung", "singt"], [18, "haus", "sortiert"], [21, "haus", "traeumt"]], [[0, "haus", "traeumt"], [6, "lichtung", "sammelt"], [10, "haus", "sortiert"], [15, "tor", "beobachtet"], [19, "haus", "webt"], [21, "haus", "traeumt"]], [[0, "tor", "wacht"], [7, "lichtung", "ruht"], [11, "haus", "sortiert"], [15, "tor", "wacht"], [19, "lichtung", "singt"], [21, "haus", "traeumt"]], [[0, "haus", "traeumt"], [4, "tor", "beobachtet"], [8, "lichtung", "wandert"], [12, "haus", "ruht"], [16, "lichtung", "singt"], [20, "haus", "traeumt"]], ] ## Höhe des nackten Hangs bei diesem Abstand von der Mitte. static func hang(r: float) -> float: return clampf((r - FLACH) * STEIGUNG, 0.0, GIPFEL) ## Jedes Gebäude steht auf einer ebenen Terrasse in Höhe seiner Tür -- so ist ## die Türseite ebenerdig, die Bergseite eingegraben. Das Tor hat keine Tür; ## seine Terrasse liegt auf Höhe der Dorfseite. const TOR_FLAECHE := 12.0 const TERRASSE_RAND := 2.6 # bis zur Türschwelle davor const BOESCHUNG := 5.0 # so weit läuft die Terrasse in den Hang aus static func terrasse(key: String) -> float: if not PLACES.has(key): return 0.0 if key == "tor": var c := Orte.center(PLACES, key) return hang(Vector2(c.x, c.z).length() - TOR_FLAECHE * 0.5) var t := Orte.door(PLACES, key) return hang(Vector2(t.x, t.z).length()) ## Höhe des Bodens an dieser Stelle: Terrasse in einem Gebäude, sonst Hang. ## Wo sich Terrassenränder überlappen, gilt das Gebäude, in dem der Punkt ## wirklich liegt -- erst danach der Rand. static func hoehe_bei(x: float, z: float) -> float: var h := hang(Vector2(x, z).length()) var rand_treffer := "" var boeschung_key := "" var boeschung_t := 1.0 var reihenfolge: Array = PLACES.keys() reihenfolge.erase("tor") reihenfolge.append("tor") for key in reihenfolge: var s: Array = PLACES[key]["size"] var bx: float = float(s[0]) var bz: float = float(s[1]) if key == "tor": bx = TOR_FLAECHE bz = TOR_FLAECHE if bx <= 0.0 or not Game.ort_offen(key): continue var c := Orte.center(PLACES, key) var dx := absf(x - c.x) - bx * 0.5 var dz := absf(z - c.z) - bz * 0.5 if dx <= 0.0 and dz <= 0.0: return terrasse(key) if dx <= TERRASSE_RAND and dz <= TERRASSE_RAND: if rand_treffer == "": rand_treffer = key continue # außerhalb des Rands: die Terrasse läuft als Böschung in den Hang aus var d := maxf(dx, dz) - TERRASSE_RAND if d < BOESCHUNG: var t := d / BOESCHUNG if t < boeschung_t: boeschung_t = t boeschung_key = key if rand_treffer != "": return terrasse(rand_treffer) if boeschung_key != "": var tt := boeschung_t * boeschung_t * (3.0 - 2.0 * boeschung_t) # weich return lerpf(terrasse(boeschung_key), h, tt) return h static func haus_key(i: int) -> String: return "haus_%d" % i ## Baut Orte, Wegenetz und Bewohner für ein Dorf mit so vielen Häusern neu auf. ## Deterministisch: Haus i landet immer am selben Fleck. static func erzeuge(haeuser: int) -> void: PLACES = FEST.duplicate(true) LIST = [] NETZ = Netz.new() ANKER = {} _hauptachse() for i in range(maxi(haeuser, 0)): var haus := _platz_suchen(i) PLACES[haus_key(i)] = haus _anschliessen(haus_key(i)) LIST.append(_bewohner(i)) ## Die feste Achse Hörsaal – Dorfplatz – Tor. Sie ist von Anfang an da und ## trägt genug Knoten, damit Häuser überall andocken können. static func _hauptachse() -> void: var punkte: Array[Vector3] = [] var a := Orte.door(PLACES, "hoersaal") var m := Orte.center(PLACES, "lichtung") var b := Orte.door(PLACES, "tor") for paar in [[a, m], [m, b]]: var von: Vector3 = paar[0] var nach: Vector3 = paar[1] var n := maxi(1, int(round(von.distance_to(nach) / SPINE_SCHRITT))) for k in range(n + 1): var p := von.lerp(nach, float(k) / float(n)) if punkte.is_empty() or punkte[punkte.size() - 1].distance_to(p) > 0.6: punkte.append(p) var vorher := -1 for p in punkte: var idx := NETZ.fuege_knoten(p) if vorher >= 0: NETZ.verbinde(vorher, idx) vorher = idx ANKER["hoersaal"] = NETZ.fuege_knoten(a) ANKER["lichtung"] = NETZ.fuege_knoten(m) ANKER["tor"] = NETZ.fuege_knoten(b) # ------------------------------------------------------------ Platzsuche ## Wählt unter vielen Probepunkten den mit dem höchsten Interesse. static func _platz_suchen(i: int) -> Dictionary: var rng := RandomNumberGenerator.new() rng.seed = 4711 + i * 7919 var breite := 12.0 + float(i % 2) * 2.0 var tiefe := 10.0 + float(i % 3) var groesse := Vector2(breite, tiefe) # Die Suchfläche wächst mit der Zahl der Häuser, nicht mit dem weitesten # Ausreißer. So bleibt die Dichte gleich, statt zu zerfasern. var aussen := maxf(_wachstumsradius() + 8.0, sqrt(PLATZ_FREI * PLATZ_FREI + 240.0 * float(i + 1))) var bester := Vector3.ZERO var bestes := -1.0 for k in range(KANDIDATEN): var winkel := rng.randf() * TAU var r := PLATZ_FREI + rng.randf() * maxf(6.0, aussen - PLATZ_FREI) var p := Vector3(cos(winkel) * r, 0.0, sin(winkel) * r) if not _platz_frei(p, groesse): continue var wert := _interesse(p) if wert > bestes: bestes = wert bester = p if bestes < 0.0: # Notnagel: irgendwo weit außen, damit das Dorf nie steckenbleibt bester = _notplatz(i, groesse) return { "name": "%s Haus" % _besitz(_name(i)), "pos": [bester.x, bester.z], "size": [breite, tiefe], "dir": _tuer_zum_netz(bester, groesse), "color": Color.from_hsv(fmod(0.60 + float(i) * 0.137, 1.0), 0.34, 0.86), } ## Wie weit reichen die Häuser? Danach richtet sich, wo neu gesucht wird -- ## nicht nach Hörsaal und Tor, sonst zerfasert das Dorf sofort. static func _wachstumsradius() -> float: var r := PLATZ_FREI + 8.0 for key in PLACES: if not key.begins_with("haus_"): continue var s: Array = PLACES[key]["size"] var c := Orte.center(PLACES, key) r = maxf(r, Vector2(absf(c.x) + float(s[0]) * 0.5, absf(c.z) + float(s[1]) * 0.5).length()) return r static func _notplatz(i: int, groesse: Vector2) -> Vector3: var r := aussenradius() + 20.0 for k in range(72): var a := TAU * float((i * 29 + k) % 72) / 72.0 var p := Vector3(cos(a) * r, 0.0, sin(a) * r) if _platz_frei(p, groesse): return p return Vector3(0, 0, r) ## Harte Bedingungen: nicht auf dem Dorfplatz, nicht in einem anderen Gebäude, ## nicht auf einem Weg. static func _platz_frei(p: Vector3, groesse: Vector2) -> bool: if p.length() - maxf(groesse.x, groesse.y) * 0.5 < PLATZ_FREI: return false for key in PLACES: var s: Array = PLACES[key]["size"] var bx: float = float(s[0]) var bz: float = float(s[1]) if key == "tor": bx = TOR_FLAECHE bz = TOR_FLAECHE if bx <= 0.0: continue 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 # 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): return false return true ## Wie gut liegt dieser Punkt? Nachbarschaft, Weganschluss, Nähe zum Platz. static func _interesse(p: Vector3) -> float: var sozial := 0.0 var n := 0 for key in PLACES: if not key.begins_with("haus_"): continue var d := Orte.center(PLACES, key).distance_to(p) sozial += _glocke(d, 16.0, 21.0, 46.0) n += 1 if n > 0: sozial = sozial / sqrt(float(n)) var an := NETZ.andocken(p) var weg := _glocke(float(an["dist"]), 3.0, 9.0, 34.0) var platz := _glocke(p.length(), PLATZ_FREI - 4.0, PLATZ_FREI + 6.0, 110.0) return 1.0 * sozial + 1.3 * weg + 0.5 * platz + 0.8 * _freiraum(p) ## Richtungen, in denen schon viel steht, sind weniger interessant. Ohne das ## wächst das Dorf einseitig davon und die halbe Insel bleibt leer. static func _freiraum(p: Vector3) -> float: var w := atan2(p.z, p.x) var nah := 0.0 var alle := 0.0 for key in PLACES: if not key.begins_with("haus_"): continue var c := Orte.center(PLACES, key) alle += 1.0 if absf(angle_difference(atan2(c.z, c.x), w)) < PI / 3.0: nah += 1.0 if alle < 1.0: return 1.0 return clampf(1.0 - 3.0 * nah / alle, 0.0, 1.0) ## Dreiecksglocke: 0 unter lo, 1 bei spitze, 0 über hi. static func _glocke(d: float, lo: float, spitze: float, hi: float) -> float: if d <= lo or d >= hi: return 0.0 if d < spitze: return (d - lo) / maxf(0.001, spitze - lo) return (hi - d) / maxf(0.001, hi - spitze) ## Die Tür zeigt auf die Seite, die dem Wegenetz am nächsten liegt. static func _tuer_zum_netz(p: Vector3, groesse: Vector2) -> String: var ziel: Vector3 = NETZ.andocken(p)["punkt"] var d := ziel - p if absf(d.x) * groesse.y > absf(d.z) * groesse.x: return "left" if d.x < 0.0 else "right" return "forward" if d.z < 0.0 else "back" # ------------------------------------------------------------ Weganschluss ## Verbindet ein neues Haus mit dem Netz. Der Weg geht um Häuser herum, statt ## durch sie hindurch. Weil er am nächsten Punkt des Netzes andockt und nicht ## in der Dorfmitte, gabeln sich die Wege und werden verwinkelt. static func _anschliessen(key: String) -> void: var tuer := Orte.door(PLACES, key) var an := NETZ.andocken(tuer) var von: Vector3 = an["punkt"] var linie := _route_um_haeuser(tuer, von, key) if not _linie_frei(linie, key): # Der nächste Punkt des Netzes liegt eingeklemmt. Dann den nächsten # Knoten nehmen, zu dem ein sauberer Weg führt -- nie durch ein Haus. var reihe: Array = range(NETZ.knoten.size()) reihe.sort_custom(func(i, j) -> bool: return NETZ.knoten[i].distance_to(tuer) < NETZ.knoten[j].distance_to(tuer)) for idx in reihe.slice(0, 16): var l2 := _route_um_haeuser(tuer, NETZ.knoten[idx], key) if _linie_frei(l2, key): linie = l2 an = {"punkt": NETZ.knoten[idx], "knoten": idx} break var vorher := -1 for i in range(linie.size()): var idx := NETZ.fuege_knoten(linie[i]) if vorher >= 0: NETZ.verbinde(vorher, idx) vorher = idx if i == 0: ANKER[key] = idx if vorher >= 0: NETZ.verbinde(vorher, int(an["knoten"])) if not ANKER.has(key): ANKER[key] = NETZ.fuege_knoten(tuer) _schleife(int(ANKER[key]), key) ## Schneidet diese Linie irgendwo ein Gebäude? static func _linie_frei(linie: Array, ausser: String) -> bool: for i in range(linie.size() - 1): if not _weg_frei(linie[i], linie[i + 1], ausser): return false return true ## Weg von a nach b, der kein Haus schneidet. Frei? Dann die Gerade. Sonst ## Dijkstra über ein Raster -- erst grob, dann fein --, danach geglättet. static func _route_um_haeuser(a: Vector3, b: Vector3, ausser: String) -> Array: if _weg_frei(a, b, ausser): return [a, b] for schritt_start in [5.0, 2.5]: var weg := _route_raster(a, b, ausser, schritt_start) if not weg.is_empty(): return weg return [a, b] static func _route_raster(a: Vector3, b: Vector3, ausser: String, schritt_start: float) -> Array: var rand := 34.0 var min_x := minf(a.x, b.x) - rand var max_x := maxf(a.x, b.x) + rand var min_z := minf(a.z, b.z) - rand var max_z := maxf(a.z, b.z) + rand var schritt := schritt_start var nx := int((max_x - min_x) / schritt) + 1 var nz := int((max_z - min_z) / schritt) + 1 while nx * nz > 2600: schritt *= 1.5 nx = int((max_x - min_x) / schritt) + 1 nz = int((max_z - min_z) / schritt) + 1 var start := Vector2i(clampi(int(round((a.x - min_x) / schritt)), 0, nx - 1), clampi(int(round((a.z - min_z) / schritt)), 0, nz - 1)) var ziel := Vector2i(clampi(int(round((b.x - min_x) / schritt)), 0, nx - 1), clampi(int(round((b.z - min_z) / schritt)), 0, nz - 1)) var kosten: Dictionary = {start: 0.0} var her: Dictionary = {} var offen: Array = [start] var gefunden := false while not offen.is_empty(): var best := 0 for i in range(1, offen.size()): if float(kosten[offen[i]]) < float(kosten[offen[best]]): best = i var c: Vector2i = offen[best] offen.remove_at(best) if c == ziel: gefunden = true break var pa := Vector3(min_x + float(c.x) * schritt, 0.0, min_z + float(c.y) * schritt) for dx in [-1, 0, 1]: for dz in [-1, 0, 1]: if dx == 0 and dz == 0: continue var n := Vector2i(c.x + dx, c.y + dz) if n.x < 0 or n.y < 0 or n.x >= nx or n.y >= nz: continue var pb := Vector3(min_x + float(n.x) * schritt, 0.0, min_z + float(n.y) * schritt) if not _weg_frei(pa, pb, ausser): continue var neu: float = float(kosten[c]) + pa.distance_to(pb) if not kosten.has(n) or neu < float(kosten[n]) - 0.001: kosten[n] = neu her[n] = c if not offen.has(n): offen.append(n) if not gefunden: return [] var roh: Array = [] var cur: Vector2i = ziel while cur != start: roh.push_front(Vector3(min_x + float(cur.x) * schritt, 0.0, min_z + float(cur.y) * schritt)) cur = her[cur] roh.push_front(a) roh.append(b) return _glaetten(roh, ausser) ## Zieht die Rasterspur auf wenige Knicke zusammen: immer so weit springen, ## wie die Sicht frei ist. static func _glaetten(linie: Array, ausser: String) -> Array: var out: Array = [linie[0]] var i := 0 while i < linie.size() - 1: var j := linie.size() - 1 while j > i + 1 and not _weg_frei(linie[i], linie[j], ausser): j -= 1 out.append(linie[j]) i = j return out ## Ein Umweg über das halbe Dorf ist unschön. Liegt ein Knoten nah, ist aber im ## Netz weit weg, wird abgekürzt. Daraus entstehen Schleifen statt Sackgassen. static func _schleife(neu: int, key: String) -> void: var p: Vector3 = NETZ.knoten[neu] var bester := -1 var beste := INF for i in range(NETZ.knoten.size()): if i == neu: continue var d := NETZ.knoten[i].distance_to(p) if d > 52.0 or d < 11.0 or d >= beste: continue if NETZ.weglaenge(neu, i) < d * 1.9: continue if not _weg_frei(p, NETZ.knoten[i], key): continue beste = d bester = i if bester >= 0: NETZ.verbinde(neu, bester) ## Kein Weg darf durch ein Gebäude laufen -- auch nicht durch das eigene. ## Am eigenen Haus reicht knappe Luft, sonst käme die Tür nie frei. static func _weg_frei(a: Vector3, b: Vector3, ausser: String) -> bool: for key in PLACES: var s: Array = PLACES[key]["size"] if float(s[0]) <= 0.0: continue var c := Orte.center(PLACES, key) var rand := 0.2 if key == ausser else 1.0 if Netz.trifft_rechteck(a, b, c, Vector2(float(s[0]), float(s[1])), rand): return false return true # ------------------------------------------------------------ Maße ## Wie weit reicht das Dorf hinaus? static func aussenradius() -> float: var r := 0.0 for key in PLACES: var s: Array = PLACES[key]["size"] var c := Orte.center(PLACES, key) var ecke := Vector2(absf(c.x) + float(s[0]) * 0.5, absf(c.z) + float(s[1]) * 0.5) r = maxf(r, ecke.length()) for k in NETZ.knoten: r = maxf(r, k.length() + 2.0) return r # ------------------------------------------------------------ Bewohner static func _bewohner(i: int) -> Dictionary: var haus := haus_key(i) 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, "von": von, "haus": haus, "leuchtet": true, "farbe": Color.from_hsv(fmod(0.72 + float(i) * 0.161, 1.0), 0.55, 0.95), "routine": ablauf, } static func _name(i: int) -> String: if i < FESTE_NAMEN.size(): return str(FESTE_NAMEN[i]) var j := i - FESTE_NAMEN.size() return str(SILBE_A[j % SILBE_A.size()]) + str(SILBE_B[(j / SILBE_A.size()) % SILBE_B.size()]) ## Genitiv: "Ailens", aber "Esas'" -- Apostroph nur nach Zischlaut. static func _besitz(n: String) -> String: var letzter := n.substr(n.length() - 1).to_lower() return n + "'" if ["s", "z", "x", "ß"].has(letzter) else n + "s" static func _rolle(i: int) -> String: if i < FESTE_ROLLEN.size(): return str(FESTE_ROLLEN[i]) return str(ROLLEN[(i - FESTE_ROLLEN.size()) % ROLLEN.size()]) static func by_id(id: String) -> Dictionary: for w in LIST: if w["id"] == id: return w return {} # ------------------------------------------------------------------ Lehrorte ## Der Hörsaal lehrt den ersten Weg. Jedes Haus bringt einen weiteren mit. static func techniken_von(place_key: String) -> Array: var nr := -1 if place_key == "hoersaal": nr = 0 elif place_key.begins_with("haus_"): nr = int(place_key.substr(5)) + 1 if nr < 0 or nr >= TechDB.LIST.size(): return [] return [TechDB.LIST[nr]["id"]] ## An welchem Ort wird diese Technik gelehrt? "" wenn nirgends. static func ort_von_technik(tech_id: String) -> String: for i in range(TechDB.LIST.size()): if TechDB.LIST[i]["id"] == tech_id: return "hoersaal" if i == 0 else haus_key(i - 1) return "" ## Alle Orte, an denen im aktuellen Dorf gelehrt wird. static func lehrorte() -> Array: var out: Array = ["hoersaal"] for i in range(LIST.size()): if not techniken_von(haus_key(i)).is_empty(): out.append(haus_key(i)) return out