Files
dreamer-game/scripts/dream_db.gd
2026-09-16 16:15:32 +02:00

594 lines
20 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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