594 lines
20 KiB
GDScript
594 lines
20 KiB
GDScript
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
|