update
This commit is contained in:
@@ -3,7 +3,7 @@ class_name Bewohner
|
||||
## Bewohner einer Welt: läuft seinen Tagesablauf ab. Auf der Erde kann er von
|
||||
## einer Technik erreicht werden -- oder eben nicht.
|
||||
|
||||
const GEHTEMPO := 6.0
|
||||
const GEHTEMPO := 8.0
|
||||
|
||||
var data: Dictionary
|
||||
var idx: int = 0
|
||||
@@ -11,6 +11,8 @@ var place: String = ""
|
||||
var activity: String = ""
|
||||
var resist_left: float = 0.0
|
||||
var places: Dictionary = {}
|
||||
var netz: Netz = null
|
||||
var anker: Dictionary = {}
|
||||
var mit_mission := false
|
||||
|
||||
var _visual: Node3D
|
||||
@@ -21,14 +23,18 @@ var _aura: MeshInstance3D
|
||||
var _strahl: MeshInstance3D
|
||||
var _path: Array = []
|
||||
var _letzte_aktivitaet: String = ""
|
||||
var _seit_regel: float = 0.0
|
||||
var _bob: float = 0.0
|
||||
var _offset: Vector3
|
||||
|
||||
|
||||
func setup(d: Dictionary, index: int, orte: Dictionary) -> void:
|
||||
func setup(d: Dictionary, index: int, orte: Dictionary,
|
||||
wegenetz: Netz = null, wegeanker: Dictionary = {}) -> void:
|
||||
data = d
|
||||
idx = index
|
||||
places = orte
|
||||
netz = wegenetz
|
||||
anker = wegeanker
|
||||
mit_mission = bool(d.get("mission", false))
|
||||
name = "Bewohner_" + str(d["id"])
|
||||
var winkel := float(index) * TAU / 4.0
|
||||
@@ -73,13 +79,14 @@ func _routine_at(h: float) -> Array:
|
||||
return [cur[1], cur[2]]
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
func _process(roh: float) -> void:
|
||||
var delta := roh * Game.zeit_faktor()
|
||||
resist_left = maxf(0.0, resist_left - delta)
|
||||
|
||||
var soll := _routine_at(Game.time_h)
|
||||
if soll[0] != place:
|
||||
var war_unterwegs := not _path.is_empty()
|
||||
_path = Orte.route(places, place, soll[0], _offset)
|
||||
_path = Orte.route(places, place, soll[0], _offset, netz, anker)
|
||||
if war_unterwegs and _path.size() > 1:
|
||||
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
|
||||
place = soll[0]
|
||||
@@ -110,8 +117,15 @@ func _process(delta: float) -> void:
|
||||
_visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0,
|
||||
1.0 - pow(0.01, delta))
|
||||
|
||||
# Solange der Moment anhält, greift die Regel erneut, sobald ihre Technik
|
||||
# wieder frei ist. Der Cooldown gibt damit den Takt vor.
|
||||
_seit_regel += delta
|
||||
if activity != _letzte_aktivitaet:
|
||||
_letzte_aktivitaet = activity
|
||||
_seit_regel = 0.0
|
||||
_regel_pruefen()
|
||||
elif _seit_regel >= 1.0:
|
||||
_seit_regel = 0.0
|
||||
_regel_pruefen()
|
||||
|
||||
var erledigt: bool = mit_mission and Game.completed.has(data["id"])
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -7,6 +7,7 @@ var spawn := Vector3(0, 2.0, 20.0)
|
||||
var bewohner: Array = []
|
||||
|
||||
var _deko: Array = []
|
||||
var _freihalten: Array[Vector3] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -17,6 +18,7 @@ func _ready() -> void:
|
||||
_schwarzes_brett()
|
||||
_lichtung()
|
||||
_tor_zur_erde()
|
||||
_gruen()
|
||||
_wesen()
|
||||
|
||||
|
||||
@@ -59,18 +61,14 @@ func _insel() -> void:
|
||||
Prims.cyl(self, Vector3(0, -4.0, 0), radius * 0.77, 4.0, Color(0.34, 0.26, 0.50), false)
|
||||
Prims.cyl(self, Vector3(0, -9.0, 0), radius * 0.42, 6.0, Color(0.24, 0.17, 0.38), false)
|
||||
|
||||
# Ringweg, auf dem die Wesen gehen
|
||||
for i in range(36):
|
||||
var a := TAU * float(i) / 36.0
|
||||
Prims.box(self, Vector3(cos(a) * 15.0, 0.03, sin(a) * 15.0),
|
||||
Vector3(2.6, 0.06, 2.6), Color(0.66, 0.58, 0.84), false)
|
||||
_wege()
|
||||
|
||||
# schwebende Deko
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 7
|
||||
for i in range(34):
|
||||
var a := rng.randf() * TAU
|
||||
var r := rng.randf_range(18.0, Game.insel_radius() - 4.0)
|
||||
var r := rng.randf_range(18.0, maxf(24.0, Game.insel_radius() - 4.0))
|
||||
var p := Vector3(cos(a) * r, rng.randf_range(5.0, 24.0), sin(a) * r)
|
||||
var s := rng.randf_range(0.6, 2.0)
|
||||
var c := Color(0.6 + rng.randf() * 0.3, 0.5 + rng.randf() * 0.4, 1.0)
|
||||
@@ -79,6 +77,91 @@ func _insel() -> void:
|
||||
_deko.append(wuerfel)
|
||||
|
||||
|
||||
## Die gewachsenen Wege: je Kante ein flaches Band, an den Knoten ein Fleck,
|
||||
## damit Ecken nicht ausfransen.
|
||||
## Begrünung nach Regel, nicht von Hand: Poisson-Scheibe über der freien
|
||||
## Fläche. Nichts wächst auf einem Weg, in einem Haus oder auf dem Dorfplatz.
|
||||
func _gruen() -> void:
|
||||
_freihalten.append(Vector3(spawn.x, 0.0, spawn.z))
|
||||
var radius := Game.insel_radius() - 4.0
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 91
|
||||
var gesetzt: Array[Vector3] = []
|
||||
var abstand := 9.0
|
||||
for versuch in range(1400):
|
||||
var a := rng.randf() * TAU
|
||||
var r := sqrt(rng.randf()) * radius
|
||||
var p := Vector3(cos(a) * r, 0.0, sin(a) * r)
|
||||
if p.length() < 20.0:
|
||||
continue
|
||||
var frei := true
|
||||
for g in gesetzt:
|
||||
if g.distance_to(p) < abstand:
|
||||
frei = false
|
||||
break
|
||||
if not frei:
|
||||
continue
|
||||
if not _freie_stelle(p, 3.2):
|
||||
continue
|
||||
gesetzt.append(p)
|
||||
_baum(p, rng)
|
||||
|
||||
|
||||
func _freie_stelle(p: Vector3, luft: float) -> bool:
|
||||
for frei in _freihalten:
|
||||
if p.distance_to(frei) < 7.0:
|
||||
return false
|
||||
for key in DreamDB.PLACES:
|
||||
var s: Array = DreamDB.PLACES[key]["size"]
|
||||
if float(s[0]) <= 0.0 or not Game.ort_offen(key):
|
||||
continue
|
||||
var c := Orte.center(DreamDB.PLACES, key)
|
||||
if absf(p.x - c.x) < float(s[0]) * 0.5 + luft \
|
||||
and absf(p.z - c.z) < float(s[1]) * 0.5 + luft:
|
||||
return false
|
||||
for k in DreamDB.NETZ.kanten:
|
||||
if Netz.trifft_rechteck(DreamDB.NETZ.knoten[k[0]], DreamDB.NETZ.knoten[k[1]],
|
||||
p, Vector2(luft * 2.0, luft * 2.0), 0.6):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Traumbäume: schlanker Stamm, zwei versetzte Kronen, leuchtende Spitze.
|
||||
func _baum(p: Vector3, rng: RandomNumberGenerator) -> void:
|
||||
var hoehe := rng.randf_range(3.0, 6.0)
|
||||
# Grün gegen den lila Grund -- das macht die Insel erst gemütlich.
|
||||
var ton := Color.from_hsv(0.26 + rng.randf() * 0.12, 0.48 + rng.randf() * 0.18,
|
||||
0.58 + rng.randf() * 0.26)
|
||||
Prims.cyl(self, p + Vector3(0, hoehe * 0.5, 0), 0.24, hoehe,
|
||||
Color(0.34, 0.26, 0.30))
|
||||
var k1 := Prims.sphere(self, p + Vector3(0, hoehe + 0.4, 0),
|
||||
rng.randf_range(1.1, 2.0), ton)
|
||||
k1.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
if rng.randf() < 0.6:
|
||||
var k2 := Prims.sphere(self, p + Vector3(rng.randf_range(-0.8, 0.8),
|
||||
hoehe - 0.7, rng.randf_range(-0.8, 0.8)), rng.randf_range(0.8, 1.5),
|
||||
ton.lightened(0.12))
|
||||
k2.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
if rng.randf() < 0.3:
|
||||
Prims.sphere(self, p + Vector3(0, hoehe + 1.6, 0), 0.28,
|
||||
Color(0.9, 0.95, 1.0), 1.6)
|
||||
|
||||
|
||||
func _wege() -> void:
|
||||
var farbe := Color(0.66, 0.58, 0.84)
|
||||
for k in DreamDB.NETZ.kanten:
|
||||
var a: Vector3 = DreamDB.NETZ.knoten[k[0]]
|
||||
var b: Vector3 = DreamDB.NETZ.knoten[k[1]]
|
||||
var d := b - a
|
||||
if d.length() < 0.2:
|
||||
continue
|
||||
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, 0.03, 0),
|
||||
Vector3(2.6, 0.06, d.length()), farbe, false)
|
||||
band.rotation.y = atan2(d.x, d.z)
|
||||
for k in DreamDB.NETZ.knoten:
|
||||
Prims.cyl(self, k + Vector3(0, 0.035, 0), 1.3, 0.06, farbe, false)
|
||||
|
||||
|
||||
func _haeuser() -> void:
|
||||
for key in DreamDB.PLACES:
|
||||
var pl: Dictionary = DreamDB.PLACES[key]
|
||||
@@ -194,8 +277,10 @@ func _schwarzes_brett() -> void:
|
||||
_marke(pos + Vector3(0, 1.6, 1.2), "brett", "Schwarzes Brett")
|
||||
|
||||
|
||||
## Unsichtbarer Punkt, an dem die Interaktion hängt.
|
||||
## Unsichtbarer Punkt, an dem die Interaktion hängt. Um jede Marke bleibt ein
|
||||
## Stück frei, damit später kein Baum davorsteht.
|
||||
func _marke(pos: Vector3, kind: String, titel: String) -> Node3D:
|
||||
_freihalten.append(Vector3(pos.x, 0.0, pos.z))
|
||||
var n := Node3D.new()
|
||||
n.position = pos
|
||||
add_child(n)
|
||||
@@ -243,7 +328,7 @@ func _wesen() -> void:
|
||||
continue
|
||||
var w := Bewohner.new()
|
||||
add_child(w)
|
||||
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES)
|
||||
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES, DreamDB.NETZ, DreamDB.ANKER)
|
||||
bewohner.append(w)
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ var _nachtlichter: Array = []
|
||||
func _ready() -> void:
|
||||
_environment()
|
||||
_boden()
|
||||
_bebauung()
|
||||
_orte()
|
||||
_park()
|
||||
_saeule()
|
||||
@@ -44,107 +45,142 @@ func _environment() -> void:
|
||||
|
||||
|
||||
func _boden() -> void:
|
||||
var weite := 240.0
|
||||
Prims.box(self, Vector3(0, -0.53, 0), Vector3(300, 1, 300), Color(0.42, 0.48, 0.40))
|
||||
_strassen()
|
||||
|
||||
|
||||
## Arterien breit, roji schmal -- beide als flaches Band mit Belag.
|
||||
func _strassen() -> void:
|
||||
for st in HumanDB.STRASSEN:
|
||||
var a: Vector3 = st["a"]
|
||||
var b: Vector3 = st["b"]
|
||||
var d := b - a
|
||||
if d.length() < 0.2:
|
||||
continue
|
||||
var breit := float(st["breite"])
|
||||
var farbe := Color(0.62, 0.60, 0.58) if breit > 5.0 else Color(0.55, 0.53, 0.52)
|
||||
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, 0.02, 0),
|
||||
Vector3(breit, 0.06, d.length() + breit), farbe, false)
|
||||
band.rotation.y = atan2(d.x, d.z)
|
||||
|
||||
|
||||
## Alles, was nicht Wohnung oder Sonderbau ist, steht als geschlossener Bau da.
|
||||
## Erst dadurch wird der Bezirk dicht.
|
||||
func _bebauung() -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 8123
|
||||
var belegt: Dictionary = {}
|
||||
for key in HumanDB.PLACES:
|
||||
var pl: Dictionary = HumanDB.PLACES[key]
|
||||
var p: Array = pl["pos"]
|
||||
var s2: Array = pl["size"]
|
||||
weite = maxf(weite, (absf(float(p[0])) + float(s2[0])) * 2.6)
|
||||
weite = maxf(weite, (absf(float(p[1])) + float(s2[1])) * 2.6)
|
||||
Prims.box(self, Vector3(0, -0.53, 0), Vector3(weite, 1, weite), Color(0.36, 0.52, 0.30))
|
||||
# Ringweg, auf dem die Menschen gehen
|
||||
for i in range(36):
|
||||
var a := TAU * float(i) / 36.0
|
||||
Prims.box(self, Vector3(cos(a) * 15.0, 0.03, sin(a) * 15.0),
|
||||
Vector3(2.8, 0.06, 2.8), Color(0.62, 0.58, 0.50), false)
|
||||
var s: Array = HumanDB.PLACES[key]["size"]
|
||||
if float(s[0]) > 0.0:
|
||||
belegt[Orte.center(HumanDB.PLACES, key).snapped(Vector3.ONE * 0.1)] = true
|
||||
for p in HumanDB.PARZELLEN:
|
||||
var c: Vector3 = p["pos"]
|
||||
if belegt.has(c.snapped(Vector3.ONE * 0.1)):
|
||||
continue
|
||||
var g: Vector2 = p["size"]
|
||||
if minf(g.x, g.y) < 3.0:
|
||||
continue
|
||||
var hoehe := rng.randf_range(4.0, 9.0)
|
||||
var ton := Color.from_hsv(fmod(0.07 + rng.randf() * 0.12, 1.0),
|
||||
0.10 + rng.randf() * 0.14, 0.62 + rng.randf() * 0.26)
|
||||
Prims.box(self, c + Vector3(0, hoehe * 0.5, 0), Vector3(g.x, hoehe, g.y), ton)
|
||||
Prims.box(self, c + Vector3(0, hoehe + 0.2, 0),
|
||||
Vector3(g.x + 0.5, 0.4, g.y + 0.5), ton.darkened(0.25))
|
||||
if rng.randf() < 0.35:
|
||||
Prims.box(self, c + Vector3(0, hoehe + 0.9, 0),
|
||||
Vector3(g.x * 0.4, 1.0, g.y * 0.4), ton.darkened(0.1))
|
||||
|
||||
|
||||
## Wohnungen und Sonderbauten stehen offen, damit der Geist von oben hineinsieht.
|
||||
func _orte() -> void:
|
||||
for key in HumanDB.PLACES:
|
||||
var pl: Dictionary = HumanDB.PLACES[key]
|
||||
if float(pl["size"][0]) <= 0.0:
|
||||
var s: Array = pl["size"]
|
||||
if float(s[0]) <= 0.0:
|
||||
continue
|
||||
var c := Orte.center(HumanDB.PLACES, key)
|
||||
var s: Array = pl["size"]
|
||||
var groesse := Vector3(s[0], 2.3, s[1])
|
||||
var groesse := Vector3(float(s[0]), 2.3, float(s[1]))
|
||||
var farbe: Color = pl["color"]
|
||||
var richtung: Vector3 = Orte.DIR_VEC[pl["dir"]]
|
||||
Prims.room(self, c, groesse, farbe, farbe.darkened(0.35), richtung, 3.4, true)
|
||||
Prims.label(self, c + Vector3(0, 5.0, 0), pl["name"], 0.6, Color(1, 1, 0.9))
|
||||
var tuer: float = minf(3.0, minf(groesse.x, groesse.z) - 3.0)
|
||||
Prims.room(self, c, groesse, farbe, farbe.darkened(0.35),
|
||||
Orte.DIR_VEC[pl["dir"]], maxf(1.6, tuer), true)
|
||||
Prims.label(self, c + Vector3(0, 4.4, 0), str(pl["name"]), 0.55, Color(1, 1, 0.9))
|
||||
_einrichtung(c, groesse, farbe, key)
|
||||
|
||||
var tuer := Orte.door(HumanDB.PLACES, key)
|
||||
var weg := tuer.normalized() * 15.0 - tuer
|
||||
var b := Prims.box(self, (tuer + tuer.normalized() * 15.0) * 0.5 + Vector3(0, 0.03, 0),
|
||||
Vector3(2.6, 0.06, weg.length()), Color(0.62, 0.58, 0.50), false)
|
||||
b.rotation.y = atan2(weg.x, weg.z)
|
||||
|
||||
var l := OmniLight3D.new()
|
||||
l.position = c + Vector3(0, 2.0, 0)
|
||||
l.omni_range = 20.0
|
||||
l.light_color = Color(1.0, 0.85, 0.6)
|
||||
l.light_energy = 0.0
|
||||
add_child(l)
|
||||
_nachtlichter.append(l)
|
||||
var licht := OmniLight3D.new()
|
||||
licht.position = c + Vector3(0, 2.0, 0)
|
||||
licht.omni_range = 16.0
|
||||
licht.light_color = Color(1.0, 0.85, 0.6)
|
||||
licht.light_energy = 0.0
|
||||
add_child(licht)
|
||||
_nachtlichter.append(licht)
|
||||
|
||||
|
||||
## Ein paar Möbel, damit man sieht, wofür ein Raum da ist.
|
||||
func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
|
||||
var h := groesse.z * 0.5 - 1.6
|
||||
var w := groesse.x * 0.5 - 1.6
|
||||
if key.begins_with("heim"):
|
||||
Prims.box(self, c + Vector3(-w + 1.0, 0.35, -h + 1.2), Vector3(2.0, 0.7, 3.4),
|
||||
Color(0.85, 0.85, 0.90)) # Bett
|
||||
Prims.box(self, c + Vector3(w - 1.0, 0.75, 0), Vector3(1.6, 0.15, 2.6),
|
||||
farbe.darkened(0.5)) # Schreibtisch
|
||||
Prims.box(self, c + Vector3(w - 1.0, 1.05, 0), Vector3(0.9, 0.6, 0.1),
|
||||
Color(0.2, 0.25, 0.3)) # Bildschirm
|
||||
var w := maxf(0.5, groesse.x * 0.5 - 2.0)
|
||||
var h := maxf(0.5, groesse.z * 0.5 - 2.0)
|
||||
if key.begins_with("heim_"):
|
||||
Prims.box(self, c + Vector3(-w * 0.6, 0.35, -h * 0.5), Vector3(1.9, 0.7, 3.0),
|
||||
Color(0.85, 0.85, 0.90))
|
||||
Prims.box(self, c + Vector3(w * 0.6, 0.75, 0), Vector3(1.5, 0.15, 2.2),
|
||||
farbe.darkened(0.5))
|
||||
Prims.box(self, c + Vector3(w * 0.6, 1.05, 0), Vector3(0.8, 0.55, 0.1),
|
||||
Color(0.2, 0.25, 0.3))
|
||||
elif key == "cafe":
|
||||
for i in range(3):
|
||||
Prims.cyl(self, c + Vector3(-3.0 + i * 3.0, 0.4, 0.5), 0.8, 0.8,
|
||||
Prims.cyl(self, c + Vector3(-w * 0.5 + float(i) * w * 0.5, 0.4, 0.4), 0.7, 0.8,
|
||||
Color(0.55, 0.40, 0.30))
|
||||
Prims.box(self, c + Vector3(0, 0.55, -3.2), Vector3(groesse.x - 3.0, 1.1, 0.8),
|
||||
Prims.box(self, c + Vector3(0, 0.55, -h * 0.7), Vector3(groesse.x - 3.0, 1.1, 0.8),
|
||||
Color(0.45, 0.32, 0.24))
|
||||
elif key == "buero":
|
||||
for i in range(4):
|
||||
Prims.box(self, c + Vector3(-4.5 + i * 3.0, 0.75, -1.0), Vector3(2.2, 0.15, 1.4),
|
||||
Color(0.75, 0.75, 0.78))
|
||||
for i in range(3):
|
||||
Prims.box(self, c + Vector3(-w * 0.5 + float(i) * w * 0.5, 0.75, 0),
|
||||
Vector3(1.6, 0.15, 1.2), Color(0.75, 0.75, 0.78))
|
||||
elif key == "baeckerei":
|
||||
Prims.box(self, c + Vector3(0, 0.55, 2.0), Vector3(groesse.x - 3.0, 1.1, 1.0),
|
||||
Prims.box(self, c + Vector3(0, 0.55, h * 0.5), Vector3(groesse.x - 3.0, 1.1, 1.0),
|
||||
Color(0.70, 0.55, 0.35))
|
||||
Prims.box(self, c + Vector3(0, 0.85, -3.0), Vector3(3.0, 1.7, 1.5), Color(0.4, 0.4, 0.42))
|
||||
Prims.box(self, c + Vector3(0, 0.85, -h * 0.6), Vector3(2.6, 1.7, 1.2),
|
||||
Color(0.4, 0.4, 0.42))
|
||||
elif key == "uni":
|
||||
for i in range(3):
|
||||
Prims.box(self, c + Vector3(0, 0.5, -2.0 + i * 2.2), Vector3(groesse.x - 4.0, 0.2, 0.8),
|
||||
Color(0.66, 0.62, 0.58))
|
||||
Prims.box(self, c + Vector3(0, 1.1, -4.0), Vector3(5.0, 1.6, 0.15), Color(0.15, 0.25, 0.20))
|
||||
Prims.box(self, c + Vector3(0, 0.5, -h * 0.6 + float(i) * h * 0.6),
|
||||
Vector3(groesse.x - 3.0, 0.2, 0.7), Color(0.66, 0.62, 0.58))
|
||||
Prims.box(self, c + Vector3(0, 1.1, -h), Vector3(groesse.x * 0.55, 1.6, 0.15),
|
||||
Color(0.15, 0.25, 0.20))
|
||||
elif key == "werkstatt":
|
||||
Prims.box(self, c + Vector3(0, 0.55, 0), Vector3(groesse.x - 4.0, 1.1, 1.4),
|
||||
Color(0.55, 0.50, 0.48))
|
||||
for i in range(3):
|
||||
Prims.box(self, c + Vector3(-2.0 + i * 2.0, 1.5, -h + 0.6), Vector3(0.5, 0.9, 0.15),
|
||||
Color(0.72, 0.68, 0.60), false)
|
||||
Prims.box(self, c + Vector3(0, 0.6, 0), Vector3(groesse.x - 4.0, 1.2, 1.4),
|
||||
Color(0.52, 0.48, 0.46))
|
||||
|
||||
|
||||
## Der mittlere Block bleibt frei: Rasen, Teich, Bäume, Bänke. Der Park ist
|
||||
## der einzige Ort im Bezirk, an dem nicht gebaut wird.
|
||||
func _park() -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 3
|
||||
Prims.cyl(self, Vector3(-6, 0.05, 2), 4.0, 0.12, Color(0.30, 0.50, 0.72), false)
|
||||
for i in range(14):
|
||||
var a := rng.randf() * TAU
|
||||
var r := rng.randf_range(5.0, 12.0)
|
||||
var p := Vector3(cos(a) * r, 0, sin(a) * r + 2.0)
|
||||
if p.length() < 4.0:
|
||||
var halb: float = (float(HumanDB.ACHSEN[2]) - float(HumanDB.ACHSEN[1])) * 0.5 \
|
||||
- HumanDB.ARTERIE * 0.5
|
||||
Prims.box(self, Vector3(0, 0.01, 0), Vector3(halb * 2.0, 0.05, halb * 2.0),
|
||||
Color(0.40, 0.58, 0.34), false)
|
||||
Prims.cyl(self, Vector3(-halb * 0.4, 0.06, halb * 0.25), halb * 0.28, 0.1,
|
||||
Color(0.30, 0.50, 0.72), false)
|
||||
for i in range(26):
|
||||
var p := Vector3(rng.randf_range(-halb, halb), 0, rng.randf_range(-halb, halb))
|
||||
if p.distance_to(Vector3(-halb * 0.4, 0, halb * 0.25)) < halb * 0.38:
|
||||
continue
|
||||
var hoehe := rng.randf_range(2.4, 4.2)
|
||||
if p.distance_to(Vector3(0, 0, 12.0)) < 6.0:
|
||||
continue
|
||||
var hoehe := rng.randf_range(2.6, 5.0)
|
||||
Prims.cyl(self, p + Vector3(0, hoehe * 0.5, 0), 0.3, hoehe, Color(0.35, 0.26, 0.18))
|
||||
Prims.sphere(self, p + Vector3(0, hoehe + 0.8, 0), rng.randf_range(1.2, 1.9),
|
||||
Color(0.25, 0.5 + rng.randf() * 0.2, 0.22))
|
||||
for i in range(4):
|
||||
var a := TAU * float(i) / 4.0 + 0.4
|
||||
var p := Vector3(cos(a) * 8.0, 0.45, sin(a) * 8.0 + 2.0)
|
||||
Prims.sphere(self, p + Vector3(0, hoehe + 0.9, 0), rng.randf_range(1.3, 2.2),
|
||||
Color(0.24, 0.48 + rng.randf() * 0.22, 0.22))
|
||||
for i in range(5):
|
||||
var a := TAU * float(i) / 5.0 + 0.4
|
||||
var p := Vector3(cos(a) * halb * 0.62, 0.45, sin(a) * halb * 0.62)
|
||||
var b := Prims.box(self, p, Vector3(2.4, 0.15, 0.7), Color(0.55, 0.40, 0.28))
|
||||
b.rotation.y = -a
|
||||
Prims.label(self, Vector3(0, 5.0, 10.0), "Stadtpark", 0.55, Color(1, 1, 0.9))
|
||||
Prims.label(self, Vector3(0, 5.0, halb - 3.0), "Stadtpark", 0.6, Color(1, 1, 0.9))
|
||||
|
||||
|
||||
func _saeule() -> void:
|
||||
@@ -167,7 +203,7 @@ func _menschen() -> void:
|
||||
for i in range(HumanDB.LIST.size()):
|
||||
var m := Bewohner.new()
|
||||
add_child(m)
|
||||
m.setup(HumanDB.LIST[i], i, HumanDB.PLACES)
|
||||
m.setup(HumanDB.LIST[i], i, HumanDB.PLACES, HumanDB.NETZ, HumanDB.ANKER)
|
||||
humans.append(m)
|
||||
|
||||
|
||||
|
||||
187
scripts/game.gd
187
scripts/game.gd
@@ -9,22 +9,40 @@ const DAY_SECONDS := 360.0 # eine Erdenrunde in Echtzeit-Sekunden
|
||||
const XP_LEVELS := [0, 4, 10, 18, 28]
|
||||
const MISSION_ZIEL := 100.0
|
||||
const RESIST_SEKUNDEN := 18.0
|
||||
## Drei Stufen wie in Die Sims: normal, schnell, sehr schnell. Die Stufe ist
|
||||
## nicht der Faktor — Stufe 2 läuft dreifach, Stufe 3 siebenfach.
|
||||
const TEMPO_FAKTOR := [1.0, 3.0, 7.0]
|
||||
const TECHNIK_COOLDOWN := 30.0 # eine Technik braucht Luft, bevor sie wieder greift
|
||||
const FEHLVERSUCH_VERLUST := 14.0 # ein Fehlschlag wirft den Traum zurück
|
||||
# Der Verlust ist mit Absicht so hoch, dass sich nur eine Technik lohnt, die
|
||||
# Thema UND Moment trifft. Wer eine Regel blind auf jede Tätigkeit hängt,
|
||||
# verliert unterm Strich — die Wahl muss aus der Technik gelesen werden.
|
||||
|
||||
var einsicht: int = 3
|
||||
var known: Dictionary = {} # tech_id -> xp
|
||||
var active_mission: String = "" # human_id oder ""
|
||||
var progress: Dictionary = {} # human_id -> 0..100
|
||||
var used: Dictionary = {} # "human:tech" -> Anzahl
|
||||
var cooldown: Dictionary = {} # tech_id -> Restsekunden
|
||||
var pipeline: Dictionary = {} # human_id -> {activity -> tech_id}
|
||||
var completed: Array = []
|
||||
var time_h: float = 7.0
|
||||
var day: int = 1
|
||||
var teleport_lock: float = 0.0
|
||||
var tempo: int = 1 # 1, 2 oder 3 -- vom Spieler gewählt
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_input()
|
||||
welt_erzeugen()
|
||||
_startweg_lernen()
|
||||
|
||||
|
||||
## Den ersten Weg bringt das Wesen schon mit. Ohne ihn hinge am Schwarzen Brett
|
||||
## nichts aus, und der erste Gang wäre eine Pflichtübung.
|
||||
func _startweg_lernen() -> void:
|
||||
for id in DreamDB.techniken_von("hoersaal"):
|
||||
if not known.has(str(id)):
|
||||
known[str(id)] = 0
|
||||
|
||||
|
||||
func _setup_input() -> void:
|
||||
@@ -37,6 +55,9 @@ func _setup_input() -> void:
|
||||
"sinken": [KEY_SHIFT],
|
||||
"interagieren": [KEY_E],
|
||||
"journal": [KEY_J, KEY_TAB],
|
||||
"tempo_1": [KEY_1, KEY_KP_1],
|
||||
"tempo_2": [KEY_2, KEY_KP_2],
|
||||
"tempo_3": [KEY_3, KEY_KP_3],
|
||||
}
|
||||
for action in binds:
|
||||
if not InputMap.has_action(action):
|
||||
@@ -47,7 +68,47 @@ func _setup_input() -> void:
|
||||
InputMap.action_add_event(action, ev)
|
||||
|
||||
|
||||
## Schläft der Mensch, dem du gerade hilfst? Dann ist ohnehin nichts zu holen.
|
||||
func schlaeft_ziel() -> bool:
|
||||
if active_mission == "":
|
||||
return false
|
||||
var h := HumanDB.by_id(active_mission)
|
||||
if h.is_empty():
|
||||
return false
|
||||
var r: Array = h["routine"]
|
||||
var cur: Array = r[r.size() - 1]
|
||||
for e in r:
|
||||
if time_h >= float(e[0]):
|
||||
cur = e
|
||||
return not erreichbar(str(cur[2]))
|
||||
|
||||
|
||||
## Um wie viel schneller läuft die Welt gerade? Über der Nacht des Menschen
|
||||
## läuft sie immer auf der höchsten Stufe -- warten bringt dort nichts.
|
||||
func zeit_faktor() -> float:
|
||||
return faktor_von(3 if schlaeft_ziel() else tempo)
|
||||
|
||||
|
||||
static func faktor_von(stufe: int) -> float:
|
||||
return float(TEMPO_FAKTOR[clampi(stufe, 1, 3) - 1])
|
||||
|
||||
|
||||
func tempo_setzen(stufe: int) -> void:
|
||||
var neu := clampi(stufe, 1, 3)
|
||||
if neu == tempo:
|
||||
return
|
||||
tempo = neu
|
||||
notified.emit("Tempo %d — %d×" % [tempo, int(faktor_von(tempo))], Color(0.8, 0.9, 1))
|
||||
state_changed.emit()
|
||||
|
||||
|
||||
func advance_time(delta: float) -> void:
|
||||
for id in cooldown.keys():
|
||||
var rest: float = float(cooldown[id]) - delta
|
||||
if rest <= 0.0:
|
||||
cooldown.erase(id)
|
||||
else:
|
||||
cooldown[id] = rest
|
||||
time_h += delta * 24.0 / DAY_SECONDS
|
||||
while time_h >= 24.0:
|
||||
time_h -= 24.0
|
||||
@@ -75,7 +136,6 @@ func time_string() -> String:
|
||||
# lehrt eine Technik, die es vorher nirgends gab.
|
||||
|
||||
const INSEL_GRUND := 46.0
|
||||
const OFFENE_MISSIONEN := 3
|
||||
|
||||
|
||||
func haeuser() -> int:
|
||||
@@ -83,14 +143,13 @@ func haeuser() -> int:
|
||||
|
||||
|
||||
func insel_radius() -> float:
|
||||
var ring := DreamDB.aeusserster_ring(haeuser())
|
||||
return maxf(INSEL_GRUND, ring + 14.0)
|
||||
return maxf(INSEL_GRUND, DreamDB.aussenradius() + 12.0)
|
||||
|
||||
|
||||
## Baut Traumdorf und Menschheit auf den Stand der erfüllten Träume.
|
||||
func welt_erzeugen() -> void:
|
||||
DreamDB.erzeuge(haeuser())
|
||||
HumanDB.erzeuge(completed.size() + OFFENE_MISSIONEN)
|
||||
HumanDB.erzeuge()
|
||||
|
||||
|
||||
## Das Traumdorf beginnt leer. Mit jedem erfüllten Traum zieht ein Wesen ein.
|
||||
@@ -117,7 +176,21 @@ func offene_techniken() -> int:
|
||||
return n
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- Cooldown
|
||||
# Jede Anwendung sperrt ihre Technik für eine Weile. Wer einen ganzen Tag
|
||||
# abdecken will, braucht deshalb mehrere Wege — einer allein kommt nicht nach.
|
||||
|
||||
func cooldown_rest(tech_id: String) -> float:
|
||||
return float(cooldown.get(tech_id, 0.0))
|
||||
|
||||
|
||||
func bereit(tech_id: String) -> bool:
|
||||
return cooldown_rest(tech_id) <= 0.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Fertigkeiten
|
||||
# Gelernt wird an einem Lehrort, besser wird eine Technik nur durch Anwendung.
|
||||
# Es gibt keinen Weg, sie am Schreibtisch zu trainieren.
|
||||
|
||||
func knows(tech_id: String) -> bool:
|
||||
return known.has(tech_id)
|
||||
@@ -149,10 +222,6 @@ func learn(tech_id: String) -> bool:
|
||||
notified.emit("Diesen Weg kennt im Dorf noch niemand. Erfülle einen Traum.",
|
||||
Color(1, 0.8, 0.5))
|
||||
return false
|
||||
if einsicht < 1:
|
||||
notified.emit("Dafür fehlt dir Einsicht. Hilf erst einem Menschen.", Color(1, 0.6, 0.4))
|
||||
return false
|
||||
einsicht -= 1
|
||||
known[tech_id] = 0
|
||||
var t := TechDB.by_id(tech_id)
|
||||
notified.emit("Technik gelernt: %s" % t["name"], Color(0.6, 1, 0.7))
|
||||
@@ -171,18 +240,6 @@ func add_xp(tech_id: String, amount: int) -> void:
|
||||
state_changed.emit()
|
||||
|
||||
|
||||
func deepen(tech_id: String) -> bool:
|
||||
if not known.has(tech_id):
|
||||
return false
|
||||
if einsicht < 1:
|
||||
notified.emit("Keine Einsicht übrig.", Color(1, 0.6, 0.4))
|
||||
return false
|
||||
einsicht -= 1
|
||||
add_xp(tech_id, 3)
|
||||
notified.emit("Vertieft: %s (+3 EP)" % TechDB.by_id(tech_id)["name"], Color(0.7, 0.9, 1))
|
||||
return true
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Missionen
|
||||
|
||||
func accept_mission(human_id: String) -> bool:
|
||||
@@ -204,6 +261,40 @@ func angenommen(human_id: String) -> bool:
|
||||
return progress.has(human_id)
|
||||
|
||||
|
||||
## Eine Mission ist machbar, wenn mindestens eine gelernte Technik das Thema
|
||||
## des Menschen trifft und in seinem Tag auch einen Moment dafür findet.
|
||||
## Nur solche werden am Schwarzen Brett überhaupt ausgehängt.
|
||||
func machbar(human_id: String) -> bool:
|
||||
var h := HumanDB.by_id(human_id)
|
||||
if h.is_empty():
|
||||
return false
|
||||
var tag_liste: Array = HumanDB.aktivitaeten(human_id)
|
||||
for id in known:
|
||||
var t := TechDB.by_id(id)
|
||||
var thema := false
|
||||
for tag in t["tags"]:
|
||||
if (h["tags"] as Array).has(tag):
|
||||
thema = true
|
||||
break
|
||||
if not thema:
|
||||
continue
|
||||
for a in t["momente"]:
|
||||
if tag_liste.has(a) and erreichbar(a):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## Die Menschen, die gerade am Brett hängen können.
|
||||
func offene_missionen() -> Array:
|
||||
var out: Array = []
|
||||
for h in HumanDB.LIST:
|
||||
var id: String = h["id"]
|
||||
if completed.has(id) or not machbar(id):
|
||||
continue
|
||||
out.append(id)
|
||||
return out
|
||||
|
||||
|
||||
func mission_progress(human_id: String) -> float:
|
||||
return progress.get(human_id, 0.0)
|
||||
|
||||
@@ -242,17 +333,17 @@ func regel_anzahl(human_id: String) -> int:
|
||||
return regeln(human_id).size()
|
||||
|
||||
|
||||
## Wo hängt diese Technik im Regelwerk gerade? "" wenn nirgends.
|
||||
func regel_moment(human_id: String, tech_id: String) -> String:
|
||||
## An welchen Tätigkeiten hängt diese Technik gerade?
|
||||
func regel_momente(human_id: String, tech_id: String) -> Array:
|
||||
var out: Array = []
|
||||
for activity in regeln(human_id):
|
||||
if regeln(human_id)[activity] == tech_id:
|
||||
return activity
|
||||
return ""
|
||||
out.append(activity)
|
||||
return out
|
||||
|
||||
|
||||
## Leere tech_id löscht die Regel. Eine Technik hält immer nur einen Posten —
|
||||
## sie an einen neuen Moment zu hängen nimmt sie vom alten weg. Das zwingt zur
|
||||
## Wahl und macht das Lernen weiterer Techniken erst wertvoll.
|
||||
## Leere tech_id löscht die Regel. Eine Technik darf an beliebig vielen
|
||||
## Tätigkeiten hängen — ob das klug ist, muss der Spieler aus ihr herauslesen.
|
||||
func regel_setzen(human_id: String, activity: String, tech_id: String) -> void:
|
||||
if not pipeline.has(human_id):
|
||||
pipeline[human_id] = {}
|
||||
@@ -260,23 +351,20 @@ func regel_setzen(human_id: String, activity: String, tech_id: String) -> void:
|
||||
if tech_id == "":
|
||||
r.erase(activity)
|
||||
else:
|
||||
var vorher := regel_moment(human_id, tech_id)
|
||||
if vorher != "" and vorher != activity:
|
||||
r.erase(vorher)
|
||||
notified.emit("%s hängt jetzt an »%s« statt an »%s«." %
|
||||
[TechDB.by_id(tech_id)["name"], Orte.text(activity), Orte.text(vorher)],
|
||||
Color(0.85, 0.9, 1))
|
||||
r[activity] = tech_id
|
||||
state_changed.emit()
|
||||
|
||||
|
||||
## Der Mensch wechselt in eine Tätigkeit: greift dafür eine Regel?
|
||||
## Greift für diese Tätigkeit eine Regel? Wird nicht nur beim Wechsel gefragt,
|
||||
## sondern immer wieder — solange der Moment anhält und die Technik frei ist.
|
||||
func regel_ausloesen(human_id: String, activity: String, resisting: bool) -> Dictionary:
|
||||
if human_id != active_mission:
|
||||
return {}
|
||||
var tech_id := regel_fuer(human_id, activity)
|
||||
if tech_id == "" or not known.has(tech_id):
|
||||
return {}
|
||||
if not bereit(tech_id):
|
||||
return {} # still: die Technik greift wieder, sobald ihre Sperre fällt
|
||||
var res := apply_technique(tech_id, human_id, activity, resisting)
|
||||
if not res["ok"]:
|
||||
return {}
|
||||
@@ -287,8 +375,8 @@ func regel_ausloesen(human_id: String, activity: String, resisting: bool) -> Dic
|
||||
[t["name"], Orte.text(activity), int(round(float(res["gain"])))],
|
||||
Color(0.6, 1, 0.7))
|
||||
else:
|
||||
notified.emit("Regel läuft ins Leere — %s bei »%s« (%d %% Chance)" %
|
||||
[t["name"], Orte.text(activity), int(round(float(res["chance"]) * 100.0))],
|
||||
notified.emit("Regel läuft ins Leere — %s bei »%s« (−%d %%)" %
|
||||
[t["name"], Orte.text(activity), int(round(float(res["verlust"])))],
|
||||
Color(1, 0.7, 0.5))
|
||||
return res
|
||||
|
||||
@@ -324,11 +412,13 @@ func evaluate(tech_id: String, human_id: String, activity: String, resisting: bo
|
||||
break
|
||||
res["moment"] = t["momente"].has(activity)
|
||||
|
||||
var c := 0.08 + 0.11 * float(skill)
|
||||
# Der Moment wiegt am schwersten: eine Technik zur falschen Zeit greift kaum,
|
||||
# egal wie gut sie zum Menschen passt. Genau das steht in ihrem "Wann".
|
||||
var c := 0.05 + 0.09 * float(skill)
|
||||
if res["tag"]:
|
||||
c += 0.25
|
||||
c += 0.20
|
||||
if res["moment"]:
|
||||
c += 0.22
|
||||
c += 0.35
|
||||
if resisting:
|
||||
c -= 0.30
|
||||
res["grund"] = "Der Mensch macht gerade dicht."
|
||||
@@ -338,9 +428,13 @@ func evaluate(tech_id: String, human_id: String, activity: String, resisting: bo
|
||||
|
||||
## Wendet die Technik wirklich an: würfelt, verteilt EP und Fortschritt.
|
||||
func apply_technique(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary:
|
||||
if not bereit(tech_id):
|
||||
return {"ok": false, "grund": "%s ist noch nicht wieder einsatzbereit." %
|
||||
TechDB.by_id(tech_id)["name"]}
|
||||
var ev := evaluate(tech_id, human_id, activity, resisting)
|
||||
if ev["blocked"]:
|
||||
return {"ok": false, "grund": ev["grund"]}
|
||||
cooldown[tech_id] = TECHNIK_COOLDOWN
|
||||
|
||||
var roll := randf()
|
||||
var success: bool = roll < float(ev["chance"])
|
||||
@@ -349,6 +443,7 @@ func apply_technique(tech_id: String, human_id: String, activity: String, resist
|
||||
used[key] = wiederholung + 1
|
||||
|
||||
var gain := 0.0
|
||||
var verlust := 0.0
|
||||
if success:
|
||||
var base := 8.0 + 2.0 * float(ev["skill"])
|
||||
if ev["tag"]:
|
||||
@@ -358,25 +453,25 @@ func apply_technique(tech_id: String, human_id: String, activity: String, resist
|
||||
gain = base / (1.0 + 0.6 * float(wiederholung))
|
||||
add_xp(tech_id, 2)
|
||||
else:
|
||||
verlust = FEHLVERSUCH_VERLUST
|
||||
add_xp(tech_id, 1)
|
||||
|
||||
var done := false
|
||||
if human_id == active_mission and gain > 0.0:
|
||||
var p: float = progress.get(human_id, 0.0) + gain
|
||||
progress[human_id] = minf(p, MISSION_ZIEL)
|
||||
if human_id == active_mission:
|
||||
var p: float = progress.get(human_id, 0.0) + gain - verlust
|
||||
progress[human_id] = clampf(p, 0.0, MISSION_ZIEL)
|
||||
if progress[human_id] >= MISSION_ZIEL and not completed.has(human_id):
|
||||
completed.append(human_id)
|
||||
active_mission = ""
|
||||
einsicht += 3
|
||||
done = true
|
||||
mission_done.emit(human_id)
|
||||
notified.emit("%s hat es geschafft. (+3 Einsicht)" % HumanDB.by_id(human_id)["name"],
|
||||
notified.emit("%s hat es geschafft." % HumanDB.by_id(human_id)["name"],
|
||||
Color(1, 0.85, 0.4))
|
||||
_insel_waechst()
|
||||
state_changed.emit()
|
||||
|
||||
return {
|
||||
"ok": true, "success": success, "chance": ev["chance"], "gain": gain,
|
||||
"wiederholung": wiederholung, "mission_done": done,
|
||||
"verlust": verlust, "wiederholung": wiederholung, "mission_done": done,
|
||||
"tag": ev["tag"], "moment": ev["moment"],
|
||||
}
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
class_name HumanDB
|
||||
## Die Kleinstadt auf der Erde. Die Gebäude im Ring um den Park stehen fest;
|
||||
## Wohnhäuser und ihre Bewohner kommen dazu, solange Menschen Hilfe brauchen.
|
||||
## Die Erde: ein Stadtbezirk nach japanischem Vorbild. Weite Arterien in einem
|
||||
## groben Raster spannen Blöcke auf. Am Blockrand stehen die Häuser direkt an
|
||||
## der Straße, ohne Vorgarten. Ins Blockinnere führen schmale Sackgassen --
|
||||
## roji --, an denen die hinteren Parzellen hängen. So entsteht die typische
|
||||
## Mischung aus klarer Ordnung außen und Gewinkel innen.
|
||||
##
|
||||
## Anders als das Traumdorf wächst hier nichts: alle Menschen sind von Anfang
|
||||
## an da und leben ihren Tag.
|
||||
|
||||
const HAEUSER_JE_RING := 8
|
||||
const RING_ERSTER := 62.0
|
||||
const RING_ABSTAND := 16.0
|
||||
const EINWOHNER := 16
|
||||
const ACHSEN := [-72.0, -24.0, 24.0, 72.0] # Arterien in x wie in z
|
||||
const ARTERIE := 7.0 # Breite einer Arterie
|
||||
const ROJI := 3.2 # Breite einer Gasse
|
||||
const RAND_TIEFE := 11.0 # Tiefe der Parzellen an der Straße
|
||||
const PARZELLE_MIN := 8.0 # schmalste Parzelle
|
||||
const PARZELLE_MAX := 12.0
|
||||
|
||||
## Gebäude liegen im Ring um den Park. Alle Türen zeigen zur Mitte.
|
||||
static var FEST: Dictionary = {
|
||||
"uni": {
|
||||
"name": "Hochschule", "pos": [-28.0, -27.0], "size": [14.0, 10.0],
|
||||
"dir": "back", "color": Color(0.72, 0.70, 0.66),
|
||||
},
|
||||
"cafe": {
|
||||
"name": "Café Nadel", "pos": [0.0, -27.0], "size": [12.0, 10.0],
|
||||
"dir": "back", "color": Color(0.82, 0.62, 0.45),
|
||||
},
|
||||
"buero": {
|
||||
"name": "Büro Ostwind", "pos": [28.0, -27.0], "size": [14.0, 10.0],
|
||||
"dir": "back", "color": Color(0.62, 0.66, 0.72),
|
||||
},
|
||||
"baeckerei": {
|
||||
"name": "Bäckerei Keller", "pos": [-31.0, 0.0], "size": [10.0, 12.0],
|
||||
"dir": "right", "color": Color(0.85, 0.72, 0.50),
|
||||
},
|
||||
"werkstatt": {
|
||||
"name": "Werkstatt Loos", "pos": [31.0, 0.0], "size": [10.0, 12.0],
|
||||
"dir": "left", "color": Color(0.70, 0.58, 0.55),
|
||||
},
|
||||
"park": {
|
||||
"name": "Stadtpark", "pos": [0.0, 4.0], "size": [0.0, 0.0],
|
||||
"dir": "none", "color": Color(0.45, 0.65, 0.38),
|
||||
},
|
||||
}
|
||||
## Alle Parzellen des Bezirks: {pos, size, dir, strasse}
|
||||
static var PARZELLEN: Array = []
|
||||
## Alle Straßenbänder: {a, b, breite}
|
||||
static var STRASSEN: Array = []
|
||||
static var NETZ: Netz = Netz.new()
|
||||
static var ANKER: Dictionary = {}
|
||||
|
||||
static var PLACES: Dictionary = FEST.duplicate(true)
|
||||
## Die fünf Sonderbauten. Sie bekommen eine Parzelle an einer Arterie.
|
||||
static var SONDER: Array = [
|
||||
["uni", "Hochschule", Color(0.72, 0.70, 0.66)],
|
||||
["cafe", "Café Nadel", Color(0.82, 0.62, 0.45)],
|
||||
["buero", "Büro Ostwind", Color(0.62, 0.66, 0.72)],
|
||||
["baeckerei", "Bäckerei Keller", Color(0.85, 0.72, 0.50)],
|
||||
["werkstatt", "Werkstatt Loos", Color(0.70, 0.58, 0.55)],
|
||||
]
|
||||
|
||||
static var PLACES: Dictionary = {}
|
||||
static var LIST: Array = []
|
||||
|
||||
## Arbeitsplätze, die ein erzeugter Mensch bekommen kann.
|
||||
@@ -125,46 +123,299 @@ static var ABLAEUFE: Array = [
|
||||
]
|
||||
|
||||
|
||||
## Zu jeder Technik ein Anliegen und ein Tagesablauf, bei dem sie greift:
|
||||
## [Anliegen, Ablauf]. Mensch i bekommt den Eintrag zu Technik i — und Technik i
|
||||
## ist genau dann entdeckt, wenn i Träume erfüllt sind. So hängt am Schwarzen
|
||||
## Brett immer mindestens ein Mensch, dem mit dem Gelernten zu helfen ist.
|
||||
static var PASSEND: Array = [
|
||||
[0, 0], # Pomodoro -> aufschieben, zögert
|
||||
[6, 4], # Zwei-Minuten -> Werkstatt, räumt auf
|
||||
[4, 3], # Wenn-Dann -> Gewohnheit, Morgenroutine
|
||||
[1, 1], # Gewohnheiten -> Klavier, isst / Abendroutine
|
||||
[9, 0], # Zerlegen -> Überforderung, grübelt
|
||||
[0, 4], # Umgebung -> Ablenkung, räumt auf
|
||||
[8, 0], # Umdeuten -> Selbstzweifel, grübelt
|
||||
[7, 2], # Box-Atmung -> Angst, angespannt
|
||||
[8, 2], # Selbstmitgefühl -> Rückschlag, grübelt
|
||||
[4, 1], # Schlaffenster -> Erschöpfung, ruht
|
||||
[5, 2], # Hilfe bitten -> Einsamkeit, wartet
|
||||
[3, 3], # Nein sagen -> Grenzen, arbeitet
|
||||
]
|
||||
|
||||
|
||||
static func heim_key(i: int) -> String:
|
||||
return "heim_%d" % i
|
||||
|
||||
|
||||
## Baut Wohnhäuser und Menschen für so viele Bewohner neu auf.
|
||||
static func erzeuge(menschen: int) -> void:
|
||||
PLACES = FEST.duplicate(true)
|
||||
## Baut den ganzen Bezirk neu auf: Straßen, Parzellen, Sonderbauten, Wohnhäuser.
|
||||
## Die Zahl der Menschen liegt fest -- sie sind alle von Anfang an da.
|
||||
static func erzeuge(_ungenutzt: int = 0) -> void:
|
||||
_bezirk()
|
||||
PLACES = {}
|
||||
LIST = []
|
||||
for i in range(maxi(menschen, 0)):
|
||||
PLACES[heim_key(i)] = _heim(i)
|
||||
_park_eintragen()
|
||||
_sonderbauten_verteilen()
|
||||
_wohnhaeuser_verteilen()
|
||||
_anker_setzen()
|
||||
for i in range(EINWOHNER):
|
||||
LIST.append(_mensch(i))
|
||||
|
||||
|
||||
static func _heim(i: int) -> Dictionary:
|
||||
var ring := i / HAEUSER_JE_RING
|
||||
var winkel := float(i % HAEUSER_JE_RING) * TAU / float(HAEUSER_JE_RING) + 0.39
|
||||
var r := RING_ERSTER + float(ring) * RING_ABSTAND
|
||||
var x := sin(winkel) * r
|
||||
var z := cos(winkel) * r
|
||||
return {
|
||||
"name": "%s %s" % [_nachname(i), "Weg %d" % (i + 1)],
|
||||
"pos": [x, z],
|
||||
"size": [12.0 + float(i % 3) * 2.0, 10.0],
|
||||
"dir": _tuer_zur_mitte(x, z),
|
||||
"color": Color.from_hsv(fmod(0.08 + float(i) * 0.113, 1.0), 0.28, 0.78),
|
||||
# ------------------------------------------------------------ Bezirk
|
||||
|
||||
## Arterien im Raster, Blöcke dazwischen, roji ins Blockinnere, Parzellen
|
||||
## an jeder Straße. Danach steht das Straßennetz, auf dem alle laufen.
|
||||
static func _bezirk() -> void:
|
||||
PARZELLEN = []
|
||||
STRASSEN = []
|
||||
NETZ = Netz.new()
|
||||
ANKER = {}
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 31337
|
||||
|
||||
# Arterien: die Achsen leicht versetzt, damit kein reines Schachbrett steht
|
||||
var xs: Array = []
|
||||
var zs: Array = []
|
||||
for i in range(ACHSEN.size()):
|
||||
xs.append(float(ACHSEN[i]) + rng.randf_range(-3.0, 3.0))
|
||||
zs.append(float(ACHSEN[i]) + rng.randf_range(-3.0, 3.0))
|
||||
var von: float = float(ACHSEN[0]) - 6.0
|
||||
var bis: float = float(ACHSEN[ACHSEN.size() - 1]) + 6.0
|
||||
for x in xs:
|
||||
_strasse(Vector3(x, 0, von), Vector3(x, 0, bis), ARTERIE)
|
||||
for z in zs:
|
||||
_strasse(Vector3(von, 0, z), Vector3(bis, 0, z), ARTERIE)
|
||||
|
||||
# Blöcke zwischen den Arterien
|
||||
for ix in range(xs.size() - 1):
|
||||
for iz in range(zs.size() - 1):
|
||||
var x0: float = float(xs[ix]) + ARTERIE * 0.5
|
||||
var x1: float = float(xs[ix + 1]) - ARTERIE * 0.5
|
||||
var z0: float = float(zs[iz]) + ARTERIE * 0.5
|
||||
var z1: float = float(zs[iz + 1]) - ARTERIE * 0.5
|
||||
if ix == 1 and iz == 1:
|
||||
continue # die Mitte bleibt Park
|
||||
_block(Vector3((x0 + x1) * 0.5, 0, (z0 + z1) * 0.5),
|
||||
Vector2(x1 - x0, z1 - z0), rng)
|
||||
_netz_bauen()
|
||||
|
||||
|
||||
static func _strasse(a: Vector3, b: Vector3, breite: float) -> void:
|
||||
STRASSEN.append({"a": a, "b": b, "breite": breite})
|
||||
|
||||
|
||||
## Aus den Straßenbändern ein zusammenhängendes Netz machen: an jeder Kreuzung
|
||||
## sitzt ein Knoten, dazwischen alle 16 Meter einer.
|
||||
static func _netz_bauen() -> void:
|
||||
for i in range(STRASSEN.size()):
|
||||
var a: Vector3 = STRASSEN[i]["a"]
|
||||
var b: Vector3 = STRASSEN[i]["b"]
|
||||
var laenge := a.distance_to(b)
|
||||
if laenge < 0.5:
|
||||
continue
|
||||
var stellen: Array = [0.0, 1.0]
|
||||
for j in range(STRASSEN.size()):
|
||||
if j == i:
|
||||
continue
|
||||
var t := _kreuzung(a, b, STRASSEN[j]["a"], STRASSEN[j]["b"])
|
||||
if t >= 0.0:
|
||||
stellen.append(t)
|
||||
stellen.sort()
|
||||
var fein: Array = [float(stellen[0])]
|
||||
for k in range(1, stellen.size()):
|
||||
var von: float = float(fein[fein.size() - 1])
|
||||
var bis: float = float(stellen[k])
|
||||
if bis - von < 0.001:
|
||||
continue
|
||||
var n := maxi(1, int((bis - von) * laenge / 16.0))
|
||||
for m in range(1, n + 1):
|
||||
fein.append(von + (bis - von) * float(m) / float(n))
|
||||
var vorher := -1
|
||||
for t2 in fein:
|
||||
var idx := NETZ.fuege_knoten(a.lerp(b, float(t2)))
|
||||
if vorher >= 0:
|
||||
NETZ.verbinde(vorher, idx)
|
||||
vorher = idx
|
||||
|
||||
|
||||
## Parameter t auf der Strecke a-b, an dem sie c-d schneidet. -1 wenn nicht.
|
||||
static func _kreuzung(a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> float:
|
||||
var r := b - a
|
||||
var s2 := d - c
|
||||
var nenner := r.x * s2.z - r.z * s2.x
|
||||
if absf(nenner) < 0.0001:
|
||||
return -1.0
|
||||
var t := ((c.x - a.x) * s2.z - (c.z - a.z) * s2.x) / nenner
|
||||
var u := ((c.x - a.x) * r.z - (c.z - a.z) * r.x) / nenner
|
||||
if t < 0.0 or t > 1.0 or u < 0.0 or u > 1.0:
|
||||
return -1.0
|
||||
return t
|
||||
|
||||
|
||||
## Ein Block: außen ein Kranz von Parzellen an den Arterien, innen eine Gasse
|
||||
## mit den hinteren Parzellen daran. Die Nord- und Südreihe laufen über die
|
||||
## volle Breite, die Ost- und Westreihe nur dazwischen -- sonst überlappen die
|
||||
## Ecken.
|
||||
static func _block(mitte: Vector3, groesse: Vector2, rng: RandomNumberGenerator) -> void:
|
||||
var tiefe := minf(RAND_TIEFE, minf(groesse.x, groesse.y) * 0.34)
|
||||
var seiten := [
|
||||
["back", Vector3(0, 0, 1), groesse.x, groesse.y],
|
||||
["forward", Vector3(0, 0, -1), groesse.x, groesse.y],
|
||||
["right", Vector3(1, 0, 0), groesse.y - tiefe * 2.0, groesse.x],
|
||||
["left", Vector3(-1, 0, 0), groesse.y - tiefe * 2.0, groesse.x],
|
||||
]
|
||||
for eintrag in seiten:
|
||||
var dir: String = eintrag[0]
|
||||
var n: Vector3 = eintrag[1]
|
||||
var laenge: float = eintrag[2]
|
||||
var quer_mass: float = eintrag[3]
|
||||
if laenge < PARZELLE_MIN:
|
||||
continue
|
||||
var quer := Vector3(absf(n.z), 0, absf(n.x))
|
||||
var anzahl := maxi(1, int(round(laenge / rng.randf_range(PARZELLE_MIN + 1.0,
|
||||
PARZELLE_MAX))))
|
||||
var breite := laenge / float(anzahl)
|
||||
var rand := mitte + n * (quer_mass * 0.5 - tiefe * 0.5)
|
||||
for k in range(anzahl):
|
||||
var versatz := (float(k) + 0.5 - float(anzahl) * 0.5) * breite
|
||||
var quer_ist := absf(n.z) > 0.0
|
||||
PARZELLEN.append({
|
||||
"pos": rand + quer * versatz,
|
||||
"size": Vector2(breite - 0.8, tiefe - 0.8) if quer_ist
|
||||
else Vector2(tiefe - 0.8, breite - 0.8),
|
||||
"dir": dir,
|
||||
})
|
||||
|
||||
# Blockinneres: eine Gasse von der längeren Seite hinein
|
||||
var innen := groesse - Vector2(tiefe, tiefe) * 2.0
|
||||
if innen.x < 12.0 or innen.y < 12.0:
|
||||
return
|
||||
var laengs := innen.y >= innen.x
|
||||
var richtung := Vector3(0, 0, -1) if laengs else Vector3(-1, 0, 0)
|
||||
# Die Gasse setzt an der Arterie an, sonst hinge sie in der Luft
|
||||
var aussen: float = (groesse.y if laengs else groesse.x) * 0.5 + ARTERIE * 0.5
|
||||
var start := mitte - richtung * aussen
|
||||
var ende := mitte + richtung * ((innen.y if laengs else innen.x) * 0.5 - 2.0)
|
||||
_strasse(start, ende, ROJI)
|
||||
var flanke := Vector3(1, 0, 0) if laengs else Vector3(0, 0, 1)
|
||||
var p_breite := (innen.x if laengs else innen.y) * 0.5 - ROJI * 0.5 - 0.8
|
||||
if p_breite < PARZELLE_MIN * 0.8:
|
||||
return
|
||||
# Nur der Teil im Blockinneren trägt Parzellen
|
||||
var innen_start := mitte - richtung * ((innen.y if laengs else innen.x) * 0.5)
|
||||
var laenge_g := innen_start.distance_to(ende)
|
||||
var anzahl_g := maxi(1, int(round(laenge_g / 13.0)))
|
||||
var tief_g := laenge_g / float(anzahl_g) - 0.8
|
||||
for seite: float in [1.0, -1.0]:
|
||||
for k in range(anzahl_g):
|
||||
var t := (float(k) + 0.5) / float(anzahl_g)
|
||||
var pos := innen_start.lerp(ende, t) + flanke * (seite * (ROJI * 0.5 + p_breite * 0.5))
|
||||
PARZELLEN.append({
|
||||
"pos": pos,
|
||||
"size": Vector2(p_breite, tief_g) if laengs else Vector2(tief_g, p_breite),
|
||||
"dir": ("left" if seite > 0.0 else "right") if laengs
|
||||
else ("forward" if seite > 0.0 else "back"),
|
||||
})
|
||||
|
||||
|
||||
# ------------------------------------------------------------ Belegung
|
||||
|
||||
static func _park_eintragen() -> void:
|
||||
PLACES["park"] = {
|
||||
"name": "Stadtpark", "pos": [0.0, 0.0], "size": [0.0, 0.0],
|
||||
"dir": "none", "color": Color(0.45, 0.65, 0.38),
|
||||
}
|
||||
|
||||
|
||||
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"
|
||||
## Die Sonderbauten bekommen Parzellen an einer Arterie, gut verteilt.
|
||||
static func _sonderbauten_verteilen() -> void:
|
||||
var kandidaten: Array = []
|
||||
for i in range(PARZELLEN.size()):
|
||||
var p: Dictionary = PARZELLEN[i]
|
||||
if minf(p["size"].x, p["size"].y) >= 10.0:
|
||||
kandidaten.append(i)
|
||||
for s in range(SONDER.size()):
|
||||
if kandidaten.is_empty():
|
||||
break
|
||||
# Das Café gehört in die Mitte -- jeder Tagesablauf führt dorthin.
|
||||
# Die übrigen Sonderbauten verteilen sich rundum.
|
||||
var wunsch := Vector3.ZERO
|
||||
if str(SONDER[s][0]) != "cafe":
|
||||
var w := TAU * float(s) / float(SONDER.size() - 1)
|
||||
wunsch = Vector3(cos(w), 0, sin(w)) * 42.0
|
||||
var idx: int = -1
|
||||
var beste := INF
|
||||
for k in kandidaten:
|
||||
var q: Dictionary = PARZELLEN[k]
|
||||
if q.has("belegt"):
|
||||
continue
|
||||
var d: float = (q["pos"] as Vector3).distance_to(wunsch)
|
||||
if d < beste:
|
||||
beste = d
|
||||
idx = int(k)
|
||||
if idx < 0:
|
||||
continue
|
||||
var p: Dictionary = PARZELLEN[idx]
|
||||
p["belegt"] = true
|
||||
PLACES[str(SONDER[s][0])] = {
|
||||
"name": str(SONDER[s][1]),
|
||||
"pos": [p["pos"].x, p["pos"].z],
|
||||
"size": [p["size"].x, p["size"].y],
|
||||
"dir": p["dir"],
|
||||
"color": SONDER[s][2],
|
||||
}
|
||||
|
||||
|
||||
## Die Wohnhäuser: gleichmäßig über den Bezirk verteilt, damit die Wege
|
||||
## nicht alle in derselben Ecke enden.
|
||||
static func _wohnhaeuser_verteilen() -> void:
|
||||
var frei: Array = []
|
||||
for i in range(PARZELLEN.size()):
|
||||
if not PARZELLEN[i].has("belegt"):
|
||||
frei.append(i)
|
||||
if frei.is_empty():
|
||||
return
|
||||
for i in range(EINWOHNER):
|
||||
var idx: int = int(frei[(i * 11 + 5) % frei.size()])
|
||||
var p: Dictionary = PARZELLEN[idx]
|
||||
if p.has("belegt"):
|
||||
for versuch in range(frei.size()):
|
||||
idx = int(frei[(i * 11 + 5 + versuch) % frei.size()])
|
||||
p = PARZELLEN[idx]
|
||||
if not p.has("belegt"):
|
||||
break
|
||||
p["belegt"] = true
|
||||
PLACES[heim_key(i)] = {
|
||||
"name": "%s %d" % [_nachname(i), i + 1],
|
||||
"pos": [p["pos"].x, p["pos"].z],
|
||||
"size": [p["size"].x, p["size"].y],
|
||||
"dir": p["dir"],
|
||||
"color": Color.from_hsv(fmod(0.08 + float(i) * 0.113, 1.0), 0.28, 0.78),
|
||||
}
|
||||
|
||||
|
||||
static func _anker_setzen() -> void:
|
||||
for key in PLACES:
|
||||
var s: Array = PLACES[key]["size"]
|
||||
var tuer := Orte.door(PLACES, key)
|
||||
if float(s[0]) <= 0.0:
|
||||
tuer = Orte.center(PLACES, key)
|
||||
ANKER[key] = int(NETZ.andocken(tuer)["knoten"])
|
||||
|
||||
|
||||
static func _mensch(i: int) -> Dictionary:
|
||||
var anliegen: Dictionary = ANLIEGEN[i % ANLIEGEN.size()]
|
||||
var arbeit: String = str(ARBEIT[i % ARBEIT.size()])
|
||||
# Bis alle Wege entdeckt sind, folgt jeder Mensch dem Weg gleicher Nummer.
|
||||
# Danach mischt sich das Feld wieder.
|
||||
var a_nr: int = i % ANLIEGEN.size()
|
||||
var b_nr: int = i % ABLAEUFE.size()
|
||||
if i < PASSEND.size():
|
||||
a_nr = int(PASSEND[i][0])
|
||||
b_nr = int(PASSEND[i][1])
|
||||
var anliegen: Dictionary = ANLIEGEN[a_nr]
|
||||
var heim := heim_key(i)
|
||||
var arbeit := _naechster_arbeitsplatz(heim, i)
|
||||
var ablauf: Array = []
|
||||
for e in ABLAEUFE[i % ABLAEUFE.size()]:
|
||||
for e in ABLAEUFE[b_nr]:
|
||||
var ort: String = str(e[1])
|
||||
if ort == "heim":
|
||||
ort = heim
|
||||
@@ -186,6 +437,24 @@ static func _mensch(i: int) -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
## Wer wo wohnt, arbeitet in der Nähe. Sonst laufen alle quer durch den Bezirk
|
||||
## und kommen nie an.
|
||||
static func _naechster_arbeitsplatz(heim: String, i: int) -> String:
|
||||
if not PLACES.has(heim):
|
||||
return str(ARBEIT[i % ARBEIT.size()])
|
||||
var von := Orte.center(PLACES, heim)
|
||||
var beste := INF
|
||||
var ziel := str(ARBEIT[i % ARBEIT.size()])
|
||||
for a in ARBEIT:
|
||||
if not PLACES.has(a):
|
||||
continue
|
||||
var d := Orte.center(PLACES, str(a)).distance_to(von)
|
||||
if d < beste:
|
||||
beste = d
|
||||
ziel = str(a)
|
||||
return ziel
|
||||
|
||||
|
||||
static func _vorname(i: int) -> String:
|
||||
return str(VORNAMEN[i % VORNAMEN.size()])
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ func _setze_welt(neu: Node3D) -> void:
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
Game.advance_time(delta)
|
||||
Game.advance_time(delta * Game.zeit_faktor())
|
||||
Game.teleport_lock = maxf(0.0, Game.teleport_lock - delta)
|
||||
if ui.offen:
|
||||
ui.setze_hinweis("")
|
||||
@@ -132,6 +132,11 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
player.look_enabled = true
|
||||
return
|
||||
for stufe in [1, 2, 3]:
|
||||
if event.is_action_pressed("tempo_%d" % stufe):
|
||||
Game.tempo_setzen(stufe)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.is_action_pressed("journal"):
|
||||
ui.journal_panel()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
141
scripts/netz.gd
Normal file
141
scripts/netz.gd
Normal file
@@ -0,0 +1,141 @@
|
||||
class_name Netz
|
||||
extends RefCounted
|
||||
## Ein Wegenetz: Knoten und Kanten in der Ebene. Wege wachsen daran, indem
|
||||
## neue Punkte an die nächste Stelle des bestehenden Netzes angebunden werden.
|
||||
## Wiederverwendung ist gratis -- deshalb bündeln sich Wege und gabeln sich,
|
||||
## statt sternförmig zur Mitte zu laufen.
|
||||
|
||||
var knoten: Array[Vector3] = []
|
||||
var kanten: Array = [] # [a, b] Indexpaare
|
||||
|
||||
|
||||
func fuege_knoten(p: Vector3) -> int:
|
||||
var q := Vector3(p.x, 0.0, p.z)
|
||||
for i in range(knoten.size()):
|
||||
if knoten[i].distance_to(q) < 0.6:
|
||||
return i
|
||||
knoten.append(q)
|
||||
return knoten.size() - 1
|
||||
|
||||
|
||||
func verbinde(a: int, b: int) -> void:
|
||||
if a == b:
|
||||
return
|
||||
for k in kanten:
|
||||
if (k[0] == a and k[1] == b) or (k[0] == b and k[1] == a):
|
||||
return
|
||||
kanten.append([a, b])
|
||||
|
||||
|
||||
func nachbarn(i: int) -> Array:
|
||||
var out: Array = []
|
||||
for k in kanten:
|
||||
if k[0] == i:
|
||||
out.append(k[1])
|
||||
elif k[1] == i:
|
||||
out.append(k[0])
|
||||
return out
|
||||
|
||||
|
||||
## Nächster Punkt auf einer Kante -- gibt {knoten, dist} zurück und teilt die
|
||||
## Kante, falls der Fußpunkt zwischen zwei Knoten liegt.
|
||||
func andocken(p: Vector3) -> Dictionary:
|
||||
var q := Vector3(p.x, 0.0, p.z)
|
||||
var beste := INF
|
||||
var best_i := -1
|
||||
var best_k := -1
|
||||
var best_p := Vector3.ZERO
|
||||
for i in range(knoten.size()):
|
||||
var d := knoten[i].distance_to(q)
|
||||
if d < beste:
|
||||
beste = d
|
||||
best_i = i
|
||||
best_k = -1
|
||||
for ki in range(kanten.size()):
|
||||
var a: Vector3 = knoten[kanten[ki][0]]
|
||||
var b: Vector3 = knoten[kanten[ki][1]]
|
||||
var f := Geometry3D.get_closest_point_to_segment(q, a, b)
|
||||
var d := f.distance_to(q)
|
||||
if d < beste - 0.01 and f.distance_to(a) > 1.5 and f.distance_to(b) > 1.5:
|
||||
beste = d
|
||||
best_k = ki
|
||||
best_p = f
|
||||
if best_k < 0:
|
||||
return {"knoten": best_i, "dist": beste, "punkt": knoten[best_i]}
|
||||
# Kante teilen
|
||||
var alt: Array = kanten[best_k]
|
||||
kanten.remove_at(best_k)
|
||||
var neu := fuege_knoten(best_p)
|
||||
verbinde(alt[0], neu)
|
||||
verbinde(neu, alt[1])
|
||||
return {"knoten": neu, "dist": beste, "punkt": knoten[neu]}
|
||||
|
||||
|
||||
## Kürzester Weg als Knotenfolge, leer wenn es keinen gibt.
|
||||
func pfad(start: int, ziel: int) -> Array:
|
||||
if start == ziel:
|
||||
return [start]
|
||||
var offen: Array = [start]
|
||||
var kosten: Dictionary = {start: 0.0}
|
||||
var her: Dictionary = {}
|
||||
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 k: int = offen[best]
|
||||
offen.remove_at(best)
|
||||
if k == ziel:
|
||||
break
|
||||
for n in nachbarn(k):
|
||||
var neu: float = float(kosten[k]) + knoten[k].distance_to(knoten[n])
|
||||
if not kosten.has(n) or neu < float(kosten[n]) - 0.001:
|
||||
kosten[n] = neu
|
||||
her[n] = k
|
||||
if not offen.has(n):
|
||||
offen.append(n)
|
||||
if not kosten.has(ziel):
|
||||
return []
|
||||
var weg: Array = [ziel]
|
||||
var cur: int = ziel
|
||||
while cur != start:
|
||||
cur = int(her[cur])
|
||||
weg.push_front(cur)
|
||||
return weg
|
||||
|
||||
|
||||
func weglaenge(start: int, ziel: int) -> float:
|
||||
var w := pfad(start, ziel)
|
||||
if w.size() < 2:
|
||||
return 0.0 if w.size() == 1 else INF
|
||||
var l := 0.0
|
||||
for i in range(w.size() - 1):
|
||||
l += knoten[w[i]].distance_to(knoten[w[i + 1]])
|
||||
return l
|
||||
|
||||
|
||||
## Hängen alle Knoten zusammen?
|
||||
func zusammenhaengend() -> bool:
|
||||
if knoten.is_empty():
|
||||
return true
|
||||
var gesehen: Dictionary = {0: true}
|
||||
var stapel: Array = [0]
|
||||
while not stapel.is_empty():
|
||||
var k: int = stapel.pop_back()
|
||||
for n in nachbarn(k):
|
||||
if not gesehen.has(n):
|
||||
gesehen[n] = true
|
||||
stapel.append(n)
|
||||
return gesehen.size() == knoten.size()
|
||||
|
||||
|
||||
## Schneidet die Strecke a-b ein Rechteck (Mittelpunkt c, Größe s, mit Rand)?
|
||||
static func trifft_rechteck(a: Vector3, b: Vector3, c: Vector3, s: Vector2, rand: float) -> bool:
|
||||
var hx := s.x * 0.5 + rand
|
||||
var hz := s.y * 0.5 + rand
|
||||
var schritte := int(maxf(2.0, a.distance_to(b) / 1.5))
|
||||
for i in range(schritte + 1):
|
||||
var p := a.lerp(b, float(i) / float(schritte))
|
||||
if absf(p.x - c.x) <= hx and absf(p.z - c.z) <= hz:
|
||||
return true
|
||||
return false
|
||||
1
scripts/netz.gd.uid
Normal file
1
scripts/netz.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dsaev7wupa8ex
|
||||
@@ -58,11 +58,22 @@ static func door(places: Dictionary, key: String) -> Vector3:
|
||||
return c + d * (half + 2.5)
|
||||
|
||||
|
||||
## Route von Ort a nach Ort b: aus der Tür, über den freien Platz, wieder hinein.
|
||||
static func route(places: Dictionary, a: String, b: String, offset: Vector3) -> Array:
|
||||
## Route von Ort a nach Ort b. Gibt es ein Wegenetz, wird darauf gelaufen --
|
||||
## sonst über den freien Platz in der Mitte.
|
||||
static func route(places: Dictionary, a: String, b: String, offset: Vector3,
|
||||
netz: Netz = null, anker: Dictionary = {}) -> Array:
|
||||
var pts: Array = []
|
||||
if a == b:
|
||||
return pts
|
||||
if netz != null and anker.has(a) and anker.has(b):
|
||||
var weg := netz.pfad(int(anker[a]), int(anker[b]))
|
||||
if not weg.is_empty():
|
||||
for idx in weg:
|
||||
var k: Vector3 = netz.knoten[idx]
|
||||
if pts.is_empty() or pts[pts.size() - 1].distance_to(k) > 0.6:
|
||||
pts.append(k)
|
||||
pts.append(center(places, b) + offset)
|
||||
return pts
|
||||
var da := door(places, a)
|
||||
var db := door(places, b)
|
||||
pts.append(da)
|
||||
|
||||
@@ -9,6 +9,7 @@ static var LIST: Array = [
|
||||
"was": "25 Minuten am Stück an einer Sache arbeiten, dann 5 Minuten Pause. Nach vier Runden eine lange Pause.",
|
||||
"wann": "Wenn eine Aufgabe zu groß wirkt und der Anfang immer wieder verschoben wird.",
|
||||
"warum": "Ein klar begrenzter Zeitraum senkt die Einstiegshürde. Der Kopf muss nur 25 Minuten durchhalten, nicht den ganzen Tag.",
|
||||
"woran": "jemandem, der zögert — oder der arbeitet und trotzdem nicht vorankommt.",
|
||||
"tags": ["prokrastination", "fokus"],
|
||||
"momente": ["arbeitet", "zoegert"],
|
||||
"farbe": Color(0.95, 0.45, 0.30),
|
||||
@@ -19,6 +20,7 @@ static var LIST: Array = [
|
||||
"was": "Was unter zwei Minuten dauert, sofort erledigen. Große Vorhaben auf einen Zwei-Minuten-Anfang schrumpfen.",
|
||||
"wann": "Bei einem Berg von Kleinkram oder wenn schon der erste Schritt blockiert.",
|
||||
"warum": "Der Start kostet die meiste Energie. Zwei Minuten sind zu klein zum Ablehnen und ziehen meist die ganze Aufgabe nach sich.",
|
||||
"woran": "jemandem, der zögert, lieber aufräumt als anzufangen, oder beim Arbeiten im Kleinkram hängt.",
|
||||
"tags": ["prokrastination", "ueberforderung"],
|
||||
"momente": ["zoegert", "aufraeumen", "arbeitet"],
|
||||
"farbe": Color(0.98, 0.72, 0.25),
|
||||
@@ -29,6 +31,7 @@ static var LIST: Array = [
|
||||
"was": "Den Vorsatz an einen konkreten Auslöser binden: \"Wenn ich X tue, dann mache ich Y.\"",
|
||||
"wann": "Wenn ein Vorhaben im Alltag jedes Mal untergeht.",
|
||||
"warum": "Der Plan hängt an einer Situation statt an Willenskraft. Der Auslöser erinnert von allein.",
|
||||
"woran": "der Morgenroutine und der Pause — den festen Punkten, an die sich ein Vorsatz hängen lässt.",
|
||||
"tags": ["gewohnheit", "prokrastination"],
|
||||
"momente": ["morgenroutine", "pause"],
|
||||
"farbe": Color(0.45, 0.80, 0.55),
|
||||
@@ -39,6 +42,7 @@ static var LIST: Array = [
|
||||
"was": "Eine neue, kleine Gewohnheit direkt an eine bestehende anhängen.",
|
||||
"wann": "Wenn eine neue Routine nie einen Platz im Tag findet.",
|
||||
"warum": "Die alte Gewohnheit ist der Anker. Es muss kein neuer Zeitpunkt erfunden werden.",
|
||||
"woran": "den Ankern des Tages: Morgenroutine, Essen, Abendroutine.",
|
||||
"tags": ["gewohnheit"],
|
||||
"momente": ["morgenroutine", "essen", "abendroutine"],
|
||||
"farbe": Color(0.40, 0.70, 0.85),
|
||||
@@ -49,6 +53,7 @@ static var LIST: Array = [
|
||||
"was": "Ein großes Ziel in benennbare Einzelschritte teilen, bis der nächste Schritt trivial ist.",
|
||||
"wann": "Wenn jemand vor einem Vorhaben erstarrt und nicht weiß, wo er anfangen soll.",
|
||||
"warum": "Unklarheit lähmt stärker als Aufwand. Ein sichtbarer nächster Schritt macht Handeln wieder möglich.",
|
||||
"woran": "jemandem, der grübelt statt anzufangen, oder der arbeitet und sich im Vorhaben verliert.",
|
||||
"tags": ["ueberforderung", "prokrastination", "ziel"],
|
||||
"momente": ["gruebelt", "arbeitet"],
|
||||
"farbe": Color(0.75, 0.55, 0.95),
|
||||
@@ -59,6 +64,7 @@ static var LIST: Array = [
|
||||
"was": "Das gewünschte Verhalten leichter machen, das störende schwerer. Handy in einen anderen Raum, Instrument sichtbar aufstellen.",
|
||||
"wann": "Wenn gute Vorsätze regelmäßig an Ablenkung scheitern.",
|
||||
"warum": "Verhalten folgt dem geringsten Widerstand. Die Umgebung wirkt zuverlässiger als Disziplin.",
|
||||
"woran": "jemandem, der aufräumt, arbeitet oder sich ausruht — dann ist der Raum um ihn herum greifbar.",
|
||||
"tags": ["ablenkung", "gewohnheit"],
|
||||
"momente": ["aufraeumen", "arbeitet", "ruht"],
|
||||
"farbe": Color(0.55, 0.85, 0.40),
|
||||
@@ -69,6 +75,7 @@ static var LIST: Array = [
|
||||
"was": "Einen belastenden Gedanken prüfen und durch eine ebenso wahre, aber tragfähigere Sicht ersetzen.",
|
||||
"wann": "Wenn jemand in einem Gedanken feststeckt, der ihn klein hält.",
|
||||
"warum": "Nicht das Ereignis belastet, sondern seine Deutung. Eine andere Deutung verändert Gefühl und Handlung.",
|
||||
"woran": "jemandem, der grübelt, angespannt ist oder sich ausruht und den Gedanken doch nicht loswird.",
|
||||
"tags": ["selbstzweifel", "angst"],
|
||||
"momente": ["gruebelt", "ruht", "angespannt"],
|
||||
"farbe": Color(0.95, 0.55, 0.75),
|
||||
@@ -79,6 +86,7 @@ static var LIST: Array = [
|
||||
"was": "Vier Sekunden einatmen, vier halten, vier ausatmen, vier halten. Mehrere Runden lang.",
|
||||
"wann": "Bei akuter Anspannung, kurz vor einem gefürchteten Gespräch oder Auftritt.",
|
||||
"warum": "Langes Ausatmen bremst den Alarmzustand des Körpers. Der Puls sinkt, der Kopf wird wieder klar.",
|
||||
"woran": "jemandem, der angespannt ist, wartet oder in der Pause nicht runterkommt.",
|
||||
"tags": ["angst", "stress"],
|
||||
"momente": ["angespannt", "wartet", "pause"],
|
||||
"farbe": Color(0.40, 0.85, 0.95),
|
||||
@@ -89,6 +97,7 @@ static var LIST: Array = [
|
||||
"was": "Mit sich selbst so sprechen wie mit einem guten Freund in derselben Lage.",
|
||||
"wann": "Nach einem Fehler oder Rückschlag, wenn die innere Kritik lauter wird als der Vorsatz.",
|
||||
"warum": "Harte Selbstkritik erhöht die Vermeidung. Freundlichkeit hält handlungsfähig und senkt die Abbruchquote.",
|
||||
"woran": "jemandem, der nach einem Fehler grübelt, angespannt ist oder sich ausruht, ohne zur Ruhe zu kommen.",
|
||||
"tags": ["selbstzweifel", "rueckschlag"],
|
||||
"momente": ["gruebelt", "ruht", "angespannt"],
|
||||
"farbe": Color(1.0, 0.80, 0.60),
|
||||
@@ -99,6 +108,7 @@ static var LIST: Array = [
|
||||
"was": "Feste Aufsteh- und Schlafenszeit, die letzte Stunde ohne Bildschirm, Zimmer dunkel und kühl.",
|
||||
"wann": "Bei Dauermüdigkeit, Reizbarkeit und wegbrechender Konzentration.",
|
||||
"warum": "Der innere Rhythmus braucht feste Anker. Erholter Schlaf ist die Grundlage für jede andere Veränderung.",
|
||||
"woran": "der Abendroutine, an jemandem, der müde ist oder sich mitten am Tag ausruht.",
|
||||
"tags": ["erschoepfung", "gesundheit"],
|
||||
"momente": ["abendroutine", "muede", "ruht"],
|
||||
"farbe": Color(0.45, 0.50, 0.90),
|
||||
@@ -109,6 +119,7 @@ static var LIST: Array = [
|
||||
"was": "Eine klare, kleine Bitte an eine konkrete Person richten, statt allgemein zu hoffen.",
|
||||
"wann": "Wenn jemand allein an etwas hängen bleibt, das andere leicht lösen könnten.",
|
||||
"warum": "Vage Bitten werden überhört. Menschen helfen gern, wenn sie genau wissen, was zu tun ist.",
|
||||
"woran": "jemandem, der wartet, Pause macht oder unterwegs ist — da ist Luft für eine Bitte.",
|
||||
"tags": ["einsamkeit", "ueberforderung"],
|
||||
"momente": ["wartet", "pause", "unterwegs"],
|
||||
"farbe": Color(0.90, 0.90, 0.45),
|
||||
@@ -119,6 +130,7 @@ static var LIST: Array = [
|
||||
"was": "Absagen und zugleich benennen, was ginge: \"Heute nicht, am Donnerstag gern.\"",
|
||||
"wann": "Wenn der eigene Kalender von fremden Anliegen aufgebraucht wird.",
|
||||
"warum": "Ein nacktes Nein fühlt sich riskant an. Mit Alternative bleibt die Beziehung heil und die Zeit geschützt.",
|
||||
"woran": "jemandem, der arbeitet, angespannt ist oder unterwegs zwischen Terminen hängt.",
|
||||
"tags": ["stress", "grenzen"],
|
||||
"momente": ["arbeitet", "angespannt", "unterwegs"],
|
||||
"farbe": Color(0.85, 0.35, 0.45),
|
||||
@@ -126,38 +138,6 @@ static var LIST: Array = [
|
||||
]
|
||||
|
||||
|
||||
static var TAG_TEXT: Dictionary = {
|
||||
"prokrastination": "Aufschieben",
|
||||
"fokus": "Konzentration",
|
||||
"ueberforderung": "Überforderung",
|
||||
"gewohnheit": "Gewohnheiten",
|
||||
"ziel": "Ziele",
|
||||
"ablenkung": "Ablenkung",
|
||||
"selbstzweifel": "Selbstzweifel",
|
||||
"angst": "Angst",
|
||||
"stress": "Stress",
|
||||
"rueckschlag": "Rückschläge",
|
||||
"erschoepfung": "Erschöpfung",
|
||||
"gesundheit": "Gesundheit",
|
||||
"einsamkeit": "Einsamkeit",
|
||||
"grenzen": "Grenzen setzen",
|
||||
}
|
||||
|
||||
|
||||
static func tags_text(tags: Array) -> String:
|
||||
var out: Array = []
|
||||
for t in tags:
|
||||
out.append(TAG_TEXT.get(t, t))
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
static func momente_text(momente: Array) -> String:
|
||||
var out: Array = []
|
||||
for m in momente:
|
||||
out.append(Orte.text(m))
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
static func by_id(id: String) -> Dictionary:
|
||||
for t in LIST:
|
||||
if t["id"] == id:
|
||||
|
||||
144
scripts/ui.gd
144
scripts/ui.gd
@@ -48,7 +48,7 @@ func _baue_hud() -> void:
|
||||
add_child(_hud)
|
||||
|
||||
var steuerung := Label.new()
|
||||
steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · J Journal · Esc Maus frei"
|
||||
steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · J Journal · 1/2/3 Tempo · Esc Maus frei"
|
||||
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
steuerung.position = Vector2(-640, 16)
|
||||
steuerung.size = Vector2(620, 24)
|
||||
@@ -144,9 +144,14 @@ func _process(_delta: float) -> void:
|
||||
|
||||
func _hud_aktualisieren() -> void:
|
||||
var zeilen: Array = []
|
||||
zeilen.append("[b]%s[/b]" % Game.time_string())
|
||||
zeilen.append("Einsicht: [b]%d[/b] Techniken: [b]%d / %d[/b]" %
|
||||
[Game.einsicht, Game.known.size(), TechDB.LIST.size()])
|
||||
var takt := ("[color=#aaaaaa]Tempo %d (%d×)[/color]"
|
||||
% [Game.tempo, int(Game.faktor_von(Game.tempo))])
|
||||
if Game.schlaeft_ziel():
|
||||
takt = ("[color=#9fb6ff]Tempo 3 (%d×) · Nachtsprung[/color]"
|
||||
% int(Game.faktor_von(3)))
|
||||
zeilen.append("[b]%s[/b] %s" % [Game.time_string(), takt])
|
||||
zeilen.append("Techniken: [b]%d[/b] gelernt · [b]%d[/b] entdeckt · %d im Spiel" %
|
||||
[Game.known.size(), Game.offene_techniken(), TechDB.LIST.size()])
|
||||
if Game.active_mission != "":
|
||||
var h := HumanDB.by_id(Game.active_mission)
|
||||
zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %% · %d Regeln" %
|
||||
@@ -246,8 +251,9 @@ func schliessen() -> void:
|
||||
|
||||
|
||||
func _hinweis_block(t: Dictionary) -> String:
|
||||
return ("[b]Was:[/b] %s\n[b]Wann:[/b] %s\n[b]Warum:[/b] %s" %
|
||||
[t["was"], t["wann"], t["warum"]])
|
||||
return ("[b]Was:[/b] %s\n[b]Wann:[/b] %s\n[b]Warum:[/b] %s\n" +
|
||||
"[color=#9fb6ff][b]Zu sehen an:[/b] %s[/color]") % \
|
||||
[t["was"], t["wann"], t["warum"], t["woran"]]
|
||||
|
||||
|
||||
# -------------------------------------------------------- Journal
|
||||
@@ -294,18 +300,17 @@ func _reiter() -> void:
|
||||
if Game.active_mission != "":
|
||||
mission = "%s (%d %%)" % [HumanDB.by_id(Game.active_mission)["name"],
|
||||
int(Game.mission_progress(Game.active_mission))]
|
||||
_text("[color=#aaaaaa]Einsicht: [b]%d[/b] · Mission: %s[/color]" %
|
||||
[Game.einsicht, mission], 15)
|
||||
_text("[color=#aaaaaa]Mission: %s[/color]" % mission, 15)
|
||||
_trenner()
|
||||
|
||||
|
||||
func _journal_techniken() -> void:
|
||||
if Game.known.is_empty():
|
||||
_text("[color=#ffcc66]Du hast noch keine Technik gelernt.[/color]\n" +
|
||||
"Im [b]Hörsaal der Wege[/b] hinter dem Dorfplatz wird der erste Weg gelehrt.", 16)
|
||||
"Im [b]Hörsaal der Wege[/b] hinter dem Dorfplatz werden sie gelehrt.", 16)
|
||||
return
|
||||
_text("Was du gelernt hast. Jede Anwendung draußen macht eine Technik von allein " +
|
||||
"besser; vertieft wird im Hörsaal.", 15)
|
||||
_text("Was du gelernt hast. Besser wird eine Technik nur, indem du sie draußen " +
|
||||
"anwendest — Üben lässt sie sich nicht.", 15)
|
||||
_trenner()
|
||||
for id in TechDB.ids():
|
||||
if not Game.knows(id):
|
||||
@@ -314,8 +319,6 @@ func _journal_techniken() -> void:
|
||||
_text("[b][color=#ffe3a0]%s[/color][/b] · Stufe %d (%s)" %
|
||||
[t["name"], Game.skill_of(id), Game.xp_to_next(id)], 18)
|
||||
_text(_hinweis_block(t), 15)
|
||||
_text("[color=#9fb6ff]Hilft bei:[/color] %s [color=#9fb6ff]Momente:[/color] %s" %
|
||||
[TechDB.tags_text(t["tags"]), TechDB.momente_text(t["momente"])], 14)
|
||||
_trenner()
|
||||
|
||||
|
||||
@@ -335,7 +338,6 @@ func _journal_missionen() -> void:
|
||||
[h["name"], h["alter"], h["beruf"]], 18)
|
||||
_text("[b]Traum:[/b] %s" % h["traum"], 16)
|
||||
_text("[b]Was im Weg steht:[/b] %s" % h["problem"], 15)
|
||||
_text("[color=#9fb6ff]Themen:[/color] %s" % TechDB.tags_text(h["tags"]), 14)
|
||||
if Game.completed.has(id):
|
||||
_text("[color=#88ff99]★ Der Traum ist erfüllt.[/color]", 15)
|
||||
elif Game.active_mission == id:
|
||||
@@ -359,7 +361,8 @@ func pipeline_panel(human_id: String) -> void:
|
||||
var h := HumanDB.by_id(human_id)
|
||||
_leeren("Regelwerk — %s" % h["name"])
|
||||
_text("Jede Tätigkeit im Tag von %s kann eine Technik auslösen. " % h["name"] +
|
||||
"Wechselt sie hinein, greift die Regel von allein — solange du auf der Erde bist.", 15)
|
||||
"Wechselt sie hinein, greift die Regel von allein — solange du auf der Erde bist.\n" +
|
||||
"Nach jedem Einsatz braucht eine Technik Ruhe, bevor sie wieder anschlägt.", 15)
|
||||
_text("[color=#aaaaaa]Fortschritt: [b]%d %%[/b] · Regeln: [b]%d[/b][/color]" %
|
||||
[int(Game.mission_progress(human_id)), Game.regel_anzahl(human_id)], 15)
|
||||
_trenner()
|
||||
@@ -371,9 +374,9 @@ func pipeline_panel(human_id: String) -> void:
|
||||
var tech: String = Game.regel_fuer(human_id, activity)
|
||||
var beschriftung := "%s → keine Regel" % tat
|
||||
if tech != "":
|
||||
var ev := Game.evaluate(tech, human_id, activity, false)
|
||||
beschriftung = "%s → %s · %d %%" % [tat, TechDB.by_id(tech)["name"],
|
||||
int(round(float(ev["chance"]) * 100.0))]
|
||||
beschriftung = "%s → %s" % [tat, TechDB.by_id(tech)["name"]]
|
||||
if not Game.bereit(tech):
|
||||
beschriftung += " (noch gesperrt)"
|
||||
_knopf(beschriftung, func() -> void: regel_panel(human_id, activity))
|
||||
_trenner()
|
||||
_knopf("Zurück zum Journal", func() -> void: journal_panel("missionen"))
|
||||
@@ -392,38 +395,32 @@ func regel_panel(human_id: String, activity: String) -> void:
|
||||
_knopf("Zurück", func() -> void: pipeline_panel(human_id))
|
||||
_zeigen("regel")
|
||||
return
|
||||
_text("Was soll greifen, wenn %s %s? Die Erfolgschance gilt für genau diesen Moment. " %
|
||||
_text("Was soll greifen, wenn %s %s? Ob eine Technik hier taugt, steht in ihr " %
|
||||
[h["name"], Orte.text(activity)] +
|
||||
"Jede Technik hält nur einen Posten — sie hier einzusetzen nimmt sie anderswo weg.", 15)
|
||||
"selbst — nachzulesen im Journal. Ein Fehlgriff wirft den Traum zurück.", 15)
|
||||
_trenner()
|
||||
for id in TechDB.ids():
|
||||
if not Game.knows(id):
|
||||
continue
|
||||
var t := TechDB.by_id(id)
|
||||
var ev := Game.evaluate(id, human_id, activity, false)
|
||||
var marker: Array = []
|
||||
if ev["tag"]:
|
||||
marker.append("[color=#88ff99]passt zum Thema[/color]")
|
||||
if ev["moment"]:
|
||||
marker.append("[color=#88ff99]passender Moment[/color]")
|
||||
if ev["blocked"]:
|
||||
marker.append("[color=#ff8877]%s[/color]" % ev["grund"])
|
||||
if marker.is_empty():
|
||||
marker.append("[color=#aaaaaa]weder Thema noch Moment passen[/color]")
|
||||
var wo: Array = Game.regel_momente(human_id, id)
|
||||
var gewaehlt := ""
|
||||
var haengt := Game.regel_moment(human_id, id)
|
||||
if haengt == activity:
|
||||
gewaehlt = " [color=#ffdd77](hier eingestellt)[/color]"
|
||||
elif haengt != "":
|
||||
gewaehlt = " [color=#ffaa66](hängt an »%s«)[/color]" % Orte.text(haengt)
|
||||
_text("[b]%s[/b] · Stufe %d%s\n[color=#cccccc]%s[/color]\n%s" %
|
||||
[t["name"], Game.skill_of(id), gewaehlt, t["wann"], " · ".join(marker)], 16)
|
||||
var beschriftung := "Blockiert" if ev["blocked"] else \
|
||||
"Diese Regel — %d %% Erfolgschance" % int(round(float(ev["chance"]) * 100.0))
|
||||
if wo.has(activity):
|
||||
gewaehlt = " — hier eingestellt"
|
||||
elif not wo.is_empty():
|
||||
var namen: Array = []
|
||||
for a in wo:
|
||||
namen.append("»%s«" % Orte.text(a))
|
||||
gewaehlt = " — hängt an %s" % ", ".join(namen)
|
||||
var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(id)]
|
||||
if ev["blocked"]:
|
||||
beschriftung += " — " + str(ev["grund"])
|
||||
elif gewaehlt != "":
|
||||
beschriftung += gewaehlt
|
||||
_knopf(beschriftung, func() -> void:
|
||||
Game.regel_setzen(human_id, activity, id)
|
||||
pipeline_panel(human_id), not ev["blocked"])
|
||||
_trenner()
|
||||
_knopf("Keine Regel für diese Tätigkeit", func() -> void:
|
||||
Game.regel_setzen(human_id, activity, "")
|
||||
pipeline_panel(human_id))
|
||||
@@ -441,8 +438,8 @@ func lehre_panel(ort: String = "hoersaal") -> void:
|
||||
var pl: Dictionary = DreamDB.PLACES[ort]
|
||||
var ids: Array = DreamDB.techniken_von(ort)
|
||||
_leeren(str(pl["name"]))
|
||||
_text("[color=#aaaaaa]Einsicht: [b]%d[/b] · Gelernt: [b]%d / %d[/b] von %d entdeckten[/color]" %
|
||||
[Game.einsicht, Game.known.size(), TechDB.LIST.size(), Game.offene_techniken()], 15)
|
||||
_text("[color=#aaaaaa]Gelernt: [b]%d[/b] von %d entdeckten Wegen · %d im Spiel[/color]" %
|
||||
[Game.known.size(), Game.offene_techniken(), TechDB.LIST.size()], 15)
|
||||
_trenner()
|
||||
if ids.is_empty():
|
||||
_text("[color=#ffcc66]Hier wird nichts gelehrt.[/color]", 16)
|
||||
@@ -454,16 +451,13 @@ func lehre_panel(ort: String = "hoersaal") -> void:
|
||||
else:
|
||||
_text("[b][color=#9aa0b8]%s[/color][/b] · noch nicht gelernt" % t["name"], 18)
|
||||
_text(_hinweis_block(t), 15)
|
||||
_text("[color=#9fb6ff]Hilft bei:[/color] %s [color=#9fb6ff]Momente:[/color] %s" %
|
||||
[TechDB.tags_text(t["tags"]), TechDB.momente_text(t["momente"])], 14)
|
||||
if Game.knows(id):
|
||||
_knopf("Vertiefen (1 Einsicht)", func() -> void:
|
||||
if Game.deepen(id):
|
||||
lehre_panel(ort), Game.einsicht >= 1)
|
||||
_text("[color=#88ff99]Gelernt. Stufe %d steigt nur, wenn du diesen Weg " %
|
||||
Game.skill_of(id) + "draußen anwendest — hier ist nichts mehr zu holen.[/color]", 15)
|
||||
else:
|
||||
_knopf("Lernen (1 Einsicht)", func() -> void:
|
||||
_knopf("Diesen Weg lernen", func() -> void:
|
||||
if Game.learn(id):
|
||||
lehre_panel(ort), Game.einsicht >= 1)
|
||||
lehre_panel(ort))
|
||||
_trenner()
|
||||
_naechster_weg()
|
||||
_zeigen("lehre")
|
||||
@@ -489,19 +483,24 @@ func _naechster_weg() -> void:
|
||||
func brett_panel() -> void:
|
||||
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "brett" else 0
|
||||
_leeren("Schwarzes Brett")
|
||||
_text("Menschen, deren Traum feststeckt. Nimm einen an — das Tor zur Erde " +
|
||||
"öffnet sich nur mit einer Mission.", 15)
|
||||
_text("Ausgehängt wird nur, wem du mit dem, was du kannst, auch helfen kannst. " +
|
||||
"Das Tor zur Erde öffnet sich nur mit einer Mission.", 15)
|
||||
_trenner()
|
||||
var gezeigt := 0
|
||||
var verdeckt := 0
|
||||
for h in HumanDB.LIST:
|
||||
var id: String = h["id"]
|
||||
if Game.completed.has(id):
|
||||
continue
|
||||
if Game.active_mission != id and not Game.machbar(id):
|
||||
verdeckt += 1
|
||||
continue
|
||||
gezeigt += 1
|
||||
_text("[b][color=#ffe3a0]%s[/color][/b], %d — %s" %
|
||||
[h["name"], h["alter"], h["beruf"]], 18)
|
||||
_text("[b]Traum:[/b] %s" % h["traum"], 16)
|
||||
_text("[b]Was im Weg steht:[/b] %s" % h["problem"], 15)
|
||||
_text("[color=#9fb6ff]Themen:[/color] %s" % TechDB.tags_text(h["tags"]), 14)
|
||||
if Game.completed.has(id):
|
||||
_text("[color=#88ff99]★ Der Traum ist erfüllt.[/color]", 15)
|
||||
elif Game.active_mission == id:
|
||||
if Game.active_mission == id:
|
||||
_text("[color=#ffdd77]Aktive Mission — %d %%[/color]" %
|
||||
int(Game.mission_progress(id)), 15)
|
||||
else:
|
||||
@@ -512,6 +511,12 @@ func brett_panel() -> void:
|
||||
Game.accept_mission(id)
|
||||
brett_panel())
|
||||
_trenner()
|
||||
if gezeigt == 0:
|
||||
_text("[color=#ffcc66]Gerade hängt nichts aus, das zu deinen Wegen passt.[/color]\n" +
|
||||
"Lerne im Hörsaal oder in einem Haus einen weiteren Weg.", 16)
|
||||
if verdeckt > 0:
|
||||
_text("[color=#aaaaaa]%d weitere Anliegen bleiben verdeckt. Keiner deiner Wege " %
|
||||
verdeckt + "trifft ihr Thema in einem Moment ihres Tages.[/color]", 15)
|
||||
_zeigen("brett")
|
||||
_scroll_wunsch = scroll_alt
|
||||
_scroll_frames = 4
|
||||
@@ -538,28 +543,22 @@ func anwenden_panel(mensch: Node3D) -> void:
|
||||
_text("[color=#ffcc66]Du kennst keine Technik.[/color]")
|
||||
_zeigen("anwenden")
|
||||
return
|
||||
_text("[color=#aaaaaa]Was eine Technik tut, steht im Journal.[/color]", 15)
|
||||
|
||||
for id in TechDB.ids():
|
||||
if not Game.knows(id):
|
||||
continue
|
||||
var t := TechDB.by_id(id)
|
||||
var ev := Game.evaluate(id, h["id"], aktiv, mensch.resist_left > 0.0)
|
||||
var marker: Array = []
|
||||
if ev["tag"]:
|
||||
marker.append("[color=#88ff99]passt zum Thema[/color]")
|
||||
if ev["moment"]:
|
||||
marker.append("[color=#88ff99]passender Moment[/color]")
|
||||
if ev["blocked"]:
|
||||
marker.append("[color=#ff8877]%s[/color]" % ev["grund"])
|
||||
var gesperrt := not Game.bereit(id)
|
||||
var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(id)]
|
||||
if gesperrt:
|
||||
beschriftung += " — noch %d s gesperrt" % int(ceil(Game.cooldown_rest(id)))
|
||||
elif ev["blocked"]:
|
||||
beschriftung += " — " + str(ev["grund"])
|
||||
elif mensch.resist_left > 0.0:
|
||||
marker.append("[color=#ff8877]macht gerade dicht[/color]")
|
||||
if marker.is_empty():
|
||||
marker.append("[color=#aaaaaa]weder Thema noch Moment passen[/color]")
|
||||
_text("[b]%s[/b] · Stufe %d\n[color=#cccccc]%s[/color]\n%s" %
|
||||
[t["name"], Game.skill_of(id), t["wann"], " · ".join(marker)], 16)
|
||||
var beschriftung := "Blockiert" if ev["blocked"] else "Anwenden — %d %% Erfolgschance" % int(round(float(ev["chance"]) * 100.0))
|
||||
_knopf(beschriftung, func() -> void: _anwenden(id), not ev["blocked"])
|
||||
_trenner()
|
||||
beschriftung += " — macht gerade dicht"
|
||||
_knopf(beschriftung, func() -> void: _anwenden(id), not ev["blocked"] and not gesperrt)
|
||||
_zeigen("anwenden")
|
||||
|
||||
|
||||
@@ -583,9 +582,10 @@ func _anwenden(id: String) -> void:
|
||||
Game.notified.emit("Immer dieselbe Technik wirkt schwächer. Probier eine andere.",
|
||||
Color(0.85, 0.85, 0.6))
|
||||
else:
|
||||
mensch.reagiere(false, "%s prallt ab" % t["name"])
|
||||
Game.notified.emit("%s dringt gerade nicht durch (%d %% Chance)." %
|
||||
[t["name"], int(round(float(res["chance"]) * 100.0))], Color(1, 0.6, 0.5))
|
||||
mensch.reagiere(false, "%s prallt ab\n−%d %%" % [t["name"],
|
||||
int(round(float(res["verlust"])))])
|
||||
Game.notified.emit("%s dringt nicht durch. (−%d %%)" %
|
||||
[t["name"], int(round(float(res["verlust"])))], Color(1, 0.6, 0.5))
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
|
||||
Reference in New Issue
Block a user