From 4d771c9a6e0fe8a11ee2506b759920b2d1eadc5d Mon Sep 17 00:00:00 2001 From: team3 Date: Sat, 29 Aug 2026 11:43:53 +0200 Subject: [PATCH] first commit --- .godot/.gdignore | 1 + .godot/editor/filesystem_cache10 | 21 + ...lding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg | 6 + .godot/editor/project_metadata.cfg | 11 + .godot/global_script_class_cache.cfg | 65 +++ .godot/uid_cache.bin | Bin 0 -> 427 bytes README.md | 72 +++ anforderungen.md | 14 + project.godot | 23 + scenes/main.tscn | 6 + scripts/dream_world.gd | 254 ++++++++++ scripts/dream_world.gd.uid | 1 + scripts/earth_world.gd | 185 +++++++ scripts/earth_world.gd.uid | 1 + scripts/game.gd | 223 +++++++++ scripts/game.gd.uid | 1 + scripts/human.gd | 147 ++++++ scripts/human.gd.uid | 1 + scripts/human_db.gd | 206 ++++++++ scripts/human_db.gd.uid | 1 + scripts/main.gd | 169 +++++++ scripts/main.gd.uid | 1 + scripts/player.gd | 150 ++++++ scripts/player.gd.uid | 1 + scripts/prims.gd | 134 +++++ scripts/prims.gd.uid | 1 + scripts/technique_db.gd | 172 +++++++ scripts/technique_db.gd.uid | 1 + scripts/ui.gd | 431 ++++++++++++++++ scripts/ui.gd.uid | 1 + tests/shots.gd | 150 ++++++ tests/shots.gd.uid | 1 + tests/shots.tscn | 6 + tests/verify.gd | 468 ++++++++++++++++++ tests/verify.gd.uid | 1 + tests/verify.tscn | 6 + 36 files changed, 2932 insertions(+) create mode 100644 .godot/.gdignore create mode 100644 .godot/editor/filesystem_cache10 create mode 100644 .godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg create mode 100644 .godot/editor/project_metadata.cfg create mode 100644 .godot/global_script_class_cache.cfg create mode 100644 .godot/uid_cache.bin create mode 100644 README.md create mode 100644 anforderungen.md create mode 100644 project.godot create mode 100644 scenes/main.tscn create mode 100644 scripts/dream_world.gd create mode 100644 scripts/dream_world.gd.uid create mode 100644 scripts/earth_world.gd create mode 100644 scripts/earth_world.gd.uid create mode 100644 scripts/game.gd create mode 100644 scripts/game.gd.uid create mode 100644 scripts/human.gd create mode 100644 scripts/human.gd.uid create mode 100644 scripts/human_db.gd create mode 100644 scripts/human_db.gd.uid create mode 100644 scripts/main.gd create mode 100644 scripts/main.gd.uid create mode 100644 scripts/player.gd create mode 100644 scripts/player.gd.uid create mode 100644 scripts/prims.gd create mode 100644 scripts/prims.gd.uid create mode 100644 scripts/technique_db.gd create mode 100644 scripts/technique_db.gd.uid create mode 100644 scripts/ui.gd create mode 100644 scripts/ui.gd.uid create mode 100644 tests/shots.gd create mode 100644 tests/shots.gd.uid create mode 100644 tests/shots.tscn create mode 100644 tests/verify.gd create mode 100644 tests/verify.gd.uid create mode 100644 tests/verify.tscn diff --git a/.godot/.gdignore b/.godot/.gdignore new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.godot/.gdignore @@ -0,0 +1 @@ + diff --git a/.godot/editor/filesystem_cache10 b/.godot/editor/filesystem_cache10 new file mode 100644 index 0000000..893a66d --- /dev/null +++ b/.godot/editor/filesystem_cache10 @@ -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 diff --git a/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg b/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg new file mode 100644 index 0000000..ae3387e --- /dev/null +++ b/.godot/editor/main.tscn-folding-3ad5c15c4f3250da0cc7c1af1770d85f.cfg @@ -0,0 +1,6 @@ +[folding] + +node_unfolds=[NodePath("."), PackedStringArray("Visibility")] +resource_unfolds=[] +nodes_folded=[] +animation_groups_folded=[] diff --git a/.godot/editor/project_metadata.cfg b/.godot/editor/project_metadata.cfg new file mode 100644 index 0000000..0f52a37 --- /dev/null +++ b/.godot/editor/project_metadata.cfg @@ -0,0 +1,11 @@ +[game_view] + +select_mode=0 + +[editor_metadata] + +executable_path="/home/arbeit/bin/godot" + +[recent_files] + +scenes=["res://scenes/main.tscn"] diff --git a/.godot/global_script_class_cache.cfg b/.godot/global_script_class_cache.cfg new file mode 100644 index 0000000..9a5f0c8 --- /dev/null +++ b/.godot/global_script_class_cache.cfg @@ -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" +}] diff --git a/.godot/uid_cache.bin b/.godot/uid_cache.bin new file mode 100644 index 0000000000000000000000000000000000000000..bf363dfb04cf039ac495df0d2d5f2ea09c9108be GIT binary patch literal 427 zcmd;KU|_J?KmY%OTJZhU!uQBI0pdP>{bv!}dv zR)k=xOHC{)$$+anr*T{KC-+Z7Q4I6a6LV962Dv}{bZGL78(d--3NlJ_6Z3#79t7%4 z`*wFHt0Z<6@hM3_MSRcX*0%)Q7s0eAH!%}rikp5JQ;51U=g@ 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" diff --git a/scripts/dream_world.gd.uid b/scripts/dream_world.gd.uid new file mode 100644 index 0000000..8b7a8ce --- /dev/null +++ b/scripts/dream_world.gd.uid @@ -0,0 +1 @@ +uid://dp7qr6rgsihfu diff --git a/scripts/earth_world.gd b/scripts/earth_world.gd new file mode 100644 index 0000000..3da14eb --- /dev/null +++ b/scripts/earth_world.gd @@ -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) diff --git a/scripts/earth_world.gd.uid b/scripts/earth_world.gd.uid new file mode 100644 index 0000000..08cdf07 --- /dev/null +++ b/scripts/earth_world.gd.uid @@ -0,0 +1 @@ +uid://csyrapuo8qnac diff --git a/scripts/game.gd b/scripts/game.gd new file mode 100644 index 0000000..cc5c364 --- /dev/null +++ b/scripts/game.gd @@ -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"], + } diff --git a/scripts/game.gd.uid b/scripts/game.gd.uid new file mode 100644 index 0000000..854a814 --- /dev/null +++ b/scripts/game.gd.uid @@ -0,0 +1 @@ +uid://brkkwynipq0by diff --git a/scripts/human.gd b/scripts/human.gd new file mode 100644 index 0000000..370b12f --- /dev/null +++ b/scripts/human.gd @@ -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))) diff --git a/scripts/human.gd.uid b/scripts/human.gd.uid new file mode 100644 index 0000000..ae2339f --- /dev/null +++ b/scripts/human.gd.uid @@ -0,0 +1 @@ +uid://let5f4ntq0j8 diff --git a/scripts/human_db.gd b/scripts/human_db.gd new file mode 100644 index 0000000..b2f73ab --- /dev/null +++ b/scripts/human_db.gd @@ -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) diff --git a/scripts/human_db.gd.uid b/scripts/human_db.gd.uid new file mode 100644 index 0000000..5532b1b --- /dev/null +++ b/scripts/human_db.gd.uid @@ -0,0 +1 @@ +uid://f44so5e502ga diff --git a/scripts/main.gd b/scripts/main.gd new file mode 100644 index 0000000..6b25f28 --- /dev/null +++ b/scripts/main.gd @@ -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() diff --git a/scripts/main.gd.uid b/scripts/main.gd.uid new file mode 100644 index 0000000..c488038 --- /dev/null +++ b/scripts/main.gd.uid @@ -0,0 +1 @@ +uid://vos2w6psagbu diff --git a/scripts/player.gd b/scripts/player.gd new file mode 100644 index 0000000..4e54dde --- /dev/null +++ b/scripts/player.gd @@ -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)) diff --git a/scripts/player.gd.uid b/scripts/player.gd.uid new file mode 100644 index 0000000..f68c862 --- /dev/null +++ b/scripts/player.gd.uid @@ -0,0 +1 @@ +uid://dwka176k6h36q diff --git a/scripts/prims.gd b/scripts/prims.gd new file mode 100644 index 0000000..101c3a4 --- /dev/null +++ b/scripts/prims.gd @@ -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) diff --git a/scripts/prims.gd.uid b/scripts/prims.gd.uid new file mode 100644 index 0000000..b36c72d --- /dev/null +++ b/scripts/prims.gd.uid @@ -0,0 +1 @@ +uid://eujyehasisce diff --git a/scripts/technique_db.gd b/scripts/technique_db.gd new file mode 100644 index 0000000..87e7422 --- /dev/null +++ b/scripts/technique_db.gd @@ -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 diff --git a/scripts/technique_db.gd.uid b/scripts/technique_db.gd.uid new file mode 100644 index 0000000..07af210 --- /dev/null +++ b/scripts/technique_db.gd.uid @@ -0,0 +1 @@ +uid://b5y2jbmp3ix38 diff --git a/scripts/ui.gd b/scripts/ui.gd new file mode 100644 index 0000000..756f0bf --- /dev/null +++ b/scripts/ui.gd @@ -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() diff --git a/scripts/ui.gd.uid b/scripts/ui.gd.uid new file mode 100644 index 0000000..5cafc50 --- /dev/null +++ b/scripts/ui.gd.uid @@ -0,0 +1 @@ +uid://mat34vhiwx2m diff --git a/tests/shots.gd b/tests/shots.gd new file mode 100644 index 0000000..121b8a0 --- /dev/null +++ b/tests/shots.gd @@ -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) diff --git a/tests/shots.gd.uid b/tests/shots.gd.uid new file mode 100644 index 0000000..0ff407e --- /dev/null +++ b/tests/shots.gd.uid @@ -0,0 +1 @@ +uid://crqjm7tf6h8mi diff --git a/tests/shots.tscn b/tests/shots.tscn new file mode 100644 index 0000000..179b517 --- /dev/null +++ b/tests/shots.tscn @@ -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") diff --git a/tests/verify.gd b/tests/verify.gd new file mode 100644 index 0000000..d24f801 --- /dev/null +++ b/tests/verify.gd @@ -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) diff --git a/tests/verify.gd.uid b/tests/verify.gd.uid new file mode 100644 index 0000000..d9b697c --- /dev/null +++ b/tests/verify.gd.uid @@ -0,0 +1 @@ +uid://b3t2t2so80lnh diff --git a/tests/verify.tscn b/tests/verify.tscn new file mode 100644 index 0000000..21ddf9f --- /dev/null +++ b/tests/verify.tscn @@ -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")