update
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user