This commit is contained in:
Team3
2026-08-30 19:56:47 +02:00
parent a46eba4145
commit f936dd2b2a
13 changed files with 1681 additions and 444 deletions

View File

@@ -20,6 +20,7 @@ var _kopf: MeshInstance3D
var _aura: MeshInstance3D
var _strahl: MeshInstance3D
var _path: Array = []
var _letzte_aktivitaet: String = ""
var _bob: float = 0.0
var _offset: Vector3
@@ -59,6 +60,7 @@ func setup(d: Dictionary, index: int, orte: Dictionary) -> void:
var start := _routine_at(Game.time_h)
place = start[0]
activity = start[1]
_letzte_aktivitaet = activity
global_position = Orte.center(places, place) + _offset
@@ -108,6 +110,10 @@ 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))
if activity != _letzte_aktivitaet:
_letzte_aktivitaet = activity
_regel_pruefen()
var erledigt: bool = mit_mission and Game.completed.has(data["id"])
var aktiv: bool = mit_mission and Game.active_mission == data["id"]
_aura.visible = aktiv or erledigt
@@ -138,6 +144,20 @@ func beschriftung() -> String:
return s
## Wechselt der Mensch die Tätigkeit, greift das Regelwerk der Mission von allein.
func _regel_pruefen() -> void:
if not mit_mission:
return
var res := Game.regel_ausloesen(data["id"], activity, resist_left > 0.0)
if res.is_empty():
return
var t := TechDB.by_id(str(res["tech"]))
if res["success"]:
reagiere(true, "%s\n+%d %%" % [t["name"], int(round(float(res["gain"])))])
else:
reagiere(false, "%s prallt ab" % t["name"])
## Sichtbare Reaktion auf eine angewandte Technik.
func reagiere(success: bool, text: String) -> void:
var l := Prims.label(self, Vector3(0, 3.5, 0), text, 0.34,

View File

@@ -1,25 +1,23 @@
class_name DreamDB
## Die Traumwelt: ein Dorf. Jedes Wesen hat ein Haus und einen Tagesablauf.
## 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.
static var PLACES: Dictionary = {
"haus_ailen": {
"name": "Ailens Webstube", "pos": [-28.0, -22.0], "size": [12.0, 10.0],
"dir": "back", "color": Color(0.72, 0.62, 0.92),
},
"haus_morr": {
"name": "Morrs Kammer", "pos": [28.0, -22.0], "size": [12.0, 10.0],
"dir": "back", "color": Color(0.58, 0.72, 0.90),
},
"haus_ivo": {
"name": "Ivos Unterstand", "pos": [-28.0, 22.0], "size": [12.0, 10.0],
"dir": "forward", "color": Color(0.88, 0.66, 0.80),
},
"haus_sela": {
"name": "Selas Turmstube", "pos": [28.0, 22.0], "size": [12.0, 10.0],
"dir": "forward", "color": Color(0.62, 0.82, 0.88),
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]
static var FEST: Dictionary = {
"hoersaal": {
"name": "Hörsaal der Wege", "pos": [0.0, 34.0], "size": [16.0, 12.0],
"dir": "forward", "color": Color(0.66, 0.72, 0.94),
},
"lichtung": {
"name": "Die Lichtung", "pos": [0.0, 2.0], "size": [0.0, 0.0],
"name": "Der Dorfplatz", "pos": [0.0, 2.0], "size": [0.0, 0.0],
"dir": "none", "color": Color(0.60, 0.50, 0.85),
},
"tor": {
@@ -28,73 +26,145 @@ static var PLACES: Dictionary = {
},
}
static var LIST: Array = [
{
"id": "ailen",
"name": "Ailen",
"rolle": "Traumweberin",
"leuchtet": true,
"farbe": Color(0.78, 0.55, 0.95),
"routine": [
[0, "haus_ailen", "webt"],
[6, "lichtung", "ruht"],
[8, "haus_ailen", "webt"],
[14, "lichtung", "singt"],
[16, "haus_ailen", "webt"],
[20, "haus_ailen", "traeumt"],
],
},
{
"id": "morr",
"name": "Morr",
"rolle": "Erinnerungssammler",
"leuchtet": true,
"farbe": Color(0.45, 0.70, 0.95),
"routine": [
[0, "haus_morr", "traeumt"],
[5, "lichtung", "sammelt"],
[9, "haus_morr", "sortiert"],
[15, "lichtung", "sammelt"],
[19, "haus_morr", "sortiert"],
[22, "haus_morr", "traeumt"],
],
},
{
"id": "ivo",
"name": "Ivo",
"rolle": "Bote",
"leuchtet": true,
"farbe": Color(0.95, 0.60, 0.75),
"routine": [
[0, "haus_ivo", "traeumt"],
[4, "tor", "beobachtet"],
[8, "lichtung", "wandert"],
[10, "haus_ivo", "ruht"],
[13, "tor", "beobachtet"],
[18, "lichtung", "singt"],
[21, "haus_ivo", "traeumt"],
],
},
{
"id": "sela",
"name": "Sela",
"rolle": "Wächterin des Tors",
"leuchtet": true,
"farbe": Color(0.55, 0.95, 0.90),
"routine": [
[0, "tor", "wacht"],
[7, "lichtung", "ruht"],
[9, "tor", "wacht"],
[17, "haus_sela", "ruht"],
[19, "lichtung", "singt"],
[21, "haus_sela", "traeumt"],
],
},
## Wird von erzeuge() gefüllt und wächst mit dem Dorf.
static var PLACES: Dictionary = FEST.duplicate(true)
static var LIST: Array = []
static var FESTE_NAMEN: Array = ["Ailen", "Morr", "Ivo", "Sela"]
static var FESTE_ROLLEN: Array = ["Traumweberin", "Erinnerungssammler", "Bote",
"Wächterin des Tors"]
static var SILBE_A: Array = ["Nu", "Ta", "Es", "Wre", "O", "Ma", "Si", "Te"]
static var SILBE_B: Array = ["un", "lia", "kar", "n", "vin", "lu", "rin", "nne"]
static var ROLLEN: Array = ["Lichtgießer", "Kartenzeichnerin", "Schwellenhüter",
"Fadensucherin", "Stillenzähler", "Wegbereiterin", "Klangsammler", "Nachtgärtnerin"]
## Vier Tagesabläufe, alle mit reichlich Zeit zwischen den Stationen —
## auch ein Haus am äußeren Ring ist so bequem zu erreichen.
static var ABLAEUFE: Array = [
[[0, "haus", "traeumt"], [6, "lichtung", "ruht"], [10, "haus", "webt"],
[14, "lichtung", "singt"], [18, "haus", "sortiert"], [21, "haus", "traeumt"]],
[[0, "haus", "traeumt"], [6, "lichtung", "sammelt"], [10, "haus", "sortiert"],
[15, "tor", "beobachtet"], [19, "haus", "webt"], [21, "haus", "traeumt"]],
[[0, "tor", "wacht"], [7, "lichtung", "ruht"], [11, "haus", "sortiert"],
[15, "tor", "wacht"], [19, "lichtung", "singt"], [21, "haus", "traeumt"]],
[[0, "haus", "traeumt"], [4, "tor", "beobachtet"], [8, "lichtung", "wandert"],
[12, "haus", "ruht"], [16, "lichtung", "singt"], [20, "haus", "traeumt"]],
]
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.
static func erzeuge(haeuser: int) -> void:
PLACES = FEST.duplicate(true)
LIST = []
for i in range(maxi(haeuser, 0)):
PLACES[haus_key(i)] = _haus(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
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
var breite := 12.0 + float(i % 2) * 2.0
var tiefe := 10.0 + float(i % 3)
return {
"name": "%s Haus" % _besitz(_name(i)),
"pos": [x, z],
"size": [breite, tiefe],
"dir": _tuer_zur_mitte(x, z),
"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"
static func _bewohner(i: int) -> Dictionary:
var haus := haus_key(i)
var ablauf: Array = []
for e in ABLAEUFE[i % ABLAEUFE.size()]:
ablauf.append([e[0], haus if e[1] == "haus" else e[1], e[2]])
return {
"id": "wesen_%d" % i,
"name": _name(i),
"rolle": _rolle(i),
"haus": haus,
"leuchtet": true,
"farbe": Color.from_hsv(fmod(0.72 + float(i) * 0.161, 1.0), 0.55, 0.95),
"routine": ablauf,
}
static func _name(i: int) -> String:
if i < FESTE_NAMEN.size():
return str(FESTE_NAMEN[i])
var j := i - FESTE_NAMEN.size()
return str(SILBE_A[j % SILBE_A.size()]) + str(SILBE_B[(j / SILBE_A.size()) % SILBE_B.size()])
## Genitiv: "Ailens", aber "Morrs" und "Esas'" — Apostroph nur nach Zischlaut.
static func _besitz(n: String) -> String:
var letzter := n.substr(n.length() - 1).to_lower()
return n + "'" if ["s", "z", "x", "ß"].has(letzter) else n + "s"
static func _rolle(i: int) -> String:
if i < FESTE_ROLLEN.size():
return str(FESTE_ROLLEN[i])
return str(ROLLEN[(i - FESTE_ROLLEN.size()) % ROLLEN.size()])
static func by_id(id: String) -> Dictionary:
for w in LIST:
if w["id"] == id:
return w
return {}
# ------------------------------------------------------------------ Lehrorte
## Der Hörsaal lehrt den ersten Weg. Jedes Haus bringt einen weiteren mit.
static func techniken_von(place_key: String) -> Array:
var nr := -1
if place_key == "hoersaal":
nr = 0
elif place_key.begins_with("haus_"):
nr = int(place_key.substr(5)) + 1
if nr < 0 or nr >= TechDB.LIST.size():
return []
return [TechDB.LIST[nr]["id"]]
## An welchem Ort wird diese Technik gelehrt? "" wenn nirgends.
static func ort_von_technik(tech_id: String) -> String:
for i in range(TechDB.LIST.size()):
if TechDB.LIST[i]["id"] == tech_id:
return "hoersaal" if i == 0 else haus_key(i - 1)
return ""
## Alle Orte, an denen im aktuellen Dorf gelehrt wird.
static func lehrorte() -> Array:
var out: Array = ["hoersaal"]
for i in range(LIST.size()):
if not techniken_von(haus_key(i)).is_empty():
out.append(haus_key(i))
return out

View File

@@ -13,6 +13,8 @@ func _ready() -> void:
_environment()
_insel()
_haeuser()
_lehrpulte()
_schwarzes_brett()
_lichtung()
_tor_zur_erde()
_wesen()
@@ -52,9 +54,10 @@ func _environment() -> void:
func _insel() -> void:
Prims.cyl(self, Vector3(0, -1.03, 0), 52.0, 2.0, Color(0.48, 0.40, 0.64))
Prims.cyl(self, Vector3(0, -4.0, 0), 40.0, 4.0, Color(0.34, 0.26, 0.50), false)
Prims.cyl(self, Vector3(0, -9.0, 0), 22.0, 6.0, Color(0.24, 0.17, 0.38), false)
var radius := Game.insel_radius()
Prims.cyl(self, Vector3(0, -1.03, 0), radius, 2.0, Color(0.48, 0.40, 0.64))
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):
@@ -67,7 +70,7 @@ func _insel() -> void:
rng.seed = 7
for i in range(34):
var a := rng.randf() * TAU
var r := rng.randf_range(18.0, 48.0)
var r := rng.randf_range(18.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)
@@ -80,7 +83,7 @@ func _haeuser() -> void:
for key in DreamDB.PLACES:
var pl: Dictionary = DreamDB.PLACES[key]
var s: Array = pl["size"]
if s[0] <= 0.0:
if s[0] <= 0.0 or not Game.ort_offen(key):
continue
var c := Orte.center(DreamDB.PLACES, key)
var groesse := Vector3(s[0], 3.2, s[1])
@@ -106,33 +109,103 @@ func _haeuser() -> void:
func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
if key == "hoersaal":
_hoersaal_einrichten(c, groesse, farbe, key)
return
var w := groesse.x * 0.5 - 1.8
var h := groesse.z * 0.5 - 1.8
# Schlafnest hat jedes Haus
Prims.cyl(self, c + Vector3(-w, 0.3, -h), 1.4, 0.5, farbe.darkened(0.2), false)
match key:
"haus_ailen": # Webstuhl
match int(key.substr(5)) % 4:
0: # Webstuhl
Prims.box(self, c + Vector3(w, 1.1, 0), Vector3(0.3, 2.2, 3.0), farbe.darkened(0.45))
for i in range(5):
Prims.box(self, c + Vector3(w, 1.1, -1.2 + i * 0.6), Vector3(0.1, 1.8, 0.06),
Color(0.9, 0.85, 1.0), false, 1.4)
"haus_morr": # Regale voller Fundstücke
1: # Regale voller Fundstücke
for i in range(3):
Prims.box(self, c + Vector3(w, 0.6 + i * 0.7, 0), Vector3(1.2, 0.12, 4.0),
farbe.darkened(0.45))
for j in range(4):
Prims.box(self, c + Vector3(w, 0.85 + i * 0.7, -1.5 + j * 1.0),
Vector3(0.4, 0.4, 0.4), Color(0.8, 0.9, 1.0), false, 0.8)
"haus_ivo": # Karten und ein Fernrohr
2: # Karten und ein Fernrohr
Prims.box(self, c + Vector3(w, 0.75, 0), Vector3(1.6, 0.12, 2.6), farbe.darkened(0.45))
var rohr := Prims.cyl(self, c + Vector3(w, 1.6, 0), 0.18, 2.0,
Color(0.85, 0.9, 1.0), false, 0.6)
rohr.rotation.x = 0.9
"haus_sela": # Schale mit Licht
_: # Schale mit Licht
Prims.cyl(self, c + Vector3(w, 0.5, 0), 1.0, 1.0, farbe.darkened(0.4), false)
Prims.sphere(self, c + Vector3(w, 1.4, 0), 0.55, Color(0.8, 1.0, 1.0), 2.0)
## Bankreihen mit Mittelgang, Tafel an der Rückwand.
func _hoersaal_einrichten(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
var richtung: Vector3 = Orte.DIR_VEC[DreamDB.PLACES[key]["dir"]] * -1.0
var quer := absf(richtung.z) > 0.0
var bank := ((groesse.x if quer else groesse.z) - 5.0) * 0.5
var laengs: Vector3 = Vector3(1, 0, 0) if quer else Vector3(0, 0, 1)
for i in range(3):
var tief := -3.6 + float(i) * 1.8
for sx: float in [-1.0, 1.0]:
var mitte: Vector3 = c + richtung * tief + laengs * (sx * (bank * 0.5 + 1.0))
var sitz: Vector3 = Vector3(bank, 0.25, 0.9) if quer else Vector3(0.9, 0.25, bank)
var lehne: Vector3 = Vector3(bank, 0.9, 0.15) if quer else Vector3(0.15, 0.9, bank)
Prims.box(self, mitte + Vector3(0, 0.5, 0), sitz, farbe.darkened(0.35))
Prims.box(self, mitte + richtung * 0.7 + Vector3(0, 0.9, 0), lehne,
farbe.darkened(0.45))
var tafel: Vector3 = Vector3(groesse.x - 5.0, 2.0, 0.15) if quer \
else Vector3(0.15, 2.0, groesse.z - 5.0)
Prims.box(self, c + richtung * 4.4 + Vector3(0, 1.4, 0), tafel, Color(0.16, 0.18, 0.30))
## Vorn in jedem offenen Lehrort steht ein Pult.
func _lehrpulte() -> void:
for key in DreamDB.lehrorte():
if not Game.ort_offen(key) or DreamDB.techniken_von(key).is_empty():
continue
var pl: Dictionary = DreamDB.PLACES[key]
var tief: float = float(pl["size"][1])
var innen: Vector3 = Orte.DIR_VEC[pl["dir"]] * -1.0
var c := Orte.center(DreamDB.PLACES, key)
var pos := c + innen * (tief * 0.5 - 2.6)
var farbe: Color = pl["color"]
Prims.box(self, pos + Vector3(0, 0.55, 0), Vector3(2.4, 1.1, 0.9), farbe.darkened(0.35))
Prims.box(self, pos + Vector3(0, 1.18, 0), Vector3(2.8, 0.16, 1.2), farbe.darkened(0.2))
Prims.sphere(self, pos + Vector3(0, 1.7, 0), 0.34, Color(0.85, 0.92, 1.0), 2.0)
var lehrt: Array = DreamDB.techniken_von(key)
Prims.label(self, pos + Vector3(0, 2.6, 0),
TechDB.by_id(str(lehrt[0]))["name"], 0.42, Color(0.85, 0.92, 1))
var n := _marke(pos - innen * 1.8 + Vector3(0, 1.4, 0), "lehre", str(pl["name"]))
n.set_meta("arg", key)
## An der Lichtung: hier hängen die Menschen aus, die Hilfe brauchen.
func _schwarzes_brett() -> void:
var pos := Orte.center(DreamDB.PLACES, "lichtung") + Vector3(-6.0, 0, 9.0)
for s: float in [-1.4, 1.4]:
Prims.cyl(self, pos + Vector3(s, 1.3, 0), 0.16, 2.6, Color(0.45, 0.38, 0.60))
Prims.box(self, pos + Vector3(0, 2.2, 0), Vector3(3.6, 2.4, 0.18), Color(0.18, 0.16, 0.26))
for i in range(HumanDB.LIST.size()):
var x := -1.2 + float(i % 2) * 2.4
var y := 2.8 - float(i / 2) * 1.1
Prims.box(self, pos + Vector3(x, y, 0.12), Vector3(1.1, 0.8, 0.04),
Color(0.92, 0.90, 0.80), false, 0.5)
Prims.label(self, pos + Vector3(0, 4.2, 0), "Schwarzes Brett", 0.5, Color(1, 0.95, 0.85))
_marke(pos + Vector3(0, 1.6, 1.2), "brett", "Schwarzes Brett")
## Unsichtbarer Punkt, an dem die Interaktion hängt.
func _marke(pos: Vector3, kind: String, titel: String) -> Node3D:
var n := Node3D.new()
n.position = pos
add_child(n)
n.add_to_group("interactable")
n.set_meta("kind", kind)
n.set_meta("arg", "")
n.set_meta("titel", titel)
return n
func _lichtung() -> void:
var c := Orte.center(DreamDB.PLACES, "lichtung")
Prims.cyl(self, c + Vector3(0, 0.04, 0), 11.0, 0.12, Color(0.60, 0.52, 0.78), false)
@@ -142,13 +215,15 @@ func _lichtung() -> void:
var p := c + Vector3(cos(a) * 8.5, 0, sin(a) * 8.5)
Prims.cyl(self, p + Vector3(0, 1.6, 0), 0.28, 3.2, Color(0.72, 0.64, 0.88))
Prims.sphere(self, p + Vector3(0, 3.6, 0), 0.42, Color(0.75, 0.85, 1.0), 2.0)
Prims.label(self, c + Vector3(0, 5.2, 0), DreamDB.PLACES["lichtung"]["name"], 0.6,
Prims.label(self, c + Vector3(0, 5.2, 0), str(DreamDB.PLACES["lichtung"]["name"]), 0.6,
Color(1, 0.95, 0.85))
func _tor_zur_erde() -> void:
var pos := Orte.center(DreamDB.PLACES, "tor")
Prims.cyl(self, pos + Vector3(0, 0.3, 0), 5.5, 0.6, Color(0.35, 0.30, 0.50))
# flach und ohne Kollision wie die Lichtung — eine 0,6 hohe Stufe hätte den
# Spieler vor dem Tor stehen lassen
Prims.cyl(self, pos + Vector3(0, 0.05, 0), 5.5, 0.1, Color(0.35, 0.30, 0.50), false)
for i in range(10):
var a := PI * float(i) / 9.0
Prims.box(self, pos + Vector3(cos(a) * 4.2, sin(a) * 4.2 + 0.6, 0),
@@ -158,17 +233,14 @@ func _tor_zur_erde() -> void:
scheibe.rotation.x = PI * 0.5
Prims.label(self, pos + Vector3(0, 6.4, 0), "Tor zur Erde", 0.8, Color(0.7, 0.95, 1))
var n := Node3D.new()
n.position = pos + Vector3(0, 1.6, 0)
add_child(n)
n.add_to_group("interactable")
n.set_meta("kind", "erde")
n.set_meta("arg", "")
n.set_meta("titel", "Tor zur Erde")
_marke(pos + Vector3(0, 1.6, 0), "erde", "Tor zur Erde")
## Am Anfang ist das Dorf leer; mit jedem erfüllten Traum zieht ein Wesen ein.
func _wesen() -> void:
for i in range(DreamDB.LIST.size()):
if not Game.wesen_eingezogen(i):
continue
var w := Bewohner.new()
add_child(w)
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES)

View File

@@ -44,7 +44,14 @@ func _environment() -> void:
func _boden() -> void:
Prims.box(self, Vector3(0, -0.53, 0), Vector3(240, 1, 240), Color(0.36, 0.52, 0.30))
var weite := 240.0
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
@@ -54,9 +61,9 @@ func _boden() -> void:
func _orte() -> void:
for key in HumanDB.PLACES:
if key == "park":
continue
var pl: Dictionary = HumanDB.PLACES[key]
if float(pl["size"][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])
@@ -84,7 +91,7 @@ func _orte() -> void:
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("haus") or key == "wohnheim":
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),
@@ -110,6 +117,12 @@ func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> vo
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))
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)
func _park() -> void:

View File

@@ -15,6 +15,7 @@ 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 pipeline: Dictionary = {} # human_id -> {activity -> tech_id}
var completed: Array = []
var time_h: float = 7.0
var day: int = 1
@@ -23,6 +24,7 @@ var teleport_lock: float = 0.0
func _ready() -> void:
_setup_input()
welt_erzeugen()
func _setup_input() -> void:
@@ -50,6 +52,15 @@ func advance_time(delta: float) -> void:
while time_h >= 24.0:
time_h -= 24.0
day += 1
_gewoehnung_klingt_ab()
## Abstand macht eine Technik wieder wirksam: über Nacht verfällt die Hälfte
## der Gewöhnung. Wer täglich denselben Weg geht, hält ihn brauchbar — wer ihn
## an einem Tag immer wieder auffährt, stumpft ihn ab.
func _gewoehnung_klingt_ab() -> void:
for key in used:
used[key] = int(used[key]) / 2
func time_string() -> String:
@@ -58,6 +69,54 @@ func time_string() -> String:
return "Tag %d · %02d:%02d" % [day, h, m]
# ------------------------------------------------------------ Wachsende Welt
# Das Traumdorf wird erzeugt. Fest sind nur Hörsaal, Dorfplatz und Tor.
# Jeder erfüllte Traum bringt einen Bewohner mit seinem Haus, und jedes Haus
# lehrt eine Technik, die es vorher nirgends gab.
const INSEL_GRUND := 46.0
const OFFENE_MISSIONEN := 3
func haeuser() -> int:
return completed.size()
func insel_radius() -> float:
var ring := DreamDB.aeusserster_ring(haeuser())
return maxf(INSEL_GRUND, ring + 14.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)
## Das Traumdorf beginnt leer. Mit jedem erfüllten Traum zieht ein Wesen ein.
func wesen_eingezogen(index: int) -> bool:
return index < haeuser()
## Steht dieser Ort schon auf der Insel? Erzeugt wird nur, was schon da ist.
func ort_offen(place_key: String) -> bool:
return DreamDB.PLACES.has(place_key)
## Ist die Technik überhaupt schon irgendwo zu finden?
func technik_offen(tech_id: String) -> bool:
var ort := DreamDB.ort_von_technik(tech_id)
return ort != "" and ort_offen(ort)
func offene_techniken() -> int:
var n := 0
for t in TechDB.LIST:
if technik_offen(t["id"]):
n += 1
return n
# ---------------------------------------------------------------- Fertigkeiten
func knows(tech_id: String) -> bool:
@@ -86,6 +145,10 @@ func xp_to_next(tech_id: String) -> String:
func learn(tech_id: String) -> bool:
if known.has(tech_id):
return false
if not technik_offen(tech_id):
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
@@ -136,12 +199,107 @@ func accept_mission(human_id: String) -> bool:
return true
## Angenommen ist, wer schon einmal im Auftragsbuch stand.
func angenommen(human_id: String) -> bool:
return progress.has(human_id)
func mission_progress(human_id: String) -> float:
return progress.get(human_id, 0.0)
## Ein erfüllter Traum lässt das Dorf wachsen: ein Wesen zieht mit seinem
## Haus ein, und in diesem Haus wird ein neuer Weg gelehrt.
func _insel_waechst() -> void:
welt_erzeugen()
var i := haeuser() - 1
if i < 0 or i >= DreamDB.LIST.size():
return
var w: Dictionary = DreamDB.LIST[i]
notified.emit("%s, %s, ist ins Traumdorf gezogen." % [w["name"], w["rolle"]],
Color(0.85, 0.75, 1))
var neu: Array = DreamDB.techniken_von(DreamDB.haus_key(i))
if neu.is_empty():
return
notified.emit("In %s wird ein neuer Weg gelehrt: %s." %
[DreamDB.PLACES[DreamDB.haus_key(i)]["name"], TechDB.by_id(str(neu[0]))["name"]],
Color(0.75, 0.95, 1))
# ------------------------------------------------------------------- Regelwerk
# Eine Regel ist ein Wenn-Dann: tut der Mensch etwas Bestimmtes, greift die
# zugeordnete Technik von allein. Damit automatisiert der Spieler den Fortschritt.
func regeln(human_id: String) -> Dictionary:
return pipeline.get(human_id, {})
func regel_fuer(human_id: String, activity: String) -> String:
return regeln(human_id).get(activity, "")
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:
for activity in regeln(human_id):
if regeln(human_id)[activity] == tech_id:
return activity
return ""
## 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.
func regel_setzen(human_id: String, activity: String, tech_id: String) -> void:
if not pipeline.has(human_id):
pipeline[human_id] = {}
var r: Dictionary = pipeline[human_id]
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?
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 {}
var res := apply_technique(tech_id, human_id, activity, resisting)
if not res["ok"]:
return {}
res["tech"] = tech_id
var t := TechDB.by_id(tech_id)
if res["success"]:
notified.emit("Regel greift — %s bei »%s« (+%d %%)" %
[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))],
Color(1, 0.7, 0.5))
return res
# ----------------------------------------------------- Technik auf Mensch anwenden
## Schlafende erreicht keine Technik — weder von Hand noch über eine Regel.
func erreichbar(activity: String) -> bool:
return activity != "schlaeft"
## Bewertet eine Technik in genau dieser Situation, ohne sie anzuwenden.
func evaluate(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary:
var t := TechDB.by_id(tech_id)
@@ -151,7 +309,7 @@ func evaluate(tech_id: String, human_id: String, activity: String, resisting: bo
"chance": 0.0, "tag": false, "moment": false, "blocked": false,
"grund": "", "skill": skill,
}
if activity == "schlaeft":
if not erreichbar(activity):
res["blocked"] = true
res["grund"] = "Sie oder er schläft. Techniken erreichen nur wache Menschen."
return res
@@ -214,6 +372,7 @@ func apply_technique(tech_id: String, human_id: String, activity: String, resist
mission_done.emit(human_id)
notified.emit("%s hat es geschafft. (+3 Einsicht)" % HumanDB.by_id(human_id)["name"],
Color(1, 0.85, 0.4))
_insel_waechst()
state_changed.emit()
return {

View File

@@ -1,9 +1,13 @@
class_name HumanDB
## Die Kleinstadt auf der Erde und die Menschen mit ihren Tagesabläufen.
## 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.
const HAEUSER_JE_RING := 8
const RING_ERSTER := 62.0
const RING_ABSTAND := 16.0
## Gebäude liegen im Ring um den Park. Alle Türen zeigen zur Mitte.
## pos = Standplatz drinnen, size = Grundfläche, dir = Richtung der Türlücke.
static var PLACES: Dictionary = {
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),
@@ -20,125 +24,184 @@ static var PLACES: Dictionary = {
"name": "Bäckerei Keller", "pos": [-31.0, 0.0], "size": [10.0, 12.0],
"dir": "right", "color": Color(0.85, 0.72, 0.50),
},
"haus_tobias": {
"name": "Wohnung Frank", "pos": [31.0, 0.0], "size": [10.0, 12.0],
"werkstatt": {
"name": "Werkstatt Loos", "pos": [31.0, 0.0], "size": [10.0, 12.0],
"dir": "left", "color": Color(0.70, 0.58, 0.55),
},
"haus_lena": {
"name": "Atelier Vogt", "pos": [-28.0, 27.0], "size": [12.0, 10.0],
"dir": "forward", "color": Color(0.66, 0.58, 0.72),
},
"haus_mira": {
"name": "Wohnheim", "pos": [0.0, 27.0], "size": [12.0, 10.0],
"dir": "forward", "color": Color(0.60, 0.68, 0.62),
},
"haus_bruno": {
"name": "Haus Keller", "pos": [28.0, 27.0], "size": [12.0, 10.0],
"dir": "forward", "color": Color(0.78, 0.66, 0.58),
},
"park": {
"name": "Stadtpark", "pos": [0.0, 4.0], "size": [0.0, 0.0],
"dir": "none", "color": Color(0.45, 0.65, 0.38),
},
}
static var LIST: Array = [
static var PLACES: Dictionary = FEST.duplicate(true)
static var LIST: Array = []
## Arbeitsplätze, die ein erzeugter Mensch bekommen kann.
static var ARBEIT: Array = ["uni", "cafe", "buero", "baeckerei", "werkstatt"]
static var VORNAMEN: Array = ["Lena", "Bruno", "Mira", "Tobias", "Halim", "Jorunn",
"Pia", "Anselm", "Ceyda", "Rune", "Malte", "Ida", "Vito", "Nour", "Greta", "Kasper"]
static var NACHNAMEN: Array = ["Vogt", "Keller", "Sahin", "Frank", "Berg", "Ostermann",
"Ruiz", "Lind", "Aslan", "Weiss", "Petit", "Novak"]
static var BERUFE: Array = ["Illustratorin", "Bäcker", "Studentin", "Sachbearbeiter",
"Pfleger", "Tischlerin", "Lehrer", "Programmiererin", "Fahrer", "Archivarin",
"Köchin", "Musiker"]
## Anliegen sind echte Klemmen, keine erzeugten Sätze — sie tragen den Sinn
## des Spiels. Erzeugt werden Person, Wohnort und Tagesablauf drumherum.
static var ANLIEGEN: Array = [
{
"id": "lena",
"name": "Lena Vogt",
"alter": 29,
"beruf": "Illustratorin",
"traum": "Ihr Bilderbuch endlich zu Ende zeichnen.",
"problem": "Sie schiebt die letzten zwölf Seiten seit acht Monaten vor sich her. Jeder Entwurf kommt ihr zu schlecht vor, also fängt sie lieber gar nicht erst an.",
"tags": ["prokrastination", "selbstzweifel", "ablenkung"],
"mission": true,
"farbe": Color(0.85, 0.45, 0.55),
"routine": [
[0, "haus_lena", "schlaeft"],
[7, "haus_lena", "morgenroutine"],
[8, "cafe", "essen"],
[10, "haus_lena", "zoegert"],
[12, "haus_lena", "arbeitet"],
[13, "park", "pause"],
[15, "haus_lena", "arbeitet"],
[18, "park", "unterwegs"],
[20, "haus_lena", "aufraeumen"],
[21, "haus_lena", "gruebelt"],
[22, "haus_lena", "abendroutine"],
[23, "haus_lena", "schlaeft"],
],
},
{
"id": "bruno",
"name": "Bruno Keller",
"alter": 54,
"beruf": "Bäcker",
"traum": "Wieder Klavier spielen wie mit zwanzig.",
"problem": "Das Klavier steht zugestellt im Flur. Nach der Frühschicht ist er zu müde, um auch nur den Deckel zu öffnen.",
"tags": ["gewohnheit", "erschoepfung", "ablenkung"],
"mission": true,
"farbe": Color(0.55, 0.45, 0.30),
"routine": [
[0, "haus_bruno", "schlaeft"],
[3, "baeckerei", "arbeitet"],
[10, "cafe", "pause"],
[12, "haus_bruno", "essen"],
[13, "haus_bruno", "ruht"],
[15, "haus_bruno", "zoegert"],
[16, "park", "unterwegs"],
[18, "haus_bruno", "muede"],
[20, "haus_bruno", "abendroutine"],
[21, "haus_bruno", "schlaeft"],
],
},
{
"id": "mira",
"name": "Mira Sahin",
"alter": 22,
"beruf": "Studentin",
"traum": "Ihre Abschlussarbeit vortragen, ohne zu erstarren.",
"traum": "Die Abschlussarbeit vortragen, ohne zu erstarren.",
"problem": "Die Arbeit ist längst fertig. Aber vor jedem Vortrag wird ihr schwarz vor Augen, und sie meldet sich wieder ab.",
"tags": ["angst", "ueberforderung", "selbstzweifel"],
"mission": true,
"farbe": Color(0.35, 0.60, 0.85),
"routine": [
[0, "haus_mira", "schlaeft"],
[8, "haus_mira", "morgenroutine"],
[9, "uni", "arbeitet"],
[13, "cafe", "angespannt"],
[15, "uni", "arbeitet"],
[17, "uni", "wartet"],
[18, "haus_mira", "gruebelt"],
[20, "park", "unterwegs"],
[21, "haus_mira", "ruht"],
[22, "haus_mira", "abendroutine"],
[23, "haus_mira", "schlaeft"],
],
},
{
"id": "tobias",
"name": "Tobias Frank",
"alter": 41,
"beruf": "Sachbearbeiter",
"traum": "Abends wieder Zeit für seine Tochter haben.",
"traum": "Abends wieder Zeit für die Tochter haben.",
"problem": "Er sagt zu jeder Bitte ja und sitzt dafür jeden Abend bis acht im Büro. Zu Hause ist er nur noch müde.",
"tags": ["stress", "grenzen", "erschoepfung"],
"mission": true,
"farbe": Color(0.40, 0.55, 0.45),
"routine": [
[0, "haus_tobias", "schlaeft"],
[6, "haus_tobias", "morgenroutine"],
[7, "buero", "arbeitet"],
[12, "cafe", "essen"],
[14, "buero", "arbeitet"],
[19, "buero", "angespannt"],
[20, "haus_tobias", "essen"],
[21, "haus_tobias", "muede"],
[23, "haus_tobias", "abendroutine"],
],
},
{
"traum": "Endlich wieder durchschlafen.",
"problem": "Der Feierabend franst aus, das Handy leuchtet bis zwei. Morgens steht er zerschlagen auf und trinkt sich wach.",
"tags": ["erschoepfung", "gesundheit", "gewohnheit"],
},
{
"traum": "Den alten Freundeskreis zurückholen.",
"problem": "Sie wartet seit Monaten, dass sich jemand meldet, und meldet sich selbst nie. Jede Einladung erscheint ihr aufdringlich.",
"tags": ["einsamkeit", "selbstzweifel"],
},
{
"traum": "Die Werkstatt in der Garage wieder aufmachen.",
"problem": "Alles liegt bereit, aber der erste Handgriff wirkt jedes Mal wie ein Berg. Am Ende räumt er lieber auf.",
"tags": ["prokrastination", "ueberforderung"],
},
{
"traum": "Vor Publikum sprechen, ohne dass die Hände zittern.",
"problem": "Schon Tage vorher liegt ihr der Termin im Magen. Kurz davor wird die Luft knapp und der Kopf leer.",
"tags": ["angst", "stress"],
},
{
"traum": "Nach dem Rückschlag noch einmal anfangen.",
"problem": "Seit die Prüfung schiefging, redet sie mit sich, wie sie mit niemandem reden würde. Jeder neue Versuch fühlt sich sinnlos an.",
"tags": ["rueckschlag", "selbstzweifel"],
},
{
"traum": "Drei Abende in der Woche für sich behalten.",
"problem": "Der Kalender füllt sich mit fremden Anliegen, weil ein Nein unhöflich wirkt. Am Ende bleibt nichts übrig.",
"tags": ["grenzen", "stress", "ueberforderung"],
},
]
## Tagesabläufe: Platzhalter werden durch Wohnung und Arbeitsplatz ersetzt.
## Tagesabläufe: Platzhalter werden durch Wohnung und Arbeitsplatz ersetzt.
## Die Reihenfolge passt zu den Anliegen — wer aufschiebt, arbeitet zu Hause.
static var ABLAEUFE: Array = [
[[0, "heim", "schlaeft"], [7, "heim", "zoegert"], [10, "heim", "arbeitet"],
[13, "park", "pause"], [15, "heim", "arbeitet"], [18, "cafe", "wartet"],
[20, "heim", "zoegert"], [22, "heim", "gruebelt"], [23, "heim", "abendroutine"]],
[[0, "heim", "schlaeft"], [4, "arbeit", "arbeitet"], [10, "cafe", "pause"],
[12, "heim", "essen"], [14, "heim", "ruht"], [16, "heim", "zoegert"],
[18, "park", "unterwegs"], [20, "heim", "muede"], [22, "heim", "abendroutine"]],
[[0, "heim", "schlaeft"], [8, "heim", "morgenroutine"], [10, "arbeit", "arbeitet"],
[13, "cafe", "angespannt"], [15, "arbeit", "wartet"], [18, "heim", "gruebelt"],
[20, "park", "unterwegs"], [22, "heim", "abendroutine"]],
[[0, "heim", "schlaeft"], [6, "heim", "morgenroutine"], [8, "arbeit", "arbeitet"],
[12, "cafe", "essen"], [14, "arbeit", "arbeitet"], [19, "arbeit", "angespannt"],
[21, "heim", "essen"], [23, "heim", "muede"]],
[[0, "heim", "schlaeft"], [7, "heim", "morgenroutine"], [9, "arbeit", "arbeitet"],
[12, "cafe", "essen"], [14, "arbeit", "arbeitet"], [17, "park", "unterwegs"],
[19, "heim", "aufraeumen"], [21, "heim", "gruebelt"], [23, "heim", "abendroutine"]],
]
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)
LIST = []
for i in range(maxi(menschen, 0)):
PLACES[heim_key(i)] = _heim(i)
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),
}
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"
static func _mensch(i: int) -> Dictionary:
var anliegen: Dictionary = ANLIEGEN[i % ANLIEGEN.size()]
var arbeit: String = str(ARBEIT[i % ARBEIT.size()])
var heim := heim_key(i)
var ablauf: Array = []
for e in ABLAEUFE[i % ABLAEUFE.size()]:
var ort: String = str(e[1])
if ort == "heim":
ort = heim
elif ort == "arbeit":
ort = arbeit
ablauf.append([e[0], ort, e[2]])
return {
"id": "mensch_%d" % i,
"name": "%s %s" % [_vorname(i), _nachname(i)],
"alter": 22 + (i * 7) % 38,
"beruf": str(BERUFE[i % BERUFE.size()]),
"traum": str(anliegen["traum"]),
"problem": str(anliegen["problem"]),
"tags": (anliegen["tags"] as Array).duplicate(),
"mission": true,
"heim": heim,
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
"routine": ablauf,
}
static func _vorname(i: int) -> String:
return str(VORNAMEN[i % VORNAMEN.size()])
static func _nachname(i: int) -> String:
return str(NACHNAMEN[(i / VORNAMEN.size() + i) % NACHNAMEN.size()])
## Die Tätigkeiten eines Menschen in der Reihenfolge seines Tages, ohne Dopplung.
static func aktivitaeten(id: String) -> Array:
var out: Array = []
for e in by_id(id)["routine"]:
if not out.has(e[2]):
out.append(e[2])
return out
static func by_id(id: String) -> Dictionary:
for h in LIST:

View File

@@ -31,7 +31,7 @@ func _ready() -> void:
Color(0.8, 0.95, 1)))
_traumwelt()
ui.intro_panel(func() -> void: pass)
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _traumwelt() -> void:
@@ -109,6 +109,10 @@ func _hinweis_setzen() -> void:
ui.setze_hinweis("[E] Zur Erde reisen")
"traum":
ui.setze_hinweis("[E] Zurück in die Traumwelt")
"lehre":
ui.setze_hinweis("[E] %s — Techniken lernen" % titel)
"brett":
ui.setze_hinweis("[E] Schwarzes Brett — Mission annehmen")
_:
ui.setze_hinweis("[E] %s" % titel)
else:
@@ -148,8 +152,13 @@ func _interagieren() -> void:
"erde":
if Game.active_mission == "" and Game.completed.size() < HumanDB.LIST.size():
Game.notified.emit(
"Das Tor öffnet sich nur mit einer angenommenen Mission.", Color(1, 0.8, 0.5))
"Das Tor öffnet sich nur mit einer Mission. Sieh am Schwarzen Brett nach.",
Color(1, 0.8, 0.5))
else:
_erde()
"traum":
_traumwelt()
"lehre":
ui.lehre_panel(str(_fokus.get_meta("arg")))
"brett":
ui.brett_panel()

View File

@@ -133,6 +133,7 @@ func _process(_delta: float) -> void:
var wunsch := _content.get_combined_minimum_size().y
var platz := get_viewport().get_visible_rect().size.y - 220.0
_scroll.custom_minimum_size.y = clampf(wunsch, 120.0, platz)
_panel.reset_size() # sonst behält der Rahmen die Höhe des Panels davor
if _scroll_frames > 0:
_scroll.scroll_vertical = _scroll_wunsch
_scroll_frames -= 1
@@ -148,13 +149,14 @@ func _hud_aktualisieren() -> void:
[Game.einsicht, Game.known.size(), TechDB.LIST.size()])
if Game.active_mission != "":
var h := HumanDB.by_id(Game.active_mission)
zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %%" %
[h["name"], int(Game.mission_progress(Game.active_mission))])
zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %% · %d Regeln" %
[h["name"], int(Game.mission_progress(Game.active_mission)),
Game.regel_anzahl(Game.active_mission)])
zeilen.append("[color=#aaaaaa]%s[/color]" % h["traum"])
elif Game.completed.size() >= HumanDB.LIST.size():
zeilen.append("[color=#88ff99]Alle Träume erfüllt.[/color]")
else:
zeilen.append("[color=#aaaaaa]Keine Mission. Journal öffnen mit J.[/color]")
zeilen.append("[color=#aaaaaa]Keine Mission. Schwarzes Brett am Dorfplatz.[/color]")
_hud.text = "\n".join(zeilen)
@@ -250,8 +252,9 @@ func _hinweis_block(t: Dictionary) -> String:
# -------------------------------------------------------- Journal
## Das Journal ist immer dabei. Hier werden Techniken gelernt und vertieft
## und Missionen angenommen.
## Das Journal ist immer dabei und führt Buch: welche Techniken du gelernt hast
## und welche Missionen du angenommen hast. Gelehrt wird im Hörsaal,
## vergeben werden die Missionen am Schwarzen Brett.
func journal_panel(seite: String = "") -> void:
if seite != "":
_journal_seite = seite
@@ -271,7 +274,11 @@ func _reiter() -> void:
var hb := HBoxContainer.new()
hb.add_theme_constant_override("separation", 8)
_content.add_child(hb)
for eintrag in [["techniken", "Techniken"], ["missionen", "Missionen"]]:
var seiten := [
["techniken", "Techniken (%d)" % Game.known.size()],
["missionen", "Missionen (%d)" % Game.progress.size()],
]
for eintrag in seiten:
var b := Button.new()
b.text = eintrag[1]
b.custom_minimum_size = Vector2(0, 36)
@@ -293,10 +300,153 @@ func _reiter() -> void:
func _journal_techniken() -> void:
_text("Zwölf Wege, die Menschen wirklich weiterbringen. Jede Anwendung " +
"draußen macht eine Technik von allein besser. Vertiefen geht auch hier.", 15)
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)
return
_text("Was du gelernt hast. Jede Anwendung draußen macht eine Technik von allein " +
"besser; vertieft wird im Hörsaal.", 15)
_trenner()
for id in TechDB.ids():
if not Game.knows(id):
continue
var t := TechDB.by_id(id)
_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()
func _journal_missionen() -> void:
if Game.progress.is_empty():
_text("[color=#ffcc66]Du hast noch keine Mission angenommen.[/color]\n" +
"Am [b]Schwarzen Brett[/b] am Dorfplatz hängen die Menschen aus, " +
"die nicht weiterkommen.", 16)
return
_text("Die Menschen, für die du dich entschieden hast.", 15)
_trenner()
for h in HumanDB.LIST:
var id: String = h["id"]
if not Game.angenommen(id):
continue
_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:
_text("[color=#ffdd77]Aktive Mission — %d %%[/color]" %
int(Game.mission_progress(id)), 15)
else:
_text("[color=#aaaaaa]Zurückgestellt — %d %%[/color]" %
int(Game.mission_progress(id)), 15)
if not Game.completed.has(id):
_knopf("Regelwerk — %d Regeln" % Game.regel_anzahl(id),
func() -> void: pipeline_panel(id))
_trenner()
# -------------------------------------------------------- Regelwerk einer Mission
## Wenn-Dann-Regeln: tut der Mensch etwas Bestimmtes, greift die zugeordnete
## Technik von allein. So automatisiert der Spieler den Fortschritt.
func pipeline_panel(human_id: String) -> void:
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "pipeline" else 0
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)
_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()
for activity in HumanDB.aktivitaeten(human_id):
var tat := Orte.text(activity)
if not Game.erreichbar(activity):
_knopf("%s → unerreichbar" % tat, func() -> void: pass, false)
continue
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))]
_knopf(beschriftung, func() -> void: regel_panel(human_id, activity))
_trenner()
_knopf("Zurück zum Journal", func() -> void: journal_panel("missionen"))
_zeigen("pipeline")
_scroll_wunsch = scroll_alt
_scroll_frames = 4
## Welche Technik soll bei dieser Tätigkeit greifen?
func regel_panel(human_id: String, activity: String) -> void:
var h := HumanDB.by_id(human_id)
_leeren("Regel für »%s«" % Orte.text(activity))
if Game.known.is_empty():
_text("[color=#ffcc66]Du kennst keine Technik. Im Hörsaal der Wege " +
"werden sie gelehrt.[/color]", 16)
_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. " %
[h["name"], Orte.text(activity)] +
"Jede Technik hält nur einen Posten — sie hier einzusetzen nimmt sie anderswo weg.", 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 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))
_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))
_zeigen("regel")
# -------------------------------------------------------- Lehrorte
## Jeder Lehrort unterrichtet genau einen Weg: der Hörsaal den ersten, jedes
## Haus den, den sein Bewohner mitgebracht hat.
func lehre_panel(ort: String = "hoersaal") -> void:
if not DreamDB.PLACES.has(ort):
return
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "lehre" else 0
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)
_trenner()
if ids.is_empty():
_text("[color=#ffcc66]Hier wird nichts gelehrt.[/color]", 16)
for id in ids:
var t := TechDB.by_id(id)
if Game.knows(id):
_text("[b][color=#ffe3a0]%s[/color][/b] · Stufe %d (%s)" %
@@ -309,16 +459,38 @@ func _journal_techniken() -> void:
if Game.knows(id):
_knopf("Vertiefen (1 Einsicht)", func() -> void:
if Game.deepen(id):
journal_panel(), Game.einsicht >= 1)
lehre_panel(ort), Game.einsicht >= 1)
else:
_knopf("Lernen (1 Einsicht)", func() -> void:
if Game.learn(id):
journal_panel(), Game.einsicht >= 1)
lehre_panel(ort), Game.einsicht >= 1)
_trenner()
_naechster_weg()
_zeigen("lehre")
_scroll_wunsch = scroll_alt
_scroll_frames = 4
func _journal_missionen() -> void:
_text("Menschen, deren Traum feststeckt.", 15)
## Was als Nächstes ins Dorf käme.
func _naechster_weg() -> void:
var offen_zahl := Game.offene_techniken()
if offen_zahl >= TechDB.LIST.size():
_text("[color=#88ff99]Alle zwölf Wege sind entdeckt. Das Dorf wächst weiter.[/color]", 15)
return
var naechste: Dictionary = TechDB.LIST[offen_zahl]
_text("[color=#aaaaaa]Noch unentdeckt: [b]%s[/b] und %d weitere. Jeder erfüllte Traum " %
[naechste["name"], TechDB.LIST.size() - offen_zahl - 1] +
"bringt einen Bewohner ins Dorf, und er lehrt einen neuen Weg.[/color]", 15)
# -------------------------------------------------------- Schwarzes Brett
## Hier hängen die Menschen aus, deren Traum feststeckt.
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)
_trenner()
for h in HumanDB.LIST:
var id: String = h["id"]
@@ -333,11 +505,16 @@ func _journal_missionen() -> void:
_text("[color=#ffdd77]Aktive Mission — %d %%[/color]" %
int(Game.mission_progress(id)), 15)
else:
_text("Fortschritt: %d %%" % int(Game.mission_progress(id)), 15)
if Game.angenommen(id):
_text("[color=#aaaaaa]Schon einmal angenommen — %d %%[/color]" %
int(Game.mission_progress(id)), 15)
_knopf("Mission annehmen", func() -> void:
Game.accept_mission(id)
journal_panel())
brett_panel())
_trenner()
_zeigen("brett")
_scroll_wunsch = scroll_alt
_scroll_frames = 4
# -------------------------------------------------------- Anwenden auf der Erde
@@ -411,37 +588,6 @@ func _anwenden(id: String) -> void:
[t["name"], int(round(float(res["chance"]) * 100.0))], Color(1, 0.6, 0.5))
# -------------------------------------------------------- Intro
func intro_panel(on_close: Callable) -> void:
_leeren("Traumwandler")
_text("Du bist ein Wesen aus der Traumwelt. Menschen tragen Träume mit sich herum, " +
"die sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt.")
_trenner()
_text("[b]Dein Journal — Taste J[/b]\n" +
"· Es ist immer dabei, in beiden Welten.\n" +
"· [b]Techniken[/b] — lerne echte Lösungswege. Jede sagt dir, was sie tut, " +
"wann sie passt und warum sie hilft. Anwenden macht sie von allein besser.\n" +
"· [b]Missionen[/b] — nimm einen Menschen an, dem du helfen willst.")
_text("[b]In der Traumwelt[/b]\n" +
"· Ein Dorf auf einer schwebenden Insel. Vier Wesen wohnen hier, jedes in " +
"seinem Haus, jedes mit eigenem Tagesablauf. Die Häuser stehen offen.\n" +
"· [b]Tor zur Erde[/b] — reise hinab, sobald du eine Mission hast.")
_text("[b]Auf der Erde[/b]\n" +
"· Du bist ein unsichtbarer Geist und schwebst durch Wände. Leertaste hoch, Umschalt runter.\n" +
"· Die Menschen leben ihren Tagesablauf: schlafen, arbeiten, grübeln, spazieren.\n" +
"· Geh nah heran und drücke [b]E[/b]. Deine Erfolgschance hängt von deiner Stufe ab — " +
"und davon, ob Technik und Moment zum Menschen passen.\n" +
"· Schlafende erreichst du nicht. Wer abblockt, braucht eine Weile Ruhe.")
_trenner()
_text("[color=#aaaaaa]Du beginnst mit 3 Einsicht. Jede Technik kostet 1. " +
"Ein erfüllter Traum bringt 3 zurück.[/color]", 15)
_knopf("Losgehen", func() -> void:
schliessen()
on_close.call())
_zeigen("intro")
func _unhandled_input(event: InputEvent) -> void:
if offen and event.is_action_pressed("ui_cancel"):
schliessen()