This commit is contained in:
team3
2026-08-31 11:07:01 +02:00
parent f936dd2b2a
commit ef2ecc08f9
19 changed files with 2094 additions and 438 deletions

View File

@@ -1,15 +1,17 @@
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 Haus und sein
## Bewohner werden erzeugt — der Reihe nach, immer weiter nach außen.
## 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 HAEUSER_JE_RING := 6
const RING_ERSTER := 30.0
const RING_ABSTAND := 24.0
## Häuser sitzen auf Achtelrichtungen. Die Achsen 0 (Hörsaal) und 4 (Tor)
## bleiben frei, damit der Weg vom Dorfplatz zu beiden offen bleibt.
const SLOTS := [1, 2, 3, 5, 6, 7]
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
static var FEST: Dictionary = {
"hoersaal": {
@@ -29,6 +31,8 @@ static var FEST: Dictionary = {
## 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",
@@ -56,47 +60,346 @@ static func haus_key(i: int) -> String:
return "haus_%d" % i
## Baut Orte und Bewohner für ein Dorf mit so vielen Häusern neu auf.
## 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)):
PLACES[haus_key(i)] = _haus(i)
var haus := _platz_suchen(i)
PLACES[haus_key(i)] = haus
_anschliessen(haus_key(i))
LIST.append(_bewohner(i))
## Wie weit reicht das Dorf hinaus?
static func aeusserster_ring(haeuser: int) -> float:
if haeuser <= 0:
return 0.0
var ring := (haeuser - 1) / HAEUSER_JE_RING
return RING_ERSTER + float(ring) * RING_ABSTAND
## 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)
static func _haus(i: int) -> Dictionary:
var ring := i / HAEUSER_JE_RING
var winkel := float(SLOTS[i % HAEUSER_JE_RING]) * TAU / 8.0
var r := RING_ERSTER + float(ring) * RING_ABSTAND
var x := sin(winkel) * r
var z := cos(winkel) * r
# ------------------------------------------------------------ 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": [x, z],
"pos": [bester.x, bester.z],
"size": [breite, tiefe],
"dir": _tuer_zur_mitte(x, z),
"dir": _tuer_zum_netz(bester, groesse),
"color": Color.from_hsv(fmod(0.60 + float(i) * 0.137, 1.0), 0.34, 0.86),
}
## Die Tür zeigt zur Dorfmitte — auf die Seite, die ihr am nächsten liegt.
static func _tuer_zur_mitte(x: float, z: float) -> String:
if absf(x) > absf(z):
return "left" if x > 0.0 else "right"
return "forward" if z > 0.0 else "back"
## 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"]
if float(s[0]) <= 0.0:
continue
var c := Orte.center(PLACES, key)
var dx: float = absf(p.x - c.x) - (groesse.x + float(s[0])) * 0.5
var dz: float = absf(p.z - c.z) - (groesse.y + float(s[1])) * 0.5
if dx < HAUS_ABSTAND and dz < HAUS_ABSTAND:
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)
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)
## Weg von a nach b, der kein Haus schneidet. Frei? Dann die Gerade. Sonst
## Dijkstra über ein grobes Raster, danach auf wenige Knicke geglättet.
static func _route_um_haeuser(a: Vector3, b: Vector3, ausser: String) -> Array:
if _weg_frei(a, b, ausser):
return [a, b]
var rand := 30.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 := 5.0
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 [a, b]
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 = []
@@ -120,7 +423,7 @@ static func _name(i: int) -> String:
return str(SILBE_A[j % SILBE_A.size()]) + str(SILBE_B[(j / SILBE_A.size()) % SILBE_B.size()])
## Genitiv: "Ailens", aber "Morrs" und "Esas'" — Apostroph nur nach Zischlaut.
## 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"