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

View File

@@ -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"])

View File

@@ -1,15 +1,17 @@
class_name DreamDB
## Die Traumwelt: ein Dorf, das mit jedem erfüllten Traum wächst.
## Fest sind nur Hörsaal, Dorfplatz und Tor zur Erde. Jedes Haus und sein
## Bewohner werden erzeugt — der Reihe nach, immer weiter nach außen.
## Fest sind nur Hörsaal, Dorfplatz und Tor zur Erde. Jedes weitere Haus sucht
## sich seinen Platz selbst: es wird dort gebaut, wo es Nachbarn, einen Weg und
## den Dorfplatz in erträglicher Nähe hat. Der Weg dorthin dockt an der
## nächsten Stelle des bestehenden Netzes an. Weil Wiederverwendung gratis ist,
## gabeln sich die Wege und werden verwinkelt, statt sternförmig zu laufen.
## Vorbild: Emilien et al., "Procedural Generation of Villages" (2012).
const HAEUSER_JE_RING := 6
const RING_ERSTER := 30.0
const RING_ABSTAND := 24.0
## Häuser sitzen auf Achtelrichtungen. Die Achsen 0 (Hörsaal) und 4 (Tor)
## bleiben frei, damit der Weg vom Dorfplatz zu beiden offen bleibt.
const SLOTS := [1, 2, 3, 5, 6, 7]
const PLATZ_FREI := 22.0 # Radius um den Dorfplatz, der leer bleibt
const HAUS_ABSTAND := 4.0 # Mindestluft zwischen zwei Gebäuden
const WEG_ABSTAND := 3.0 # Mindestluft zwischen Haus und Weg
const KANDIDATEN := 64 # Probepunkte je Haus
const SPINE_SCHRITT := 8.0 # Abstand der Knoten auf der Hauptachse
static var FEST: Dictionary = {
"hoersaal": {
@@ -29,6 +31,8 @@ static var FEST: Dictionary = {
## Wird von erzeuge() gefüllt und wächst mit dem Dorf.
static var PLACES: Dictionary = FEST.duplicate(true)
static var LIST: Array = []
static var NETZ: Netz = Netz.new()
static var ANKER: Dictionary = {}
static var FESTE_NAMEN: Array = ["Ailen", "Morr", "Ivo", "Sela"]
static var FESTE_ROLLEN: Array = ["Traumweberin", "Erinnerungssammler", "Bote",
@@ -56,47 +60,346 @@ static func haus_key(i: int) -> String:
return "haus_%d" % i
## Baut Orte und Bewohner für ein Dorf mit so vielen Häusern neu auf.
## Baut Orte, Wegenetz und Bewohner für ein Dorf mit so vielen Häusern neu auf.
## Deterministisch: Haus i landet immer am selben Fleck.
static func erzeuge(haeuser: int) -> void:
PLACES = FEST.duplicate(true)
LIST = []
NETZ = Netz.new()
ANKER = {}
_hauptachse()
for i in range(maxi(haeuser, 0)):
PLACES[haus_key(i)] = _haus(i)
var haus := _platz_suchen(i)
PLACES[haus_key(i)] = haus
_anschliessen(haus_key(i))
LIST.append(_bewohner(i))
## Wie weit reicht das Dorf hinaus?
static func aeusserster_ring(haeuser: int) -> float:
if haeuser <= 0:
return 0.0
var ring := (haeuser - 1) / HAEUSER_JE_RING
return RING_ERSTER + float(ring) * RING_ABSTAND
## Die feste Achse Hörsaal – Dorfplatz – Tor. Sie ist von Anfang an da und
## trägt genug Knoten, damit Häuser überall andocken können.
static func _hauptachse() -> void:
var punkte: Array[Vector3] = []
var a := Orte.door(PLACES, "hoersaal")
var m := Orte.center(PLACES, "lichtung")
var b := Orte.door(PLACES, "tor")
for paar in [[a, m], [m, b]]:
var von: Vector3 = paar[0]
var nach: Vector3 = paar[1]
var n := maxi(1, int(round(von.distance_to(nach) / SPINE_SCHRITT)))
for k in range(n + 1):
var p := von.lerp(nach, float(k) / float(n))
if punkte.is_empty() or punkte[punkte.size() - 1].distance_to(p) > 0.6:
punkte.append(p)
var vorher := -1
for p in punkte:
var idx := NETZ.fuege_knoten(p)
if vorher >= 0:
NETZ.verbinde(vorher, idx)
vorher = idx
ANKER["hoersaal"] = NETZ.fuege_knoten(a)
ANKER["lichtung"] = NETZ.fuege_knoten(m)
ANKER["tor"] = NETZ.fuege_knoten(b)
static func _haus(i: int) -> Dictionary:
var ring := i / HAEUSER_JE_RING
var winkel := float(SLOTS[i % HAEUSER_JE_RING]) * TAU / 8.0
var r := RING_ERSTER + float(ring) * RING_ABSTAND
var x := sin(winkel) * r
var z := cos(winkel) * r
# ------------------------------------------------------------ Platzsuche
## Wählt unter vielen Probepunkten den mit dem höchsten Interesse.
static func _platz_suchen(i: int) -> Dictionary:
var rng := RandomNumberGenerator.new()
rng.seed = 4711 + i * 7919
var breite := 12.0 + float(i % 2) * 2.0
var tiefe := 10.0 + float(i % 3)
var groesse := Vector2(breite, tiefe)
# Die Suchfläche wächst mit der Zahl der Häuser, nicht mit dem weitesten
# Ausreißer. So bleibt die Dichte gleich, statt zu zerfasern.
var aussen := maxf(_wachstumsradius() + 8.0,
sqrt(PLATZ_FREI * PLATZ_FREI + 240.0 * float(i + 1)))
var bester := Vector3.ZERO
var bestes := -1.0
for k in range(KANDIDATEN):
var winkel := rng.randf() * TAU
var r := PLATZ_FREI + rng.randf() * maxf(6.0, aussen - PLATZ_FREI)
var p := Vector3(cos(winkel) * r, 0.0, sin(winkel) * r)
if not _platz_frei(p, groesse):
continue
var wert := _interesse(p)
if wert > bestes:
bestes = wert
bester = p
if bestes < 0.0:
# Notnagel: irgendwo weit außen, damit das Dorf nie steckenbleibt
bester = _notplatz(i, groesse)
return {
"name": "%s Haus" % _besitz(_name(i)),
"pos": [x, z],
"pos": [bester.x, bester.z],
"size": [breite, tiefe],
"dir": _tuer_zur_mitte(x, z),
"dir": _tuer_zum_netz(bester, groesse),
"color": Color.from_hsv(fmod(0.60 + float(i) * 0.137, 1.0), 0.34, 0.86),
}
## Die Tür zeigt zur Dorfmitte — auf die Seite, die ihr am nächsten liegt.
static func _tuer_zur_mitte(x: float, z: float) -> String:
if absf(x) > absf(z):
return "left" if x > 0.0 else "right"
return "forward" if z > 0.0 else "back"
## Wie weit reichen die Häuser? Danach richtet sich, wo neu gesucht wird --
## nicht nach Hörsaal und Tor, sonst zerfasert das Dorf sofort.
static func _wachstumsradius() -> float:
var r := PLATZ_FREI + 8.0
for key in PLACES:
if not key.begins_with("haus_"):
continue
var s: Array = PLACES[key]["size"]
var c := Orte.center(PLACES, key)
r = maxf(r, Vector2(absf(c.x) + float(s[0]) * 0.5,
absf(c.z) + float(s[1]) * 0.5).length())
return r
static func _notplatz(i: int, groesse: Vector2) -> Vector3:
var r := aussenradius() + 20.0
for k in range(72):
var a := TAU * float((i * 29 + k) % 72) / 72.0
var p := Vector3(cos(a) * r, 0.0, sin(a) * r)
if _platz_frei(p, groesse):
return p
return Vector3(0, 0, r)
## Harte Bedingungen: nicht auf dem Dorfplatz, nicht in einem anderen Gebäude,
## nicht auf einem Weg.
static func _platz_frei(p: Vector3, groesse: Vector2) -> bool:
if p.length() - maxf(groesse.x, groesse.y) * 0.5 < PLATZ_FREI:
return false
for key in PLACES:
var s: Array = PLACES[key]["size"]
if float(s[0]) <= 0.0:
continue
var c := Orte.center(PLACES, key)
var dx: float = absf(p.x - c.x) - (groesse.x + float(s[0])) * 0.5
var dz: float = absf(p.z - c.z) - (groesse.y + float(s[1])) * 0.5
if dx < HAUS_ABSTAND and dz < HAUS_ABSTAND:
return false
for k in NETZ.kanten:
if Netz.trifft_rechteck(NETZ.knoten[k[0]], NETZ.knoten[k[1]], p, groesse, WEG_ABSTAND):
return false
return true
## Wie gut liegt dieser Punkt? Nachbarschaft, Weganschluss, Nähe zum Platz.
static func _interesse(p: Vector3) -> float:
var sozial := 0.0
var n := 0
for key in PLACES:
if not key.begins_with("haus_"):
continue
var d := Orte.center(PLACES, key).distance_to(p)
sozial += _glocke(d, 16.0, 21.0, 46.0)
n += 1
if n > 0:
sozial = sozial / sqrt(float(n))
var an := NETZ.andocken(p)
var weg := _glocke(float(an["dist"]), 3.0, 9.0, 34.0)
var platz := _glocke(p.length(), PLATZ_FREI - 4.0, PLATZ_FREI + 6.0, 110.0)
return 1.0 * sozial + 1.3 * weg + 0.5 * platz + 0.8 * _freiraum(p)
## Richtungen, in denen schon viel steht, sind weniger interessant. Ohne das
## wächst das Dorf einseitig davon und die halbe Insel bleibt leer.
static func _freiraum(p: Vector3) -> float:
var w := atan2(p.z, p.x)
var nah := 0.0
var alle := 0.0
for key in PLACES:
if not key.begins_with("haus_"):
continue
var c := Orte.center(PLACES, key)
alle += 1.0
if absf(angle_difference(atan2(c.z, c.x), w)) < PI / 3.0:
nah += 1.0
if alle < 1.0:
return 1.0
return clampf(1.0 - 3.0 * nah / alle, 0.0, 1.0)
## Dreiecksglocke: 0 unter lo, 1 bei spitze, 0 über hi.
static func _glocke(d: float, lo: float, spitze: float, hi: float) -> float:
if d <= lo or d >= hi:
return 0.0
if d < spitze:
return (d - lo) / maxf(0.001, spitze - lo)
return (hi - d) / maxf(0.001, hi - spitze)
## Die Tür zeigt auf die Seite, die dem Wegenetz am nächsten liegt.
static func _tuer_zum_netz(p: Vector3, groesse: Vector2) -> String:
var ziel: Vector3 = NETZ.andocken(p)["punkt"]
var d := ziel - p
if absf(d.x) * groesse.y > absf(d.z) * groesse.x:
return "left" if d.x < 0.0 else "right"
return "forward" if d.z < 0.0 else "back"
# ------------------------------------------------------------ Weganschluss
## Verbindet ein neues Haus mit dem Netz. Der Weg geht um Häuser herum, statt
## durch sie hindurch. Weil er am nächsten Punkt des Netzes andockt und nicht
## in der Dorfmitte, gabeln sich die Wege und werden verwinkelt.
static func _anschliessen(key: String) -> void:
var tuer := Orte.door(PLACES, key)
var an := NETZ.andocken(tuer)
var von: Vector3 = an["punkt"]
var linie := _route_um_haeuser(tuer, von, key)
var vorher := -1
for i in range(linie.size()):
var idx := NETZ.fuege_knoten(linie[i])
if vorher >= 0:
NETZ.verbinde(vorher, idx)
vorher = idx
if i == 0:
ANKER[key] = idx
if vorher >= 0:
NETZ.verbinde(vorher, int(an["knoten"]))
if not ANKER.has(key):
ANKER[key] = NETZ.fuege_knoten(tuer)
_schleife(int(ANKER[key]), key)
## Weg von a nach b, der kein Haus schneidet. Frei? Dann die Gerade. Sonst
## Dijkstra über ein grobes Raster, danach auf wenige Knicke geglättet.
static func _route_um_haeuser(a: Vector3, b: Vector3, ausser: String) -> Array:
if _weg_frei(a, b, ausser):
return [a, b]
var rand := 30.0
var min_x := minf(a.x, b.x) - rand
var max_x := maxf(a.x, b.x) + rand
var min_z := minf(a.z, b.z) - rand
var max_z := maxf(a.z, b.z) + rand
var schritt := 5.0
var nx := int((max_x - min_x) / schritt) + 1
var nz := int((max_z - min_z) / schritt) + 1
while nx * nz > 2600:
schritt *= 1.5
nx = int((max_x - min_x) / schritt) + 1
nz = int((max_z - min_z) / schritt) + 1
var start := Vector2i(clampi(int(round((a.x - min_x) / schritt)), 0, nx - 1),
clampi(int(round((a.z - min_z) / schritt)), 0, nz - 1))
var ziel := Vector2i(clampi(int(round((b.x - min_x) / schritt)), 0, nx - 1),
clampi(int(round((b.z - min_z) / schritt)), 0, nz - 1))
var kosten: Dictionary = {start: 0.0}
var her: Dictionary = {}
var offen: Array = [start]
var gefunden := false
while not offen.is_empty():
var best := 0
for i in range(1, offen.size()):
if float(kosten[offen[i]]) < float(kosten[offen[best]]):
best = i
var c: Vector2i = offen[best]
offen.remove_at(best)
if c == ziel:
gefunden = true
break
var pa := Vector3(min_x + float(c.x) * schritt, 0.0, min_z + float(c.y) * schritt)
for dx in [-1, 0, 1]:
for dz in [-1, 0, 1]:
if dx == 0 and dz == 0:
continue
var n := Vector2i(c.x + dx, c.y + dz)
if n.x < 0 or n.y < 0 or n.x >= nx or n.y >= nz:
continue
var pb := Vector3(min_x + float(n.x) * schritt, 0.0,
min_z + float(n.y) * schritt)
if not _weg_frei(pa, pb, ausser):
continue
var neu: float = float(kosten[c]) + pa.distance_to(pb)
if not kosten.has(n) or neu < float(kosten[n]) - 0.001:
kosten[n] = neu
her[n] = c
if not offen.has(n):
offen.append(n)
if not gefunden:
return [a, b]
var roh: Array = []
var cur: Vector2i = ziel
while cur != start:
roh.push_front(Vector3(min_x + float(cur.x) * schritt, 0.0,
min_z + float(cur.y) * schritt))
cur = her[cur]
roh.push_front(a)
roh.append(b)
return _glaetten(roh, ausser)
## Zieht die Rasterspur auf wenige Knicke zusammen: immer so weit springen,
## wie die Sicht frei ist.
static func _glaetten(linie: Array, ausser: String) -> Array:
var out: Array = [linie[0]]
var i := 0
while i < linie.size() - 1:
var j := linie.size() - 1
while j > i + 1 and not _weg_frei(linie[i], linie[j], ausser):
j -= 1
out.append(linie[j])
i = j
return out
## Ein Umweg über das halbe Dorf ist unschön. Liegt ein Knoten nah, ist aber im
## Netz weit weg, wird abgekürzt. Daraus entstehen Schleifen statt Sackgassen.
static func _schleife(neu: int, key: String) -> void:
var p: Vector3 = NETZ.knoten[neu]
var bester := -1
var beste := INF
for i in range(NETZ.knoten.size()):
if i == neu:
continue
var d := NETZ.knoten[i].distance_to(p)
if d > 52.0 or d < 11.0 or d >= beste:
continue
if NETZ.weglaenge(neu, i) < d * 1.9:
continue
if not _weg_frei(p, NETZ.knoten[i], key):
continue
beste = d
bester = i
if bester >= 0:
NETZ.verbinde(neu, bester)
## Kein Weg darf durch ein Gebäude laufen -- auch nicht durch das eigene.
## Am eigenen Haus reicht knappe Luft, sonst käme die Tür nie frei.
static func _weg_frei(a: Vector3, b: Vector3, ausser: String) -> bool:
for key in PLACES:
var s: Array = PLACES[key]["size"]
if float(s[0]) <= 0.0:
continue
var c := Orte.center(PLACES, key)
var rand := 0.2 if key == ausser else 1.0
if Netz.trifft_rechteck(a, b, c, Vector2(float(s[0]), float(s[1])), rand):
return false
return true
# ------------------------------------------------------------ Maße
## Wie weit reicht das Dorf hinaus?
static func aussenradius() -> float:
var r := 0.0
for key in PLACES:
var s: Array = PLACES[key]["size"]
var c := Orte.center(PLACES, key)
var ecke := Vector2(absf(c.x) + float(s[0]) * 0.5, absf(c.z) + float(s[1]) * 0.5)
r = maxf(r, ecke.length())
for k in NETZ.knoten:
r = maxf(r, k.length() + 2.0)
return r
# ------------------------------------------------------------ Bewohner
static func _bewohner(i: int) -> Dictionary:
var haus := haus_key(i)
var ablauf: Array = []
@@ -120,7 +423,7 @@ static func _name(i: int) -> String:
return str(SILBE_A[j % SILBE_A.size()]) + str(SILBE_B[(j / SILBE_A.size()) % SILBE_B.size()])
## Genitiv: "Ailens", aber "Morrs" und "Esas'" — Apostroph nur nach Zischlaut.
## Genitiv: "Ailens", aber "Esas'" -- Apostroph nur nach Zischlaut.
static func _besitz(n: String) -> String:
var letzter := n.substr(n.length() - 1).to_lower()
return n + "'" if ["s", "z", "x", "ß"].has(letzter) else n + "s"

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"],
}

View File

@@ -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()])

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
uid://dsaev7wupa8ex

View File

@@ -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)

View File

@@ -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:

View File

@@ -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: