first commit

This commit is contained in:
team3
2026-08-29 11:43:53 +02:00
commit 4d771c9a6e
36 changed files with 2932 additions and 0 deletions

254
scripts/dream_world.gd Normal file
View File

@@ -0,0 +1,254 @@
extends Node3D
class_name DreamWorld
## Die Traumwelt: Insel mit drei betretbaren Gebäuden und dem Tor zur Erde.
const INNEN_Y := -300.0
var spawn := Vector3(0, 2.0, 14.0)
var _deko: Array = []
var _regale: Dictionary = {} # tech_id -> MeshInstance3D
var _stelen: Dictionary = {} # human_id -> Dictionary
func _ready() -> void:
_environment()
_insel()
_gebaeude_bibliothek()
_gebaeude_missionshalle()
_gebaeude_meditation()
_tor_zur_erde()
Game.state_changed.connect(_refresh)
_refresh()
func _environment() -> void:
var we := WorldEnvironment.new()
var env := Environment.new()
env.background_mode = Environment.BG_SKY
var sky := Sky.new()
var mat := ProceduralSkyMaterial.new()
mat.sky_top_color = Color(0.10, 0.05, 0.24)
mat.sky_horizon_color = Color(0.42, 0.24, 0.55)
mat.ground_bottom_color = Color(0.06, 0.03, 0.14)
mat.ground_horizon_color = Color(0.32, 0.18, 0.45)
mat.sun_angle_max = 40.0
sky.sky_material = mat
env.sky = sky
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
env.ambient_light_energy = 1.8
env.glow_enabled = true
env.glow_intensity = 0.5
env.glow_bloom = 0.15
env.fog_enabled = true
env.fog_light_color = Color(0.30, 0.18, 0.45)
env.fog_density = 0.006
we.environment = env
add_child(we)
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-46, 25, 0)
sun.light_energy = 1.15
sun.light_color = Color(0.85, 0.80, 1.0)
sun.shadow_enabled = true
sun.shadow_blur = 2.0
add_child(sun)
func _insel() -> void:
Prims.cyl(self, Vector3(0, -1.0, 0), 50.0, 2.0, Color(0.28, 0.20, 0.42))
Prims.cyl(self, Vector3(0, -4.0, 0), 38.0, 4.0, Color(0.20, 0.14, 0.32), false)
Prims.cyl(self, Vector3(0, -9.0, 0), 20.0, 6.0, Color(0.15, 0.10, 0.26), false)
# Wege
var wege: Array[Vector3] = [Vector3(-22, 0, -6), Vector3(0, 0, -12), Vector3(22, 0, -6),
Vector3(0, 0, 8)]
for w in wege:
var d: Vector3 = w.normalized() * (w.length() * 0.5)
var b := Prims.box(self, Vector3(d.x, 0.02, d.z),
Vector3(3.0, 0.08, w.length()), Color(0.45, 0.35, 0.62), false)
b.rotation.y = atan2(w.x, w.z)
# schwebende Deko
var rng := RandomNumberGenerator.new()
rng.seed = 7
for i in range(34):
var a := rng.randf() * TAU
var r := rng.randf_range(16.0, 46.0)
var p := Vector3(cos(a) * r, rng.randf_range(4.0, 22.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)
var wuerfel := Prims.box(self, p, Vector3(s, s, s), c, false, 1.1)
wuerfel.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_deko.append(wuerfel)
func _process(delta: float) -> void:
for i in range(_deko.size()):
var n: Node3D = _deko[i]
n.rotation.y += delta * (0.2 + 0.05 * (i % 5))
n.rotation.x += delta * 0.13
n.position.y += sin(Time.get_ticks_msec() * 0.0007 + i) * delta * 0.6
# ---------------------------------------------------------------- Bausteine
## Erzeugt Außenbau + Innenraum und verbindet beide mit Tür-Zonen.
func _gebaeude(titel: String, a_center: Vector3, a_size: Vector3,
i_center: Vector3, i_size: Vector3, farbe: Color) -> Node3D:
Prims.room(self, a_center, a_size, farbe, farbe.darkened(0.3), Vector3.BACK, 3.6, false)
Prims.box(self, a_center + Vector3(0, a_size.y + 0.25, 0),
Vector3(a_size.x + 1.0, 0.5, a_size.z + 1.0), farbe.lightened(0.15))
Prims.box(self, a_center + Vector3(0, a_size.y + 1.1, 0),
Vector3(a_size.x * 0.5, 1.2, a_size.z * 0.5), farbe.lightened(0.3))
Prims.label(self, a_center + Vector3(0, a_size.y + 3.0, 0), titel, 1.4,
Color(1, 0.95, 0.8))
var aussen_tuer := a_center + Vector3(0, 0, a_size.z * 0.5)
var draussen := a_center + Vector3(0, 0.2, a_size.z * 0.5 + 3.2)
var innen := Node3D.new()
innen.name = titel.replace(" ", "_")
add_child(innen)
Prims.room(innen, i_center, i_size, farbe.lightened(0.12), farbe.darkened(0.25),
Vector3.BACK, 3.6, true)
Prims.box(innen, i_center + Vector3(0, i_size.y + 0.2, 0),
Vector3(i_size.x, 0.4, i_size.z), farbe.darkened(0.4))
for sx: float in [-1.0, 1.0]:
for sz: float in [-1.0, 1.0]:
var l := OmniLight3D.new()
l.position = i_center + Vector3(sx * i_size.x * 0.28, i_size.y - 0.8, sz * i_size.z * 0.28)
l.omni_range = 34.0
l.light_energy = 4.5
l.light_color = Color(0.90, 0.90, 1.0)
innen.add_child(l)
var innen_spawn := i_center + Vector3(0, 0.3, i_size.z * 0.5 - 3.0)
_tuer(self, aussen_tuer + Vector3(0, 1.2, 0), innen_spawn)
_tuer(innen, i_center + Vector3(0, 1.2, i_size.z * 0.5), draussen)
Prims.label(innen, i_center + Vector3(0, i_size.y - 0.9, i_size.z * 0.5 - 0.35), "Ausgang", 0.45,
Color(1, 0.85, 0.6))
return innen
func _tuer(parent: Node, pos: Vector3, ziel: Vector3) -> void:
var a := Area3D.new()
a.position = pos
var cs := CollisionShape3D.new()
var sh := BoxShape3D.new()
sh.size = Vector3(3.4, 2.4, 1.6)
cs.shape = sh
a.add_child(cs)
parent.add_child(a)
a.body_entered.connect(func(body: Node3D) -> void:
if body is Player and Game.teleport_lock <= 0.0:
Game.teleport_lock = 0.9
body.global_position = ziel
body.velocity = Vector3.ZERO)
func _interaktion(parent: Node, pos: Vector3, kind: String, arg: String, titel: String) -> Node3D:
var n := Node3D.new()
n.position = pos
parent.add_child(n)
n.add_to_group("interactable")
n.set_meta("kind", kind)
n.set_meta("arg", arg)
n.set_meta("titel", titel)
return n
# ---------------------------------------------------------------- Gebäude
func _gebaeude_bibliothek() -> void:
var innen := _gebaeude("Bibliothek der Techniken",
Vector3(-24, 0, -16), Vector3(16, 5, 14),
Vector3(0, INNEN_Y, 0), Vector3(34, 6, 24),
Color(0.58, 0.50, 0.80))
Prims.label(innen, Vector3(0, INNEN_Y + 5.0, -10.5),
"Berühre eine Technik, um ihre Hinweise zu lesen.", 0.4, Color(0.9, 0.9, 1))
var ids := TechDB.ids()
for i in range(ids.size()):
var reihe := i / 6
var spalte := i % 6
var p := Vector3(-12.5 + spalte * 5.0, INNEN_Y, -6.5 + reihe * 7.0)
var t := TechDB.by_id(ids[i])
Prims.cyl(innen, p + Vector3(0, 0.5, 0), 0.8, 1.0, Color(0.30, 0.26, 0.45))
var kristall := Prims.box(innen, p + Vector3(0, 1.8, 0), Vector3(0.9, 0.9, 0.9),
t["farbe"], false, 1.4)
kristall.rotation = Vector3(0.6, 0.8, 0.0)
_regale[ids[i]] = kristall
Prims.label(innen, p + Vector3(0, 3.2 + float(reihe) * 0.6, 0), t["name"], 0.36, Color(1, 1, 1))
_interaktion(innen, p + Vector3(0, 1.2, 0), "technik", ids[i], t["name"])
func _gebaeude_missionshalle() -> void:
var innen := _gebaeude("Missionshalle",
Vector3(0, 0, -28), Vector3(18, 5, 14),
Vector3(90, INNEN_Y, 0), Vector3(26, 6, 20),
Color(0.74, 0.55, 0.48))
Prims.label(innen, Vector3(90, INNEN_Y + 5.0, -8.5),
"Menschen, deren Traum feststeckt.", 0.42, Color(1, 0.92, 0.8))
for i in range(HumanDB.LIST.size()):
var h: Dictionary = HumanDB.LIST[i]
var p := Vector3(90.0 + (float(i) - 1.5) * 7.0, INNEN_Y, -3.0)
Prims.cyl(innen, p + Vector3(0, 0.4, 0), 1.2, 0.8, Color(0.35, 0.28, 0.26))
var saeule := Prims.box(innen, p + Vector3(0, 2.0, 0), Vector3(1.4, 3.2, 1.4),
h["farbe"], false, 0.8)
var sphere := Prims.sphere(innen, p + Vector3(0, 4.1, 0), 0.55,
(h["farbe"] as Color).lightened(0.4), 1.6)
var lab := Prims.label(innen, p + Vector3(0, 5.2, 0), h["name"], 0.32, Color(1, 1, 1))
_stelen[h["id"]] = {"saeule": saeule, "kugel": sphere, "label": lab}
_interaktion(innen, p + Vector3(0, 1.5, 0), "mission", h["id"], h["name"])
func _gebaeude_meditation() -> void:
var innen := _gebaeude("Meditationskammer",
Vector3(24, 0, -16), Vector3(14, 5, 14),
Vector3(-90, INNEN_Y, 0), Vector3(20, 6, 18),
Color(0.46, 0.66, 0.74))
Prims.label(innen, Vector3(-90, INNEN_Y + 5.0, -7.5),
"Übung vertieft, was du schon kannst.", 0.42, Color(0.85, 1, 1))
Prims.cyl(innen, Vector3(-90, INNEN_Y + 0.5, -1.0), 3.0, 1.0, Color(0.24, 0.34, 0.40))
var kugel := Prims.sphere(innen, Vector3(-90, INNEN_Y + 2.6, -1.0), 1.2,
Color(0.55, 0.95, 1.0), 1.8)
kugel.name = "Meditationskugel"
_interaktion(innen, Vector3(-90, INNEN_Y + 1.5, -1.0), "vertiefen", "", "Meditationskugel")
func _tor_zur_erde() -> void:
var pos := Vector3(0, 0, 18)
Prims.cyl(self, pos + Vector3(0, 0.3, 0), 5.0, 0.6, Color(0.35, 0.30, 0.50))
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),
Vector3(0.7, 0.7, 0.7), Color(0.5, 0.85, 1.0), false, 1.8)
var scheibe := Prims.cyl(self, pos + Vector3(0, 2.6, 0), 3.0, 0.2,
Color(0.45, 0.75, 1.0, 0.28), false, 1.0)
scheibe.rotation.x = PI * 0.5
Prims.label(self, pos + Vector3(0, 6.4, 0), "Tor zur Erde", 0.7, Color(0.7, 0.95, 1))
_interaktion(self, pos + Vector3(0, 1.6, 0), "erde", "", "Tor zur Erde")
# ---------------------------------------------------------------- Aktualisieren
func _refresh() -> void:
for id in _regale:
var m: MeshInstance3D = _regale[id]
var t := TechDB.by_id(id)
if Game.knows(id):
m.material_override = Prims.mat(t["farbe"], 2.2)
m.scale = Vector3.ONE * (1.0 + 0.12 * Game.skill_of(id))
else:
m.material_override = Prims.mat((t["farbe"] as Color).darkened(0.35), 0.5)
m.scale = Vector3.ONE * 0.75
for id in _stelen:
var s: Dictionary = _stelen[id]
var h := HumanDB.by_id(id)
var lab: Label3D = s["label"]
if Game.completed.has(id):
s["kugel"].material_override = Prims.mat(Color(0.5, 1.0, 0.6), 2.4)
lab.text = h["name"] + "\n★ erfüllt"
elif Game.active_mission == id:
s["kugel"].material_override = Prims.mat(Color(1, 0.85, 0.35), 2.4)
lab.text = h["name"] + "\naktive Mission · %d %%" % int(Game.mission_progress(id))
else:
s["kugel"].material_override = Prims.mat((h["farbe"] as Color).lightened(0.4), 1.2)
lab.text = h["name"] + "\nwartet"

View File

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

185
scripts/earth_world.gd Normal file
View File

@@ -0,0 +1,185 @@
extends Node3D
class_name EarthWorld
## Die Erde: eine Kleinstadt mit vier Menschen, die ihren Tag leben.
var spawn := Vector3(0, 3.0, 12.0)
var humans: Array = []
var _sun: DirectionalLight3D
var _sky: ProceduralSkyMaterial
var _env: Environment
var _nachtlichter: Array = []
func _ready() -> void:
_environment()
_boden()
_orte()
_park()
_saeule()
_menschen()
_himmel_aktualisieren()
func _environment() -> void:
var we := WorldEnvironment.new()
_env = Environment.new()
_env.background_mode = Environment.BG_SKY
var sky := Sky.new()
_sky = ProceduralSkyMaterial.new()
sky.sky_material = _sky
_env.sky = sky
_env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
_env.glow_enabled = true
_env.glow_intensity = 0.35
_env.fog_enabled = true
_env.fog_density = 0.0012
we.environment = _env
add_child(we)
_sun = DirectionalLight3D.new()
_sun.shadow_enabled = true
_sun.shadow_blur = 1.5
add_child(_sun)
func _boden() -> void:
Prims.box(self, Vector3(0, -0.5, 0), Vector3(240, 1, 240), 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)
func _orte() -> void:
for key in HumanDB.PLACES:
if key == "park":
continue
var pl: Dictionary = HumanDB.PLACES[key]
var c := HumanDB.place_center(key)
var s: Array = pl["size"]
var groesse := Vector3(s[0], 2.3, s[1])
var farbe: Color = pl["color"]
var richtung: Vector3 = HumanDB.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))
_einrichtung(c, groesse, farbe, key)
var tuer := HumanDB.place_door(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)
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":
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
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,
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),
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))
elif key == "baeckerei":
Prims.box(self, c + Vector3(0, 0.55, 2.0), 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))
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))
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:
continue
var hoehe := rng.randf_range(2.4, 4.2)
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)
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))
func _saeule() -> void:
var pos := Vector3(0, 0, 12.0)
Prims.cyl(self, pos + Vector3(0, 0.2, 0), 2.6, 0.4, Color(0.50, 0.48, 0.55), false)
Prims.cyl(self, pos + Vector3(0, 2.2, 0), 0.7, 4.0, Color(0.55, 0.80, 1.0), false, 1.6)
Prims.sphere(self, pos + Vector3(0, 4.6, 0), 0.9, Color(0.75, 0.92, 1.0), 2.2)
Prims.label(self, pos + Vector3(0, 6.0, 0), "Rückkehr in die Traumwelt", 0.4,
Color(0.75, 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", "traum")
n.set_meta("arg", "")
n.set_meta("titel", "Rückkehr in die Traumwelt")
func _menschen() -> void:
for i in range(HumanDB.LIST.size()):
var m := Human.new()
add_child(m)
m.setup(HumanDB.LIST[i], i)
humans.append(m)
func _process(_delta: float) -> void:
_himmel_aktualisieren()
func _himmel_aktualisieren() -> void:
var t: float = Game.time_h
var sonne := clampf(sin((t - 6.0) / 12.0 * PI), 0.0, 1.0)
_sun.rotation_degrees = Vector3(-((t - 6.0) / 12.0) * 180.0, -35.0, 0.0)
_sun.light_energy = 0.25 + sonne * 1.0
_sun.light_color = Color(1.0, 0.75 + sonne * 0.25, 0.55 + sonne * 0.4)
var tag_top := Color(0.30, 0.55, 0.90)
var nacht_top := Color(0.03, 0.04, 0.12)
var tag_hor := Color(0.75, 0.85, 0.95)
var nacht_hor := Color(0.10, 0.10, 0.25)
_sky.sky_top_color = nacht_top.lerp(tag_top, sonne)
_sky.sky_horizon_color = nacht_hor.lerp(tag_hor, sonne)
_sky.ground_horizon_color = nacht_hor.lerp(Color(0.55, 0.60, 0.55), sonne)
_sky.ground_bottom_color = nacht_top.lerp(Color(0.30, 0.38, 0.28), sonne)
_env.ambient_light_energy = 0.45 + sonne * 0.8
_env.fog_light_color = nacht_hor.lerp(tag_hor, sonne)
var nacht := 1.0 - sonne
for l in _nachtlichter:
l.light_energy = clampf(nacht * 2.4 - 0.3, 0.0, 2.2)

View File

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

223
scripts/game.gd Normal file
View File

@@ -0,0 +1,223 @@
extends Node
## Globaler Spielzustand: Wissen, Fertigkeiten, Missionen, Uhrzeit.
signal state_changed
signal notified(text: String, color: Color)
signal mission_done(human_id: String)
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
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 completed: Array = []
var time_h: float = 7.0
var day: int = 1
var teleport_lock: float = 0.0
func _ready() -> void:
_setup_input()
func _setup_input() -> void:
var binds := {
"vor": [KEY_W, KEY_UP],
"zurueck": [KEY_S, KEY_DOWN],
"links": [KEY_A, KEY_LEFT],
"rechts": [KEY_D, KEY_RIGHT],
"steigen": [KEY_SPACE],
"sinken": [KEY_SHIFT],
"interagieren": [KEY_E],
"kodex": [KEY_TAB],
}
for action in binds:
if not InputMap.has_action(action):
InputMap.add_action(action)
for code in binds[action]:
var ev := InputEventKey.new()
ev.physical_keycode = code
InputMap.action_add_event(action, ev)
func advance_time(delta: float) -> void:
time_h += delta * 24.0 / DAY_SECONDS
while time_h >= 24.0:
time_h -= 24.0
day += 1
func time_string() -> String:
var h := int(time_h)
var m := int((time_h - h) * 60.0)
return "Tag %d · %02d:%02d" % [day, h, m]
# ---------------------------------------------------------------- Fertigkeiten
func knows(tech_id: String) -> bool:
return known.has(tech_id)
func skill_of(tech_id: String) -> int:
if not known.has(tech_id):
return 0
var xp: int = known[tech_id]
var lvl := 1
for i in range(XP_LEVELS.size()):
if xp >= XP_LEVELS[i]:
lvl = i + 1
return lvl
func xp_to_next(tech_id: String) -> String:
var lvl := skill_of(tech_id)
if lvl >= XP_LEVELS.size():
return "Meisterschaft"
var need: int = XP_LEVELS[lvl]
return "%d / %d EP" % [known[tech_id], need]
func learn(tech_id: String) -> bool:
if known.has(tech_id):
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))
state_changed.emit()
return true
func add_xp(tech_id: String, amount: int) -> void:
if not known.has(tech_id):
return
var before := skill_of(tech_id)
known[tech_id] = int(known[tech_id]) + amount
if skill_of(tech_id) > before:
var t := TechDB.by_id(tech_id)
notified.emit("%s ist jetzt Stufe %d." % [t["name"], skill_of(tech_id)], Color(0.7, 0.9, 1))
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:
if completed.has(human_id):
return false
if active_mission == human_id:
return false
active_mission = human_id
if not progress.has(human_id):
progress[human_id] = 0.0
var h := HumanDB.by_id(human_id)
notified.emit("Mission angenommen: %s" % h["name"], Color(1, 0.9, 0.5))
state_changed.emit()
return true
func mission_progress(human_id: String) -> float:
return progress.get(human_id, 0.0)
# ----------------------------------------------------- Technik auf Mensch anwenden
## 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)
var h := HumanDB.by_id(human_id)
var skill := skill_of(tech_id)
var res := {
"chance": 0.0, "tag": false, "moment": false, "blocked": false,
"grund": "", "skill": skill,
}
if activity == "schlaeft":
res["blocked"] = true
res["grund"] = "Sie oder er schläft. Techniken erreichen nur wache Menschen."
return res
if skill == 0:
res["blocked"] = true
res["grund"] = "Technik nicht gelernt."
return res
for tag in t["tags"]:
if h["tags"].has(tag):
res["tag"] = true
break
res["moment"] = t["momente"].has(activity)
var c := 0.08 + 0.11 * float(skill)
if res["tag"]:
c += 0.25
if res["moment"]:
c += 0.22
if resisting:
c -= 0.30
res["grund"] = "Der Mensch macht gerade dicht."
res["chance"] = clampf(c, 0.05, 0.95)
return res
## 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:
var ev := evaluate(tech_id, human_id, activity, resisting)
if ev["blocked"]:
return {"ok": false, "grund": ev["grund"]}
var roll := randf()
var success: bool = roll < float(ev["chance"])
var key := "%s:%s" % [human_id, tech_id]
var wiederholung: int = used.get(key, 0)
used[key] = wiederholung + 1
var gain := 0.0
if success:
var base := 8.0 + 2.0 * float(ev["skill"])
if ev["tag"]:
base += 5.0
if ev["moment"]:
base += 5.0
gain = base / (1.0 + 0.6 * float(wiederholung))
add_xp(tech_id, 2)
else:
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 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"],
Color(1, 0.85, 0.4))
state_changed.emit()
return {
"ok": true, "success": success, "chance": ev["chance"], "gain": gain,
"wiederholung": wiederholung, "mission_done": done,
"tag": ev["tag"], "moment": ev["moment"],
}

1
scripts/game.gd.uid Normal file
View File

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

147
scripts/human.gd Normal file
View File

@@ -0,0 +1,147 @@
extends Node3D
class_name Human
## Ein Mensch auf der Erde. Läuft seinen Tagesablauf ab und kann von einer
## Technik erreicht werden -- oder eben nicht.
const GEHTEMPO := 6.0
var data: Dictionary
var idx: int = 0
var place: String = ""
var activity: String = "schlaeft"
var resist_left: float = 0.0
var _visual: Node3D
var _label: Label3D
var _koerper: MeshInstance3D
var _kopf: MeshInstance3D
var _aura: MeshInstance3D
var _strahl: MeshInstance3D
var _path: Array = []
var _bob: float = 0.0
var _offset: Vector3
func setup(d: Dictionary, index: int) -> void:
data = d
idx = index
name = "Human_" + str(d["id"])
var winkel := float(index) * TAU / 4.0
_offset = Vector3(cos(winkel), 0, sin(winkel)) * 2.2
_visual = Node3D.new()
add_child(_visual)
var c: Color = d["farbe"]
var hoehe := 1.05 + float(index % 3) * 0.08
_koerper = Prims.capsule(_visual, Vector3(0, hoehe * 0.62, 0), 0.32, hoehe, c)
_kopf = Prims.sphere(_visual, Vector3(0, hoehe * 1.18 + 0.1, 0), 0.24, c.lightened(0.45))
Prims.box(_visual, Vector3(0, hoehe * 0.95, 0), Vector3(0.9, 0.14, 0.3), c.darkened(0.25), false)
Prims.box(_visual, Vector3(0, 0.06, 0), Vector3(0.5, 0.12, 0.7), c.darkened(0.45), false)
_aura = Prims.cyl(self, Vector3(0, 0.05, 0), 0.95, 0.05, Color(1, 0.85, 0.35), false, 2.0)
_aura.visible = false
_strahl = Prims.cyl(self, Vector3(0, 11.0, 0), 0.14, 16.0, Color(1, 0.85, 0.35, 0.22), false, 1.2)
_strahl.visible = false
_label = Prims.label(self, Vector3(0, 2.9, 0), d["name"], 0.30)
var start := _routine_at(Game.time_h)
place = start[0]
activity = start[1]
global_position = HumanDB.place_center(place) + _offset
func _routine_at(h: float) -> Array:
var r: Array = data["routine"]
var cur: Array = r[r.size() - 1]
for eintrag in r:
if h >= float(eintrag[0]):
cur = eintrag
return [cur[1], cur[2]]
func _process(delta: float) -> void:
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 = HumanDB.route(place, soll[0], _offset)
if war_unterwegs and _path.size() > 1:
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
place = soll[0]
activity = "unterwegs"
elif _path.is_empty() and activity != soll[1]:
activity = soll[1]
if not _path.is_empty():
var ziel: Vector3 = _path[0]
ziel.y = 0.0
var hier := global_position
hier.y = 0.0
var d := ziel - hier
if d.length() < 0.4:
_path.remove_at(0)
if _path.is_empty():
activity = soll[1]
else:
var schritt := d.normalized() * GEHTEMPO * delta
global_position += schritt
_visual.rotation.y = lerp_angle(_visual.rotation.y, atan2(-d.x, -d.z), 1.0 - pow(0.002, delta))
_bob += delta * 9.0
_visual.position.y = absf(sin(_bob)) * 0.09
if _path.is_empty():
_visual.position.y = lerpf(_visual.position.y, 0.0, 1.0 - pow(0.01, delta))
_visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0,
1.0 - pow(0.01, delta))
var erledigt: bool = Game.completed.has(data["id"])
var aktiv: bool = Game.active_mission == data["id"]
_aura.visible = aktiv or erledigt
_strahl.visible = aktiv
if _aura.visible:
_aura.material_override = Prims.mat(
Color(0.4, 1.0, 0.5) if erledigt else Color(1, 0.85, 0.35), 2.0)
_aura.scale.x = 1.0 + sin(Time.get_ticks_msec() * 0.003) * 0.08
_aura.scale.z = _aura.scale.x
_label.text = beschriftung()
_label.modulate = Color(0.55, 0.85, 0.6) if erledigt else (
Color(1, 0.9, 0.6) if aktiv else Color(1, 1, 1))
func beschriftung() -> String:
var s: String = data["name"] + "\n" + HumanDB.activity_text(activity)
if resist_left > 0.0:
s += " (macht dicht)"
if Game.completed.has(data["id"]):
s += "\n★ Traum erfüllt"
elif Game.active_mission == data["id"]:
s += "\nTraum: %d %%" % int(Game.mission_progress(data["id"]))
return s
## 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,
Color(0.55, 1.0, 0.6) if success else Color(1, 0.55, 0.45))
var tw := l.create_tween()
tw.set_parallel(true)
tw.tween_property(l, "position:y", 5.2, 2.4)
tw.tween_property(l, "modulate:a", 0.0, 2.4).set_delay(0.9)
tw.chain().tween_callback(l.queue_free)
if not success:
resist_left = Game.RESIST_SEKUNDEN
var blitz := Prims.mat(
Color(0.6, 1.0, 0.7) if success else Color(1, 0.5, 0.4), 1.6)
var orig := _koerper.material_override
_koerper.material_override = blitz
_kopf.material_override = blitz
var t2 := create_tween()
t2.tween_interval(0.35)
t2.tween_callback(func() -> void:
if is_instance_valid(_koerper):
_koerper.material_override = orig
_kopf.material_override = Prims.mat((data["farbe"] as Color).lightened(0.45)))

1
scripts/human.gd.uid Normal file
View File

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

206
scripts/human_db.gd Normal file
View File

@@ -0,0 +1,206 @@
class_name HumanDB
## Orte der Kleinstadt und die Menschen mit ihren Tagesabläufen.
## 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 = {
"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),
},
"haus_tobias": {
"name": "Wohnung Frank", "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 DIR_VEC: Dictionary = {
"back": Vector3(0, 0, 1),
"forward": Vector3(0, 0, -1),
"left": Vector3(-1, 0, 0),
"right": Vector3(1, 0, 0),
"none": Vector3(0, 0, 0),
}
static var ACTIVITY_TEXT: Dictionary = {
"schlaeft": "schläft",
"morgenroutine": "Morgenroutine",
"arbeitet": "arbeitet",
"pause": "macht Pause",
"essen": "isst",
"gruebelt": "grübelt",
"ruht": "ruht sich aus",
"angespannt": "ist angespannt",
"abendroutine": "Abendroutine",
"aufraeumen": "räumt auf",
"wartet": "wartet",
"muede": "ist müde",
"unterwegs": "geht spazieren",
"zoegert": "zögert",
}
static var LIST: 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"],
"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"],
"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.",
"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"],
"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.",
"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"],
"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"],
],
},
]
static func by_id(id: String) -> Dictionary:
for h in LIST:
if h["id"] == id:
return h
return {}
static func place_center(key: String) -> Vector3:
var p: Array = PLACES[key]["pos"]
return Vector3(p[0], 0.0, p[1])
## Punkt direkt vor der Türlücke, außerhalb des Gebäudes.
static func place_door(key: String) -> Vector3:
var pl: Dictionary = PLACES[key]
var c := place_center(key)
var d: Vector3 = DIR_VEC[pl["dir"]]
if d == Vector3.ZERO:
return c
var size: Array = pl["size"]
var half: float = size[1] * 0.5 if absf(d.z) > 0.0 else size[0] * 0.5
return c + d * (half + 2.5)
## Route von Ort a nach Ort b: aus der Tür, über den Platz, zur neuen Tür, hinein.
static func route(a: String, b: String, offset: Vector3) -> Array:
var pts: Array = []
if a == b:
return pts
var da := place_door(a)
var db := place_door(b)
pts.append(da)
if da.length() > 17.0:
pts.append(da.normalized() * 15.0)
if db.length() > 17.0:
pts.append(db.normalized() * 15.0)
pts.append(db)
pts.append(place_center(b) + offset)
return pts
static func activity_text(a: String) -> String:
return ACTIVITY_TEXT.get(a, a)

1
scripts/human_db.gd.uid Normal file
View File

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

169
scripts/main.gd Normal file
View File

@@ -0,0 +1,169 @@
extends Node3D
## Wurzelknoten: hält Spieler und Oberfläche, wechselt zwischen den Welten
## und wickelt Interaktionen ab.
const REICHWEITE_OBJEKT := 4.2
const REICHWEITE_MENSCH := 3.6
var player: Player
var ui: UI
var welt: Node3D
var auf_erde := false
var _fokus: Node3D = null
var _fokus_mensch: Node3D = null
func _ready() -> void:
ui = UI.new()
add_child(ui)
ui.panel_opened.connect(func() -> void:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
player.look_enabled = false)
ui.panel_closed.connect(func() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
player.look_enabled = true)
player = Player.new()
add_child(player)
Game.mission_done.connect(func(_id: String) -> void:
Game.notified.emit("Kehre zur Traumwelt zurück — die Säule im Park bringt dich hin.",
Color(0.8, 0.95, 1)))
_traumwelt()
ui.intro_panel(func() -> void: pass)
func _traumwelt() -> void:
auf_erde = false
_setze_welt(DreamWorld.new())
player.set_mode(Player.Mode.TRAUM)
player.global_position = (welt as DreamWorld).spawn
player.yaw = 0.0
player.pitch = -0.18
func _erde() -> void:
auf_erde = true
_setze_welt(EarthWorld.new())
player.set_mode(Player.Mode.GEIST)
player.global_position = (welt as EarthWorld).spawn
player.yaw = 0.0
player.pitch = -0.42
func _setze_welt(neu: Node3D) -> void:
if welt != null:
remove_child(welt)
welt.queue_free()
welt = neu
add_child(welt)
move_child(welt, 0)
Game.teleport_lock = 1.0
_fokus = null
_fokus_mensch = null
func _process(delta: float) -> void:
Game.advance_time(delta)
Game.teleport_lock = maxf(0.0, Game.teleport_lock - delta)
if ui.offen:
ui.setze_hinweis("")
return
_fokus_suchen()
_hinweis_setzen()
func _fokus_suchen() -> void:
_fokus = null
_fokus_mensch = null
var p := player.global_position
var best := REICHWEITE_OBJEKT
for n in get_tree().get_nodes_in_group("interactable"):
var d: float = p.distance_to((n as Node3D).global_position)
if d < best:
best = d
_fokus = n
if auf_erde:
var bestm := REICHWEITE_MENSCH
for m in (welt as EarthWorld).humans:
var d: float = p.distance_to((m as Node3D).global_position + Vector3(0, 1.0, 0))
if d < bestm:
bestm = d
_fokus_mensch = m
if _fokus_mensch != null and (_fokus == null or bestm < best):
_fokus = null
elif _fokus != null:
_fokus_mensch = null
func _hinweis_setzen() -> void:
if _fokus_mensch != null:
ui.setze_hinweis("[E] %s ansprechen — %s" %
[_fokus_mensch.data["name"], HumanDB.activity_text(_fokus_mensch.activity)])
elif _fokus != null:
var kind: String = _fokus.get_meta("kind")
var titel: String = _fokus.get_meta("titel")
match kind:
"technik":
ui.setze_hinweis("[E] %s%s" % [titel,
"" if not Game.knows(_fokus.get_meta("arg")) else " (gelernt)"])
"mission":
ui.setze_hinweis("[E] %s — Mission ansehen" % titel)
"vertiefen":
ui.setze_hinweis("[E] Meditieren")
"erde":
ui.setze_hinweis("[E] Zur Erde reisen")
"traum":
ui.setze_hinweis("[E] Zurück in die Traumwelt")
_:
ui.setze_hinweis("[E] %s" % titel)
else:
ui.setze_hinweis("")
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel") and not ui.offen:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if \
Input.mouse_mode == Input.MOUSE_MODE_CAPTURED else Input.MOUSE_MODE_CAPTURED
player.look_enabled = Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
get_viewport().set_input_as_handled()
return
if ui.offen:
return
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_VISIBLE:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
player.look_enabled = true
return
if event.is_action_pressed("kodex"):
ui.kodex_panel()
get_viewport().set_input_as_handled()
return
if event.is_action_pressed("interagieren"):
_interagieren()
get_viewport().set_input_as_handled()
func _interagieren() -> void:
if _fokus_mensch != null:
ui.anwenden_panel(_fokus_mensch)
return
if _fokus == null:
return
var kind: String = _fokus.get_meta("kind")
var arg: String = _fokus.get_meta("arg")
match kind:
"technik":
ui.technik_panel(arg)
"mission":
ui.mission_panel(arg)
"vertiefen":
ui.vertiefen_panel()
"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))
else:
_erde()
"traum":
_traumwelt()

1
scripts/main.gd.uid Normal file
View File

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

150
scripts/player.gd Normal file
View File

@@ -0,0 +1,150 @@
extends CharacterBody3D
class_name Player
## Ein Wesen aus der Traumwelt. In der Traumwelt läuft es, auf der Erde schwebt
## es als unsichtbarer Geist durch Wände.
enum Mode { TRAUM, GEIST }
const SPEED_TRAUM := 8.0
const SPEED_GEIST := 11.0
const GRAVITY := 24.0
const JUMP := 9.0
const MAUS := 0.0026
var mode: int = Mode.TRAUM
var yaw := 0.0
var pitch := -0.25
var look_enabled := true
var pivot: Node3D
var arm: SpringArm3D
var cam: Camera3D
var body: Node3D
var _kern: MeshInstance3D
var _ringe: Array = []
func _ready() -> void:
var cs := CollisionShape3D.new()
var shape := CapsuleShape3D.new()
shape.radius = 0.45
shape.height = 1.9
cs.shape = shape
cs.position.y = 0.95
add_child(cs)
body = Node3D.new()
add_child(body)
_kern = Prims.capsule(body, Vector3(0, 1.0, 0), 0.42, 1.6, Color(0.55, 0.85, 1.0))
_kern.material_override = Prims.mat(Color(0.55, 0.85, 1.0), 0.9)
var kopf := Prims.sphere(body, Vector3(0, 2.0, 0), 0.28, Color(0.80, 0.92, 1.0), 0.6)
kopf.name = "Kopf"
for i in range(2):
var r := Prims.cyl(body, Vector3(0, 0.5 + i * 0.9, 0), 0.75 + i * 0.15, 0.06,
Color(0.6, 0.9, 1.0), false, 1.6)
r.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_ringe.append(r)
# kleiner Blick nach vorn, damit man die Laufrichtung sieht
Prims.box(body, Vector3(0, 1.55, -0.36), Vector3(0.5, 0.12, 0.12), Color(1, 1, 1), false, 2.0)
pivot = Node3D.new()
pivot.position.y = 1.5
add_child(pivot)
arm = SpringArm3D.new()
arm.position.x = 0.75
arm.spring_length = 6.5
arm.margin = 0.3
pivot.add_child(arm)
cam = Camera3D.new()
cam.fov = 74.0
arm.add_child(cam)
set_mode(Mode.TRAUM)
func set_mode(m: int) -> void:
mode = m
if m == Mode.GEIST:
collision_layer = 0
collision_mask = 0
arm.collision_mask = 0
arm.spring_length = 5.0
pivot.position.y = 2.1
body.scale = Vector3.ONE * 0.72
_kern.material_override = Prims.mat(Color(0.65, 0.9, 1.0, 0.26), 0.5)
for r in _ringe:
r.material_override = Prims.mat(Color(0.7, 0.95, 1.0, 0.20), 0.8)
r.scale = Vector3(0.55, 1.0, 0.55)
else:
collision_layer = 1
collision_mask = 1
arm.collision_mask = 1
arm.spring_length = 6.5
pivot.position.y = 1.5
body.scale = Vector3.ONE
_kern.material_override = Prims.mat(Color(0.55, 0.85, 1.0), 0.9)
for r in _ringe:
r.material_override = Prims.mat(Color(0.6, 0.9, 1.0), 1.6)
r.scale = Vector3.ONE
velocity = Vector3.ZERO
func _unhandled_input(event: InputEvent) -> void:
if not look_enabled:
return
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
yaw -= event.relative.x * MAUS
pitch = clampf(pitch - event.relative.y * MAUS, -1.3, 0.6)
func _physics_process(delta: float) -> void:
pivot.rotation.y = yaw
arm.rotation.x = pitch
for i in range(_ringe.size()):
var r: MeshInstance3D = _ringe[i]
r.rotation.y += delta * (0.8 + 0.5 * i)
r.position.y = 0.5 + i * 0.9 + sin(Time.get_ticks_msec() * 0.001 * (1.2 + i * 0.4)) * 0.12
var input := Vector2.ZERO
if look_enabled:
input.x = Input.get_axis("links", "rechts")
input.y = Input.get_axis("vor", "zurueck")
var dir := Vector3.ZERO
if input.length() > 0.01:
var basis_yaw := Basis(Vector3.UP, yaw)
dir = (basis_yaw * Vector3(input.x, 0, input.y)).normalized()
if mode == Mode.GEIST:
var v := dir * SPEED_GEIST
if look_enabled:
if Input.is_action_pressed("steigen"):
v.y += SPEED_GEIST * 0.75
if Input.is_action_pressed("sinken"):
v.y -= SPEED_GEIST * 0.75
velocity = velocity.lerp(v, 1.0 - pow(0.001, delta))
move_and_slide()
if global_position.y < 0.4:
global_position.y = 0.4
if global_position.y > 45.0:
global_position.y = 45.0
else:
velocity.x = lerpf(velocity.x, dir.x * SPEED_TRAUM, 1.0 - pow(0.0005, delta))
velocity.z = lerpf(velocity.z, dir.z * SPEED_TRAUM, 1.0 - pow(0.0005, delta))
if is_on_floor():
if look_enabled and Input.is_action_pressed("steigen"):
velocity.y = JUMP
else:
velocity.y = -1.0
else:
velocity.y -= GRAVITY * delta
move_and_slide()
if global_position.y < -400.0:
global_position.y = 2.0
if dir.length() > 0.01:
var ziel := atan2(-dir.x, -dir.z)
body.rotation.y = lerp_angle(body.rotation.y, ziel, 1.0 - pow(0.0005, delta))
func blickrichtung() -> Vector3:
return -Vector3(sin(yaw), 0, cos(yaw))

1
scripts/player.gd.uid Normal file
View File

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

134
scripts/prims.gd Normal file
View File

@@ -0,0 +1,134 @@
class_name Prims
## Kleine Helfer, um die Welt aus Godot-Primitiven zusammenzusetzen.
static func mat(c: Color, emis: float = 0.0, rough: float = 0.85) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
m.albedo_color = c
m.roughness = rough
m.metallic = 0.0
if emis > 0.0:
m.emission_enabled = true
m.emission = c
m.emission_energy_multiplier = emis
if c.a < 1.0:
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
return m
static func box(parent: Node, pos: Vector3, size: Vector3, c: Color,
collide: bool = true, emis: float = 0.0) -> MeshInstance3D:
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = size
mi.mesh = bm
mi.material_override = mat(c, emis)
mi.position = pos
parent.add_child(mi)
if collide:
_add_box_body(mi, size)
return mi
static func _add_box_body(mi: MeshInstance3D, size: Vector3) -> void:
var sb := StaticBody3D.new()
var cs := CollisionShape3D.new()
var sh := BoxShape3D.new()
sh.size = size
cs.shape = sh
sb.add_child(cs)
mi.add_child(sb)
static func cyl(parent: Node, pos: Vector3, radius: float, height: float, c: Color,
collide: bool = true, emis: float = 0.0) -> MeshInstance3D:
var mi := MeshInstance3D.new()
var cm := CylinderMesh.new()
cm.top_radius = radius
cm.bottom_radius = radius
cm.height = height
mi.mesh = cm
mi.material_override = mat(c, emis)
mi.position = pos
parent.add_child(mi)
if collide:
var sb := StaticBody3D.new()
var cs := CollisionShape3D.new()
var sh := CylinderShape3D.new()
sh.radius = radius
sh.height = height
cs.shape = sh
sb.add_child(cs)
mi.add_child(sb)
return mi
static func sphere(parent: Node, pos: Vector3, radius: float, c: Color,
emis: float = 0.0) -> MeshInstance3D:
var mi := MeshInstance3D.new()
var sm := SphereMesh.new()
sm.radius = radius
sm.height = radius * 2.0
mi.mesh = sm
mi.material_override = mat(c, emis)
mi.position = pos
parent.add_child(mi)
return mi
static func capsule(parent: Node, pos: Vector3, radius: float, height: float, c: Color) -> MeshInstance3D:
var mi := MeshInstance3D.new()
var cm := CapsuleMesh.new()
cm.radius = radius
cm.height = height
mi.mesh = cm
mi.material_override = mat(c)
mi.position = pos
parent.add_child(mi)
return mi
static func label(parent: Node, pos: Vector3, text: String, size: float = 0.35,
c: Color = Color.WHITE) -> Label3D:
var l := Label3D.new()
l.text = text
l.font_size = 96
l.pixel_size = size / 96.0
l.position = pos
l.billboard = BaseMaterial3D.BILLBOARD_ENABLED
l.no_depth_test = false
l.modulate = c
l.outline_size = 24
l.outline_modulate = Color(0, 0, 0, 0.85)
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
parent.add_child(l)
return l
## Ein rechteckiger Raum aus vier Wänden mit einer Türlücke.
## door_dir: Vector3.FORWARD/BACK/LEFT/RIGHT -- auf welcher Seite die Lücke sitzt.
static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color,
floor_c: Color, door_dir: Vector3, door_w: float = 3.0,
with_floor: bool = true) -> void:
var t := 0.4
var h := size.y
if with_floor:
box(parent, center + Vector3(0, 0.04, 0), Vector3(size.x, 0.2, size.z), floor_c)
var seiten: Array[Vector3] = [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]
for d in seiten:
var quer := absf(d.z) > 0.0 # Wand liegt entlang der X-Achse
var along: Vector3 = Vector3(1, 0, 0) if quer else Vector3(0, 0, 1)
var len_along: float = size.x if quer else size.z
var off: Vector3 = Vector3(d.x * (size.x * 0.5 - t * 0.5), h * 0.5,
d.z * (size.z * 0.5 - t * 0.5))
var voll: Vector3 = Vector3(size.x, h, t) if quer else Vector3(t, h, size.z)
if d.is_equal_approx(door_dir) and door_w > 0.0 and door_w < len_along:
var seg := (len_along - door_w) * 0.5
var seg_size: Vector3 = Vector3(seg, h, t) if quer else Vector3(t, h, seg)
for s: float in [-1.0, 1.0]:
var seg_pos: Vector3 = center + off + along * (s * (door_w * 0.5 + seg * 0.5))
box(parent, seg_pos, seg_size, wall_c)
if h >= 3.0:
var sturz: Vector3 = Vector3(door_w, h * 0.3, t) if quer else Vector3(t, h * 0.3, door_w)
box(parent, center + off + Vector3(0, h * 0.35, 0), sturz, wall_c)
else:
box(parent, center + off, voll, wall_c)

1
scripts/prims.gd.uid Normal file
View File

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

172
scripts/technique_db.gd Normal file
View File

@@ -0,0 +1,172 @@
class_name TechDB
## Techniken sind echte, in der Praxis erprobte Lösungsansätze.
## Jede trägt drei Hinweise: was sie tut, wann man sie anwendet, warum sie hilft.
static var LIST: Array = [
{
"id": "pomodoro",
"name": "Pomodoro-Technik",
"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.",
"tags": ["prokrastination", "fokus"],
"momente": ["arbeitet", "zoegert"],
"farbe": Color(0.95, 0.45, 0.30),
},
{
"id": "zwei_minuten",
"name": "Zwei-Minuten-Regel",
"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.",
"tags": ["prokrastination", "ueberforderung"],
"momente": ["zoegert", "aufraeumen", "arbeitet"],
"farbe": Color(0.98, 0.72, 0.25),
},
{
"id": "wenn_dann",
"name": "Wenn-Dann-Plan",
"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.",
"tags": ["gewohnheit", "prokrastination"],
"momente": ["morgenroutine", "pause"],
"farbe": Color(0.45, 0.80, 0.55),
},
{
"id": "habit_stack",
"name": "Gewohnheiten stapeln",
"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.",
"tags": ["gewohnheit"],
"momente": ["morgenroutine", "essen", "abendroutine"],
"farbe": Color(0.40, 0.70, 0.85),
},
{
"id": "zerlegen",
"name": "Aufgabe zerlegen",
"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.",
"tags": ["ueberforderung", "prokrastination", "ziel"],
"momente": ["gruebelt", "arbeitet"],
"farbe": Color(0.75, 0.55, 0.95),
},
{
"id": "umgebung",
"name": "Umgebung gestalten",
"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.",
"tags": ["ablenkung", "gewohnheit"],
"momente": ["aufraeumen", "arbeitet", "ruht"],
"farbe": Color(0.55, 0.85, 0.40),
},
{
"id": "reframing",
"name": "Gedanken umdeuten",
"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.",
"tags": ["selbstzweifel", "angst"],
"momente": ["gruebelt", "ruht", "angespannt"],
"farbe": Color(0.95, 0.55, 0.75),
},
{
"id": "box_atmung",
"name": "Box-Atmung",
"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.",
"tags": ["angst", "stress"],
"momente": ["angespannt", "wartet", "pause"],
"farbe": Color(0.40, 0.85, 0.95),
},
{
"id": "selbstmitgefuehl",
"name": "Selbstmitgefühl",
"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.",
"tags": ["selbstzweifel", "rueckschlag"],
"momente": ["gruebelt", "ruht", "angespannt"],
"farbe": Color(1.0, 0.80, 0.60),
},
{
"id": "schlaffenster",
"name": "Schlaffenster festlegen",
"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.",
"tags": ["erschoepfung", "gesundheit"],
"momente": ["abendroutine", "muede", "ruht"],
"farbe": Color(0.45, 0.50, 0.90),
},
{
"id": "hilfe_bitten",
"name": "Konkret um Hilfe bitten",
"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.",
"tags": ["einsamkeit", "ueberforderung"],
"momente": ["wartet", "pause", "unterwegs"],
"farbe": Color(0.90, 0.90, 0.45),
},
{
"id": "nein_sagen",
"name": "Nein sagen mit Alternative",
"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.",
"tags": ["stress", "grenzen"],
"momente": ["arbeitet", "angespannt", "unterwegs"],
"farbe": Color(0.85, 0.35, 0.45),
},
]
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(HumanDB.activity_text(m))
return ", ".join(out)
static func by_id(id: String) -> Dictionary:
for t in LIST:
if t["id"] == id:
return t
return {}
static func ids() -> Array:
var out: Array = []
for t in LIST:
out.append(t["id"])
return out

View File

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

431
scripts/ui.gd Normal file
View File

@@ -0,0 +1,431 @@
extends CanvasLayer
class_name UI
signal panel_opened
signal panel_closed
const BREITE := 900
var offen := false
var _kind := ""
var _hud: RichTextLabel
var _hinweis: Label
var _log: VBoxContainer
var _dim: ColorRect
var _panel: PanelContainer
var _titel: Label
var _content: VBoxContainer
var _scroll: ScrollContainer
var _apply_human: Node3D
var _on_travel: Callable
func _ready() -> void:
layer = 10
_baue_hud()
_baue_panel()
Game.notified.connect(_notiz)
Game.state_changed.connect(_hud_aktualisieren)
_hud_aktualisieren()
func _baue_hud() -> void:
_hud = RichTextLabel.new()
_hud.bbcode_enabled = true
_hud.fit_content = true
_hud.scroll_active = false
_hud.set_anchors_preset(Control.PRESET_TOP_LEFT)
_hud.position = Vector2(18, 14)
_hud.custom_minimum_size = Vector2(420, 130)
_hud.size = Vector2(420, 130)
_hud.mouse_filter = Control.MOUSE_FILTER_IGNORE
_hud.add_theme_font_size_override("normal_font_size", 17)
_hud.add_theme_font_size_override("bold_font_size", 17)
add_child(_hud)
var steuerung := Label.new()
steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · Tab Kodex · Esc Maus frei"
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
steuerung.position = Vector2(-640, 16)
steuerung.size = Vector2(620, 24)
steuerung.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
steuerung.add_theme_font_size_override("font_size", 14)
steuerung.add_theme_color_override("font_color", Color(1, 1, 1, 0.65))
steuerung.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(steuerung)
_hinweis = Label.new()
_hinweis.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
_hinweis.position = Vector2(-300, -110)
_hinweis.size = Vector2(600, 30)
_hinweis.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_hinweis.add_theme_font_size_override("font_size", 19)
_hinweis.add_theme_color_override("font_color", Color(1, 0.95, 0.75))
_hinweis.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
_hinweis.add_theme_constant_override("outline_size", 6)
_hinweis.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_hinweis)
_log = VBoxContainer.new()
_log.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
_log.position = Vector2(-520, -230)
_log.size = Vector2(500, 200)
_log.alignment = BoxContainer.ALIGNMENT_END
_log.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_log)
func _baue_panel() -> void:
_dim = ColorRect.new()
_dim.color = Color(0, 0, 0, 0.62)
_dim.set_anchors_preset(Control.PRESET_FULL_RECT)
_dim.visible = false
add_child(_dim)
_panel = PanelContainer.new()
_panel.set_anchors_preset(Control.PRESET_CENTER)
_panel.custom_minimum_size = Vector2(BREITE, 200)
_panel.visible = false
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.10, 0.09, 0.16, 0.98)
sb.border_color = Color(0.55, 0.70, 1.0, 0.8)
sb.set_border_width_all(2)
sb.set_corner_radius_all(10)
sb.set_content_margin_all(22)
_panel.add_theme_stylebox_override("panel", sb)
add_child(_panel)
var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 10)
_panel.add_child(vb)
_titel = Label.new()
_titel.add_theme_font_size_override("font_size", 26)
_titel.add_theme_color_override("font_color", Color(1, 0.93, 0.75))
vb.add_child(_titel)
_scroll = ScrollContainer.new()
_scroll.custom_minimum_size = Vector2(BREITE - 44, 140)
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
vb.add_child(_scroll)
var scroll := _scroll
_content = VBoxContainer.new()
_content.custom_minimum_size.x = BREITE - 70
_content.add_theme_constant_override("separation", 10)
scroll.add_child(_content)
var fuss := Label.new()
fuss.text = "Esc schließt"
fuss.add_theme_font_size_override("font_size", 14)
fuss.add_theme_color_override("font_color", Color(1, 1, 1, 0.5))
vb.add_child(fuss)
func _process(_delta: float) -> void:
if not offen:
_hud_aktualisieren()
else:
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.position = (get_viewport().get_visible_rect().size - _panel.size) * 0.5
# ------------------------------------------------------------------ HUD
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()])
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("[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. Missionshalle in der Traumwelt.[/color]")
_hud.text = "\n".join(zeilen)
func setze_hinweis(text: String) -> void:
_hinweis.text = text
func _notiz(text: String, farbe: Color) -> void:
var l := Label.new()
l.text = text
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
l.custom_minimum_size.x = 500
l.add_theme_font_size_override("font_size", 16)
l.add_theme_color_override("font_color", farbe)
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
l.add_theme_constant_override("outline_size", 5)
_log.add_child(l)
while _log.get_child_count() > 6:
var alt := _log.get_child(0)
_log.remove_child(alt)
alt.queue_free()
var tw := l.create_tween()
tw.tween_interval(6.0)
tw.tween_property(l, "modulate:a", 0.0, 1.5)
tw.tween_callback(l.queue_free)
# ------------------------------------------------------------------ Panel
func _leeren(titel: String) -> void:
for c in _content.get_children():
_content.remove_child(c)
c.queue_free()
_titel.text = titel
func _text(bb: String, groesse: int = 17) -> RichTextLabel:
var r := RichTextLabel.new()
r.bbcode_enabled = true
r.fit_content = true
r.scroll_active = false
r.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
r.custom_minimum_size.x = BREITE - 80
r.text = bb
r.add_theme_font_size_override("normal_font_size", groesse)
r.add_theme_font_size_override("bold_font_size", groesse)
_content.add_child(r)
return r
func _knopf(beschriftung: String, cb: Callable, aktiv: bool = true) -> Button:
var b := Button.new()
b.text = beschriftung
b.disabled = not aktiv
b.custom_minimum_size = Vector2(0, 38)
b.add_theme_font_size_override("font_size", 17)
_content.add_child(b)
if aktiv:
b.pressed.connect(cb)
return b
func _trenner() -> void:
var s := HSeparator.new()
_content.add_child(s)
func _zeigen(kind: String) -> void:
_kind = kind
offen = true
_dim.visible = true
_panel.visible = true
_panel.reset_size()
panel_opened.emit()
func schliessen() -> void:
if not offen:
return
offen = false
_kind = ""
_apply_human = null
_dim.visible = false
_panel.visible = false
panel_closed.emit()
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"]])
# -------------------------------------------------------- Bibliothek
func technik_panel(id: String) -> void:
var t := TechDB.by_id(id)
_leeren(t["name"])
_text(_hinweis_block(t))
_trenner()
_text("[color=#9fb6ff]Hilft bei:[/color] %s\n[color=#9fb6ff]Gute Momente:[/color] %s" %
[TechDB.tags_text(t["tags"]), TechDB.momente_text(t["momente"])], 15)
if Game.knows(id):
_text("[color=#88ff99]Gelernt — Stufe %d (%s)[/color]" %
[Game.skill_of(id), Game.xp_to_next(id)])
else:
_text("[color=#ffcc66]Noch nicht gelernt. Kosten: 1 Einsicht (du hast %d).[/color]" %
Game.einsicht)
_knopf("Technik lernen (1 Einsicht)", func() -> void:
if Game.learn(id):
technik_panel(id), Game.einsicht >= 1)
_zeigen("technik")
# -------------------------------------------------------- Missionshalle
func mission_panel(id: String) -> void:
var h := HumanDB.by_id(id)
_leeren("%s, %d — %s" % [h["name"], h["alter"], h["beruf"]])
_text("[b]Traum:[/b] %s" % h["traum"])
_text("[b]Was im Weg steht:[/b] %s" % h["problem"])
_text("[color=#9fb6ff]Themen:[/color] %s" % TechDB.tags_text(h["tags"]), 15)
_trenner()
if Game.completed.has(id):
_text("[color=#88ff99]★ Der Traum ist erfüllt.[/color]")
elif Game.active_mission == id:
_text("[color=#ffdd77]Aktive Mission — %d %%[/color]" % int(Game.mission_progress(id)))
else:
_text("Fortschritt: %d %%" % int(Game.mission_progress(id)))
_knopf("Mission annehmen", func() -> void:
Game.accept_mission(id)
mission_panel(id))
_zeigen("mission")
# -------------------------------------------------------- Meditation
func vertiefen_panel() -> void:
_leeren("Meditationskammer")
_text("Übung kostet 1 Einsicht und bringt 3 Erfahrungspunkte. Du hast [b]%d[/b] Einsicht." %
Game.einsicht)
_trenner()
if Game.known.is_empty():
_text("[color=#ffcc66]Du kennst noch keine Technik. Geh in die Bibliothek.[/color]")
for id in TechDB.ids():
if not Game.knows(id):
continue
var t := TechDB.by_id(id)
_text("[b]%s[/b] — Stufe %d (%s)" % [t["name"], Game.skill_of(id), Game.xp_to_next(id)], 16)
_knopf("Vertiefen (1 Einsicht)", func() -> void:
if Game.deepen(id):
vertiefen_panel(), Game.einsicht >= 1)
_zeigen("vertiefen")
# -------------------------------------------------------- Kodex
func kodex_panel() -> void:
_leeren("Kodex der Techniken")
if Game.known.is_empty():
_text("Du kennst noch keine Technik. In der Bibliothek der Traumwelt liegen zwölf.")
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()
var fehlend := TechDB.LIST.size() - Game.known.size()
if fehlend > 0:
_text("[color=#aaaaaa]%d weitere Techniken warten in der Bibliothek.[/color]" % fehlend, 15)
_zeigen("kodex")
# -------------------------------------------------------- Anwenden auf der Erde
func anwenden_panel(mensch: Node3D) -> void:
_apply_human = mensch
var h: Dictionary = mensch.data
var aktiv: String = mensch.activity
_leeren("%s — %s" % [h["name"], HumanDB.activity_text(aktiv)])
_text("[b]Traum:[/b] %s" % h["traum"], 16)
if Game.active_mission == h["id"]:
_text("Fortschritt: [b]%d %%[/b]" % int(Game.mission_progress(h["id"])), 16)
elif Game.completed.has(h["id"]):
_text("[color=#88ff99]★ Traum erfüllt.[/color]", 16)
else:
_text("[color=#ffcc66]Keine aktive Mission für diesen Menschen. " +
"Fortschritt zählt nur mit angenommener Mission.[/color]", 15)
_trenner()
if Game.known.is_empty():
_text("[color=#ffcc66]Du kennst keine Technik.[/color]")
_zeigen("anwenden")
return
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"])
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()
_zeigen("anwenden")
func _anwenden(id: String) -> void:
if _apply_human == null or not is_instance_valid(_apply_human):
schliessen()
return
var mensch := _apply_human
var h: Dictionary = mensch.data
var res := Game.apply_technique(id, h["id"], mensch.activity, mensch.resist_left > 0.0)
schliessen()
if not res["ok"]:
Game.notified.emit(res["grund"], Color(1, 0.6, 0.4))
return
var t := TechDB.by_id(id)
if res["success"]:
mensch.reagiere(true, "%s wirkt\n+%d %%" % [t["name"], int(round(float(res["gain"])))])
Game.notified.emit("%s erreicht %s. (+%d %%)" %
[t["name"], h["name"], int(round(float(res["gain"])))], Color(0.6, 1, 0.7))
if res["wiederholung"] >= 2:
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))
# -------------------------------------------------------- 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]In der Traumwelt[/b]\n" +
"· [b]Bibliothek der Techniken[/b] — lerne echte Lösungswege. Jede Technik sagt dir, " +
"was sie tut, wann sie passt und warum sie hilft.\n" +
"· [b]Missionshalle[/b] — nimm einen Menschen an, dem du helfen willst.\n" +
"· [b]Meditationskammer[/b] — vertiefe, was du schon kannst.\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()
get_viewport().set_input_as_handled()

1
scripts/ui.gd.uid Normal file
View File

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