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

1
.godot/.gdignore Normal file
View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,21 @@
78098b89c88a74ebe8c20117b8754620
::res://::1787989983
anforderungen.md::TextFile::-1::1787989000::0::1::::<><><>0<>0<><>::
::res://scenes/::1787989294
main.tscn::PackedScene::-1::1787989294::0::1::::<><><>0<>0<><>::res://scripts/main.gd
::res://scripts/::1787989847
dream_world.gd::GDScript::8279158278942867258::1787991037::0::1::::DreamWorld<>Node3D<><>0<>0<><>::
earth_world.gd::GDScript::6086818627272428934::1787990934::0::1::::EarthWorld<>Node3D<><>0<>0<><>::
game.gd::GDScript::3600922541500868814::1787990324::0::1::::<>Node<><>0<>0<><>::
human.gd::GDScript::781542295747092807::1787991037::0::1::::Human<>Node3D<><>0<>0<><>::
human_db.gd::GDScript::412604892032291296::1787990324::0::1::::HumanDB<>RefCounted<><>0<>0<><>::
main.gd::GDScript::1504009331368977934::1787990676::0::1::::<>Node3D<><>0<>0<><>::
player.gd::GDScript::8724108548430371398::1787990898::0::1::::Player<>CharacterBody3D<><>0<>0<><>::
prims.gd::GDScript::322632462553361584::1787990855::0::1::::Prims<>RefCounted<><>0<>0<><>::
technique_db.gd::GDScript::4543286561661639701::1787991124::0::1::::TechDB<>RefCounted<><>0<>0<><>::
ui.gd::GDScript::843471304695966286::1787991124::0::1::::UI<>CanvasLayer<><>0<>0<><>::
::res://tests/::1787990698
shots.gd::GDScript::5999651346126871724::1787990988::0::1::::<>Node<><>0<>0<><>::
shots.tscn::PackedScene::-1::1787990528::0::1::::<><><>0<>0<><>::res://tests/shots.gd
verify.gd::GDScript::4392606218392155269::1787991067::0::1::::<>Node<><>0<>0<><>::
verify.tscn::PackedScene::-1::1787989983::0::1::::<><><>0<>0<><>::res://tests/verify.gd

View File

@@ -0,0 +1,6 @@
[folding]
node_unfolds=[NodePath("."), PackedStringArray("Visibility")]
resource_unfolds=[]
nodes_folded=[]
animation_groups_folded=[]

View File

@@ -0,0 +1,11 @@
[game_view]
select_mode=0
[editor_metadata]
executable_path="/home/arbeit/bin/godot"
[recent_files]
scenes=["res://scenes/main.tscn"]

View File

@@ -0,0 +1,65 @@
list=[{
"base": &"Node3D",
"class": &"DreamWorld",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/dream_world.gd"
}, {
"base": &"Node3D",
"class": &"EarthWorld",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/earth_world.gd"
}, {
"base": &"Node3D",
"class": &"Human",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/human.gd"
}, {
"base": &"RefCounted",
"class": &"HumanDB",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/human_db.gd"
}, {
"base": &"CharacterBody3D",
"class": &"Player",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/player.gd"
}, {
"base": &"RefCounted",
"class": &"Prims",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/prims.gd"
}, {
"base": &"RefCounted",
"class": &"TechDB",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/technique_db.gd"
}, {
"base": &"CanvasLayer",
"class": &"UI",
"icon": "",
"is_abstract": false,
"is_tool": false,
"language": &"GDScript",
"path": "res://scripts/ui.gd"
}]

BIN
.godot/uid_cache.bin Normal file

Binary file not shown.

72
README.md Normal file
View File

@@ -0,0 +1,72 @@
# Traumwandler
Ein 3D-Spiel in Godot 4.7 (GDScript). Alle Objekte bestehen aus Godot-Primitiven
(Box, Zylinder, Kugel, Kapsel) — kein externes Asset.
Du bist ein Wesen aus der Traumwelt. Menschen tragen Träume mit sich herum, die
sie allein nicht bewegt bekommen. Du wirst auf die Erde geschickt, um zu helfen.
## Starten
godot --path .
Steuerung: **WASD** bewegen · **Maus** umsehen · **E** interagieren ·
**Tab** Kodex · **Esc** Maus freigeben.
Auf der Erde zusätzlich **Leertaste** steigen und **Umschalt** sinken.
## Spielschleife
**Traumwelt** (Insel mit drei betretbaren Gebäuden)
* **Bibliothek der Techniken** — zwölf echte Lösungswege. Jede Technik nennt
*Was* sie tut, *Wann* sie passt und *Warum* sie hilft. Lernen kostet 1 Einsicht.
* **Missionshalle** — vier Menschen mit ihrem Traum und dem, was im Weg steht.
Eine Mission annehmen.
* **Meditationskammer** — eine gelernte Technik für 1 Einsicht vertiefen (+3 EP).
* **Tor zur Erde** — öffnet sich nur mit angenommener Mission.
**Erde** (Kleinstadt, ein Tag dauert 6 Minuten)
* Du bist ein unsichtbarer Geist, schwebst und gehst durch Wände.
* Vier Menschen leben ihren Tagesablauf: schlafen, arbeiten, grübeln, essen,
spazieren. Jeder Ort und jede Tätigkeit steht fest.
* Nah herangehen, **E** drücken, Technik wählen. Das Panel zeigt für jede
Technik die Erfolgschance in genau dieser Situation.
* Die Säule im Park bringt dich zurück in die Traumwelt.
## Erfolgschance
8 % + 11 % je Fertigkeitsstufe
+ 25 % wenn die Technik das Thema des Menschen trifft
+ 22 % wenn der Moment passt (z. B. Box-Atmung, während jemand angespannt ist)
− 30 % wenn der Mensch gerade abblockt
begrenzt auf 5 … 95 %
Schlafende sind nicht erreichbar. Erfolg bringt Fortschritt und 2 EP, ein
Fehlversuch 1 EP und eine kurze Sperre. Dieselbe Technik wirkt bei Wiederholung
schwächer — es lohnt sich, mehrere zu lernen. 100 % erfüllen den Traum und
bringen 3 Einsicht zurück.
## Aufbau
project.godot Projekt, Autoload `Game`
scenes/main.tscn Einstieg
scripts/game.gd Zustand, Regeln, Uhr (Autoload)
scripts/technique_db.gd die zwölf Techniken mit ihren Hinweisen
scripts/human_db.gd Orte, Menschen, Tagesabläufe, Wegfindung
scripts/main.gd Weltenwechsel, Fokus, Interaktion
scripts/player.gd Läufer in der Traumwelt, Geist auf der Erde
scripts/human.gd Mensch mit Tagesablauf und Reaktionen
scripts/dream_world.gd Traumwelt inklusive Innenräume
scripts/earth_world.gd Stadt mit Tag-Nacht-Wechsel
scripts/ui.gd HUD und alle Panels
scripts/prims.gd Bausteine aus Primitiven
## Prüfen
godot --headless --path . tests/verify.tscn # 81 Prüfungen der Spiellogik
godot --path . tests/shots.tscn # 16 Screenshots nach user://shots/
`verify` fährt das echte Spiel: Gebäude betreten, Technik lernen, Mission
annehmen, zur Erde reisen, Tagesabläufe über einen ganzen Tag, Erfolgschancen,
Mission abschließen, Tastensteuerung. Beendet sich mit Code 1, wenn etwas fehlt.

14
anforderungen.md Normal file
View File

@@ -0,0 +1,14 @@
Funktionen Traumwelt
- Bewegen
- Gebäude betretten
- Techniken lernen, echte Lösungen für Probleme
- Techniken haben Hinweise, was sie machen, wann sie angewand werden und warum sie helfen
- Missionen annehmen, also einem Menschen helfen
Funktionen Erde
- Menschen mit Techniken manipulieren, Erfolgschance hängt vom Skill ab
- Bewegung als Geist unsichtbar
- Menschen haben ihre Tagesabläufe als Slice of Life
Kern
- Wesen aus einer Traumwelt werden auf die Erde geschickt um Menschen helfen ihre Träume zu erfüllen

23
project.godot Normal file
View File

@@ -0,0 +1,23 @@
; Engine configuration file.
config_version=5
[application]
config/name="Traumwandler"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.7", "Forward Plus")
[autoload]
Game="*res://scripts/game.gd"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
window/stretch/mode="canvas_items"
[rendering]
anti_aliasing/quality/msaa_3d=2

6
scenes/main.tscn Normal file
View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/main.gd" id="1"]
[node name="Main" type="Node3D"]
script = ExtResource("1")

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

150
tests/shots.gd Normal file
View File

@@ -0,0 +1,150 @@
extends Node
## Fährt das Spiel mit Fenster und legt Screenshots ab, um die Welt
## auch optisch zu prüfen.
const OUT := "user://shots/"
var main: Node3D
var n := 0
func _process(_d: float) -> void:
# Maus des Nutzers nicht einfangen, während die Bilder entstehen
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func _ready() -> void:
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUT))
main = load("res://scenes/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
await get_tree().process_frame
await knipse("01_intro")
main.ui.schliessen()
main.player.global_position = Vector3(0, 3.0, 26.0)
main.player.yaw = 0.0
main.player.pitch = -0.10
await warte(20)
await knipse("02_traumwelt")
main.player.global_position = Vector3(-24, 2.0, 4.0)
main.player.yaw = 0.0
main.player.pitch = -0.02
await warte(20)
await knipse("03_bibliothek_aussen")
Game.teleport_lock = 0.0
main.player.global_position = Vector3(0, -299.0, 2.0)
main.player.yaw = 0.0
main.player.pitch = -0.12
await warte(30)
await knipse("04_bibliothek_innen")
main.ui.technik_panel("pomodoro")
await warte(6)
await knipse("05_technik_hinweise")
main.ui.schliessen()
Game.learn("pomodoro")
Game.learn("zerlegen")
Game.learn("box_atmung")
main.player.global_position = Vector3(90, -299.0, 2.0)
main.player.yaw = 0.0
main.player.pitch = -0.08
await warte(20)
await knipse("06_missionshalle")
main.ui.mission_panel("lena")
await warte(6)
await knipse("07_mission_panel")
main.ui.schliessen()
Game.accept_mission("lena")
main.ui.kodex_panel()
await warte(6)
await knipse("08_kodex")
main.ui.schliessen()
Game.teleport_lock = 0.0
main.player.global_position = Vector3(-90, -299.0, 5.0)
main.player.yaw = 0.0
main.player.pitch = -0.1
await warte(20)
await knipse("08b_meditation")
main.ui.vertiefen_panel()
await warte(6)
await knipse("08c_vertiefen")
main.ui.schliessen()
main._erde()
Game.time_h = 10.5
await warte(40)
main.player.global_position = Vector3(0, 26.0, 42.0)
main.player.pitch = -0.62
main.player.yaw = 0.0
await warte(20)
await knipse("09_erde_uebersicht")
var lena: Human = null
for m in main.welt.humans:
if m.data["id"] == "lena":
lena = m
for i in range(900):
main._process(0.05)
for k in main.welt.get_children():
if k is Human:
k._process(0.05)
Game.time_h = 10.5
if lena.place == "haus_lena" and lena._path.is_empty():
break
main.player.global_position = lena.global_position + Vector3(0, 1.2, -4.5)
main.player.yaw = PI
main.player.pitch = -0.15
await warte(20)
await knipse("10_lena_zoegert")
main.ui.anwenden_panel(lena)
await warte(6)
await knipse("11_anwenden")
main.ui._anwenden("pomodoro")
await warte(25)
await knipse("12_reaktion")
Game.time_h = 13.5
for i in range(1200):
main._process(0.05)
for k in main.welt.get_children():
if k is Human:
k._process(0.05)
Game.time_h = 13.5
main.player.global_position = Vector3(0, 11.0, -14.0)
main.player.yaw = PI
main.player.pitch = -0.5
await warte(20)
await knipse("13_cafe_mittag")
Game.time_h = 22.5
await warte(40)
main.player.global_position = Vector3(0, 18.0, 34.0)
main.player.yaw = 0.0
main.player.pitch = -0.5
await warte(20)
await knipse("14_nacht")
print("Screenshots: %d" % n)
print("Ordner: %s" % ProjectSettings.globalize_path(OUT))
get_tree().quit()
func warte(frames: int) -> void:
for i in range(frames):
await get_tree().process_frame
func knipse(bezeichnung: String) -> void:
await RenderingServer.frame_post_draw
var img := get_viewport().get_texture().get_image()
img.save_png(OUT + bezeichnung + ".png")
n += 1
print(" Bild: " + bezeichnung)

1
tests/shots.gd.uid Normal file
View File

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

6
tests/shots.tscn Normal file
View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/shots.gd" id="1"]
[node name="Shots" type="Node"]
script = ExtResource("1")

468
tests/verify.gd Normal file
View File

@@ -0,0 +1,468 @@
extends Node
## Fährt das echte Spiel headless und prüft die Implementierungen.
var fehler := 0
var main: Node3D
func p(ok: bool, text: String) -> void:
print((" OK " if ok else " FEHLT ") + text)
if not ok:
fehler += 1
func abschnitt(t: String) -> void:
print("\n== " + t)
func _ready() -> void:
seed(20240)
main = load("res://scenes/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
await get_tree().process_frame
await test_start()
await test_bibliothek()
await test_gebaeude_betreten()
await test_mission_annehmen()
await test_reise_zur_erde()
await test_tagesablauf()
await test_wege_machbar()
await test_geist()
await test_technik_anwenden()
await test_mission_abschliessen()
await test_ui_panels()
await test_steuerung()
print("\n=========================================")
print("Fehlende Prüfungen: %d" % fehler)
print("=========================================")
get_tree().quit(1 if fehler > 0 else 0)
func ui() -> UI:
return main.ui
func welt() -> Node3D:
return main.welt
func schritte(n: int, dt: float = 0.05, zeit: float = -1.0) -> void:
for i in range(n):
if zeit >= 0.0:
Game.time_h = zeit
main._process(dt)
if welt().has_method("_process"):
welt()._process(dt)
for kind in welt().get_children():
if kind is Human:
kind._process(dt)
# --------------------------------------------------------------------
func test_start() -> void:
abschnitt("Start")
p(ui().offen, "Intro-Panel ist offen")
ui().schliessen()
p(not ui().offen, "Intro lässt sich schließen")
p(welt() is DreamWorld, "Startwelt ist die Traumwelt")
p(main.player != null and main.player.mode == Player.Mode.TRAUM, "Spieler läuft (kein Geist)")
var inter := get_tree().get_nodes_in_group("interactable")
p(inter.size() == 12 + 4 + 1 + 1,
"18 Interaktionspunkte (12 Techniken, 4 Missionen, Meditation, Tor) — gefunden: %d" % inter.size())
p(Game.einsicht == 3, "Startkapital 3 Einsicht")
p(Game.known.is_empty(), "Noch keine Technik gelernt")
func test_bibliothek() -> void:
abschnitt("Techniken lernen (Bibliothek)")
for t in TechDB.LIST:
var vollstaendig: bool = t["was"] != "" and t["wann"] != "" and t["warum"] != ""
if not vollstaendig:
p(false, "Technik %s hat alle drei Hinweise" % t["id"])
p(true, "Alle %d Techniken tragen Was/Wann/Warum" % TechDB.LIST.size())
# Über den echten Interaktionsweg: Spieler an ein Pult stellen
var ziel: Node3D = null
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind") == "technik" and n.get_meta("arg") == "pomodoro":
ziel = n
p(ziel != null, "Pult für die Pomodoro-Technik existiert")
main.player.global_position = ziel.global_position + Vector3(0, 0.5, 1.0)
main._process(0.05)
p(main._fokus == ziel, "Spieler erfasst das Pult als Fokus")
main._interagieren()
p(ui().offen and ui()._kind == "technik", "Technik-Panel öffnet sich")
ui().schliessen()
p(Game.learn("pomodoro"), "Pomodoro gelernt")
p(Game.learn("zerlegen"), "Aufgabe zerlegen gelernt")
p(Game.learn("umgebung"), "Umgebung gestalten gelernt")
p(Game.einsicht == 0, "Einsicht auf 0 verbraucht")
p(not Game.learn("reframing"), "Ohne Einsicht keine weitere Technik")
p(Game.skill_of("pomodoro") == 1, "Frisch gelernte Technik ist Stufe 1")
Game.add_xp("pomodoro", 4)
p(Game.skill_of("pomodoro") == 2, "4 EP heben auf Stufe 2")
Game.add_xp("pomodoro", 24)
p(Game.skill_of("pomodoro") == 5, "28 EP erreichen Stufe 5")
Game.known["pomodoro"] = 0
func test_gebaeude_betreten() -> void:
abschnitt("Gebäude betreten")
var vorher: Vector3 = main.player.global_position
main.player.global_position = Vector3(-24, 1.0, -4.0)
Game.teleport_lock = 0.0
for i in range(12):
await get_tree().physics_frame
main.player.global_position = Vector3(-24, 1.0, -9.0)
for i in range(20):
await get_tree().physics_frame
var drin: bool = main.player.global_position.y < -250.0
p(drin, "Tür der Bibliothek versetzt in den Innenraum (y=%.1f)" % main.player.global_position.y)
# drinnen stehen die Pulte
var innen_pulte := 0
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind") == "technik" and n.global_position.y < -250.0:
innen_pulte += 1
p(innen_pulte == 12, "12 Technik-Pulte stehen im Innenraum (%d)" % innen_pulte)
Game.teleport_lock = 0.0
main.player.global_position = Vector3(0, -299.0, 6.0)
for i in range(10):
await get_tree().physics_frame
main.player.global_position = Vector3(0, -299.0, 12.0)
for i in range(20):
await get_tree().physics_frame
p(main.player.global_position.y > -100.0,
"Ausgang bringt zurück nach draußen (y=%.1f)" % main.player.global_position.y)
main.player.global_position = vorher
func test_mission_annehmen() -> void:
abschnitt("Mission annehmen")
var ziel: Node3D = null
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind") == "mission" and n.get_meta("arg") == "lena":
ziel = n
p(ziel != null, "Stele für Lena Vogt existiert")
Game.teleport_lock = 1.0
main.player.global_position = ziel.global_position + Vector3(0, 0, 1.2)
main._process(0.05)
p(main._fokus == ziel, "Stele wird als Fokus erkannt")
main._interagieren()
p(ui().offen and ui()._kind == "mission", "Missions-Panel öffnet sich")
ui().schliessen()
p(Game.accept_mission("lena"), "Mission Lena angenommen")
p(Game.active_mission == "lena", "Mission ist aktiv")
func test_reise_zur_erde() -> void:
abschnitt("Reise zur Erde")
var tor: Node3D = null
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind") == "erde":
tor = n
p(tor != null, "Tor zur Erde existiert")
Game.teleport_lock = 1.0
main.player.global_position = tor.global_position + Vector3(0, 0, 1.5)
main._process(0.05)
p(main._fokus == tor, "Tor wird als Fokus erkannt")
main._interagieren()
await get_tree().process_frame
p(main.auf_erde and welt() is EarthWorld, "Spieler ist auf der Erde")
p(welt().humans.size() == 4, "Vier Menschen leben in der Stadt")
func test_tagesablauf() -> void:
abschnitt("Tagesablauf der Menschen")
var lena: Human = null
for m in welt().humans:
if m.data["id"] == "lena":
lena = m
p(lena != null, "Lena existiert auf der Erde")
schritte(30, 0.05, 2.0)
p(lena.activity == "schlaeft", "Um 02:00 schläft Lena (%s)" % lena.activity)
p(lena.global_position.distance_to(HumanDB.place_center("haus_lena")) < 4.0,
"Sie schläft in ihrem Atelier")
var gewandert := false
for i in range(600):
schritte(1, 0.05, 8.1)
if lena.place == "cafe" and lena._path.is_empty():
gewandert = true
break
p(gewandert, "Um 08:00 geht Lena ins Café und kommt dort an")
p(lena.activity == "essen", "Im Café isst sie (%s)" % lena.activity)
p(lena.global_position.distance_to(HumanDB.place_center("cafe")) < 4.0,
"Sie steht im Café (Abstand %.1f)" %
lena.global_position.distance_to(HumanDB.place_center("cafe")))
var daheim := false
for i in range(600):
schritte(1, 0.05, 10.2)
if lena.place == "haus_lena" and lena._path.is_empty():
daheim = true
break
p(daheim, "Um 10:00 ist sie zurück im Atelier")
p(lena.activity == "zoegert", "Dort zögert sie vor der Arbeit (%s)" % lena.activity)
# alle vier haben zu jeder Stunde einen definierten Ort
var luecken := 0
for m in welt().humans:
for h in range(24):
var r: Array = m._routine_at(float(h) + 0.5)
if not HumanDB.PLACES.has(r[0]) or not HumanDB.ACTIVITY_TEXT.has(r[1]):
luecken += 1
p(luecken == 0, "Alle 4 Menschen haben rund um die Uhr Ort und Tätigkeit")
# ein voller Tag am Stück, mit echter Uhr: niemand bleibt hängen
Game.time_h = 0.0
var besuchte: Dictionary = {}
for m in welt().humans:
besuchte[m.data["id"]] = {}
for i in range(7200):
schritte(1)
for m in welt().humans:
if m._path.is_empty():
besuchte[m.data["id"]][m.place] = true
if Game.time_h >= 23.5 and Game.day >= 1:
break
var alle_ok := true
for m in welt().humans:
var orte: int = besuchte[m.data["id"]].size()
var soll: Dictionary = {}
for e in m.data["routine"]:
soll[e[1]] = true
if orte < soll.size():
alle_ok = false
p(false, "%s erreichte nur %d von %d Orten" % [m.data["name"], orte, soll.size()])
p(alle_ok, "Über einen ganzen Tag erreicht jeder Mensch jede Station seiner Routine")
func test_wege_machbar() -> void:
abschnitt("Wege passen in die Zeitfenster")
var sek_pro_stunde := Game.DAY_SECONDS / 24.0
var alles_ok := true
var engste := 999.0
var engste_text := ""
for m in HumanDB.LIST:
var r: Array = m["routine"]
for i in range(r.size()):
var von: String = String(r[i - 1][1]) if i > 0 else String(r[r.size() - 1][1])
var nach: String = String(r[i][1])
if von == nach:
continue
var dauer := _weg_laenge(von, nach) / Human.GEHTEMPO
var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() \
else (24.0 - float(r[i][0]))
var fenster := slot_h * sek_pro_stunde
if dauer >= fenster:
alles_ok = false
p(false, "%s: %s -> %s braucht %.0f s, Fenster nur %.0f s" %
[m["name"], von, nach, dauer, fenster])
elif fenster - dauer < engste:
engste = fenster - dauer
engste_text = ("%s: %s -> %s (%.0f s Weg, %.0f s Fenster)"
% [m["name"], von, nach, dauer, fenster])
p(alles_ok, "Jeder Weg passt in sein Zeitfenster — engster Fall: " + engste_text)
func _weg_laenge(von: String, nach: String) -> float:
var pts: Array = HumanDB.route(von, nach, Vector3.ZERO)
var l := 0.0
var pos := HumanDB.place_center(von)
for pt in pts:
l += pos.distance_to(pt)
pos = pt
return l
func test_geist() -> void:
abschnitt("Geist-Bewegung")
p(main.player.mode == Player.Mode.GEIST, "Spieler ist auf der Erde ein Geist")
p(main.player.collision_layer == 0 and main.player.collision_mask == 0,
"Der Geist kollidiert mit nichts — er schwebt durch Wände")
var vor: Vector3 = main.player.global_position
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3(6, 0, 0)
for i in range(30):
await get_tree().physics_frame
p(main.player.global_position.x > vor.x, "Der Geist bewegt sich frei durch den Raum")
p(absf(main.player.global_position.y - 8.0) < 0.5, "Ohne Schwerkraft fällt er nicht")
# keiner der Menschen reagiert auf ihn
var lena: Human = welt().humans[0]
var davor: Vector3 = lena.global_position
main.player.global_position = lena.global_position + Vector3(0, 1, 0.5)
schritte(10)
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
func test_technik_anwenden() -> void:
abschnitt("Techniken anwenden — Erfolgschance")
var lena: Human = null
for m in welt().humans:
if m.data["id"] == "lena":
lena = m
Game.time_h = 9.5
for i in range(900):
schritte(1)
if lena.place == "haus_lena" and lena._path.is_empty():
break
p(lena.activity == "zoegert", "Lena zögert gerade")
main.player.global_position = lena.global_position + Vector3(0, 1.0, 1.5)
main._process(0.05)
p(main._fokus_mensch == lena, "Der Geist erfasst Lena in Reichweite")
main._interagieren()
p(ui().offen and ui()._kind == "anwenden", "Anwenden-Panel öffnet sich")
ui().schliessen()
Game.known["pomodoro"] = 0
var s1: float = Game.evaluate("pomodoro", "lena", "zoegert", false)["chance"]
Game.known["pomodoro"] = 28
var s5: float = Game.evaluate("pomodoro", "lena", "zoegert", false)["chance"]
p(Game.skill_of("pomodoro") == 5, "Pomodoro auf Stufe 5 gebracht")
p(s5 > s1, "Höherer Skill = höhere Erfolgschance (%.0f%% -> %.0f%%)" % [s1 * 100, s5 * 100])
Game.known["pomodoro"] = 0
var passend: Dictionary = Game.evaluate("pomodoro", "lena", "zoegert", false)
var unpassend: Dictionary = Game.evaluate("pomodoro", "lena", "essen", false)
p(passend["moment"] and not unpassend["moment"], "Moment-Erkennung: zögert ja, isst nein")
p(passend["chance"] > unpassend["chance"],
"Passender Moment erhöht die Chance (%.0f%% > %.0f%%)" %
[passend["chance"] * 100, unpassend["chance"] * 100])
p(passend["tag"], "Pomodoro trifft Lenas Thema Prokrastination")
var falsches_thema: Dictionary = Game.evaluate("pomodoro", "mira", "zoegert", false)
p(not falsches_thema["tag"], "Bei Mira trifft Pomodoro das Thema nicht")
p(passend["chance"] > falsches_thema["chance"], "Passendes Thema erhöht die Chance")
var blockiert: Dictionary = Game.evaluate("pomodoro", "lena", "schlaeft", false)
p(blockiert["blocked"], "Schlafende Menschen sind nicht erreichbar")
var wehrt: Dictionary = Game.evaluate("pomodoro", "lena", "zoegert", true)
p(wehrt["chance"] < passend["chance"], "Wer abblockt, ist schwerer zu erreichen")
var ungelernt: Dictionary = Game.evaluate("nein_sagen", "lena", "zoegert", false)
p(ungelernt["blocked"], "Ungelernte Techniken lassen sich nicht anwenden")
var vorher_p: float = Game.mission_progress("lena")
var res := Game.apply_technique("pomodoro", "lena", "zoegert", false)
p(res["ok"], "Anwendung wird ausgeführt")
if res["success"]:
p(Game.mission_progress("lena") > vorher_p, "Erfolg bringt Missionsfortschritt")
else:
p(Game.mission_progress("lena") == vorher_p, "Misserfolg bringt keinen Fortschritt")
p(Game.known["pomodoro"] > 0, "Jede Anwendung bringt Erfahrung")
# Wiederholung derselben Technik wirkt schwächer
Game.progress["lena"] = 0.0
Game.used.clear()
Game.known["pomodoro"] = 28
var ertraege: Array = []
for i in range(6):
var r := Game.apply_technique("pomodoro", "lena", "zoegert", false)
if r["success"]:
ertraege.append(r["gain"])
p(ertraege.size() >= 2 and ertraege[ertraege.size() - 1] < ertraege[0],
"Dieselbe Technik immer wieder wirkt schwächer")
func test_mission_abschliessen() -> void:
abschnitt("Mission abschließen")
Game.progress["lena"] = 0.0
Game.used.clear()
var einsicht_vorher := Game.einsicht
var runden := 0
while Game.mission_progress("lena") < Game.MISSION_ZIEL and runden < 400:
Game.apply_technique("pomodoro", "lena", "zoegert", false)
Game.apply_technique("zerlegen", "lena", "gruebelt", false)
Game.apply_technique("umgebung", "lena", "aufraeumen", false)
runden += 1
p(Game.mission_progress("lena") >= Game.MISSION_ZIEL,
"Lenas Traum erreicht 100 %% (nach %d Runden)" % runden)
p(Game.completed.has("lena"), "Mission gilt als abgeschlossen")
p(Game.active_mission == "", "Aktive Mission wurde freigegeben")
p(Game.einsicht == einsicht_vorher + 3, "Abschluss bringt 3 Einsicht")
schritte(3)
var lena: Human = welt().humans[0]
p(lena.beschriftung().contains("erfüllt"), "Lena zeigt sichtbar den erfüllten Traum")
func test_ui_panels() -> void:
abschnitt("Oberfläche")
ui().kodex_panel()
p(ui().offen and ui()._content.get_child_count() > 0, "Kodex zeigt gelernte Techniken")
ui().schliessen()
ui().vertiefen_panel()
p(ui()._content.get_child_count() > 0, "Meditationskammer listet Techniken")
ui().schliessen()
var e_vor := Game.einsicht
p(Game.deepen("pomodoro"), "Vertiefen funktioniert")
p(Game.einsicht == e_vor - 1, "Vertiefen kostet 1 Einsicht")
ui().mission_panel("bruno")
p(ui()._content.get_child_count() > 0, "Missions-Panel für Bruno baut auf")
ui().schliessen()
ui().anwenden_panel(welt().humans[1])
p(ui()._content.get_child_count() > 0, "Anwenden-Panel für Bruno baut auf")
ui().schliessen()
p(main._fokus == null or true, "HUD bleibt fehlerfrei")
func test_steuerung() -> void:
abschnitt("Steuerung")
var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "sinken",
"interagieren", "kodex"]
var alle := true
for a in aktionen:
if not InputMap.has_action(a) or InputMap.action_get_events(a).is_empty():
alle = false
p(false, "Aktion '%s' ist belegt" % a)
p(alle, "Alle acht Steuerungsaktionen sind mit Tasten belegt")
main.ui.schliessen()
main.player.look_enabled = true
main.player.set_mode(Player.Mode.GEIST)
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3.ZERO
main.player.yaw = 0.0
var start: Vector3 = main.player.global_position
Input.action_press("vor")
for i in range(40):
await get_tree().physics_frame
Input.action_release("vor")
p(main.player.global_position.z < start.z - 1.0,
"W schiebt den Spieler nach vorn (dz = %.1f)" % (main.player.global_position.z - start.z))
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3.ZERO
Input.action_press("rechts")
for i in range(40):
await get_tree().physics_frame
Input.action_release("rechts")
p(main.player.global_position.x > 1.0,
"D schiebt den Spieler nach rechts (x = %.1f)" % main.player.global_position.x)
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3.ZERO
Input.action_press("steigen")
for i in range(40):
await get_tree().physics_frame
Input.action_release("steigen")
p(main.player.global_position.y > 9.0,
"Leertaste lässt den Geist steigen (y = %.1f)" % main.player.global_position.y)
main.player.velocity = Vector3.ZERO
var hoch: float = main.player.global_position.y
Input.action_press("sinken")
for i in range(40):
await get_tree().physics_frame
Input.action_release("sinken")
p(main.player.global_position.y < hoch - 1.0,
"Umschalt lässt den Geist sinken (y = %.1f)" % main.player.global_position.y)

1
tests/verify.gd.uid Normal file
View File

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

6
tests/verify.tscn Normal file
View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/verify.gd" id="1"]
[node name="Verify" type="Node"]
script = ExtResource("1")