diff --git a/.godot/editor/filesystem_cache10 b/.godot/editor/filesystem_cache10 index 893a66d..732fa46 100644 --- a/.godot/editor/filesystem_cache10 +++ b/.godot/editor/filesystem_cache10 @@ -1,21 +1,24 @@ 78098b89c88a74ebe8c20117b8754620 -::res://::1787989983 -anforderungen.md::TextFile::-1::1787989000::0::1::::<><><>0<>0<><>:: +::res://::1787996624 +anforderungen.md::TextFile::-1::1788038502::0::1::::<><><>0<>0<><>:: +README.md::TextFile::-1::1788038596::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<><>:: +::res://scripts/::1788039082 +bewohner.gd::GDScript::2125278938812678811::1788039082::0::1::::Bewohner<>Node3D<><>0<>0<><>:: +dream_db.gd::GDScript::4217518896355437099::1788039046::0::1::::DreamDB<>RefCounted<><>0<>0<><>:: +dream_world.gd::GDScript::8279158278942867258::1788039140::0::1::::DreamWorld<>Node3D<><>0<>0<><>:: +earth_world.gd::GDScript::6086818627272428934::1788039160::0::1::::EarthWorld<>Node3D<><>0<>0<><>:: +game.gd::GDScript::3600922541500868814::1788038470::0::1::::<>Node<><>0<>0<><>:: +human_db.gd::GDScript::412604892032291296::1788039057::0::1::::HumanDB<>RefCounted<><>0<>0<><>:: +main.gd::GDScript::1504009331368977934::1788039160::0::1::::<>Node3D<><>0<>0<><>:: +orte.gd::GDScript::5536340784294498671::1788039046::0::1::::Orte<>RefCounted<><>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<><>:: +technique_db.gd::GDScript::4543286561661639701::1788039160::0::1::::TechDB<>RefCounted<><>0<>0<><>:: +ui.gd::GDScript::843471304695966286::1788039160::0::1::::UI<>CanvasLayer<><>0<>0<><>:: ::res://tests/::1787990698 -shots.gd::GDScript::5999651346126871724::1787990988::0::1::::<>Node<><>0<>0<><>:: +shots.gd::GDScript::5999651346126871724::1788038553::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.gd::GDScript::4392606218392155269::1788038533::0::1::::<>Node<><>0<>0<><>:: verify.tscn::PackedScene::-1::1787989983::0::1::::<><><>0<>0<><>::res://tests/verify.gd diff --git a/.godot/global_script_class_cache.cfg b/.godot/global_script_class_cache.cfg index 9a5f0c8..3de2cd2 100644 --- a/.godot/global_script_class_cache.cfg +++ b/.godot/global_script_class_cache.cfg @@ -1,5 +1,21 @@ list=[{ "base": &"Node3D", +"class": &"Bewohner", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/bewohner.gd" +}, { +"base": &"RefCounted", +"class": &"DreamDB", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/dream_db.gd" +}, { +"base": &"Node3D", "class": &"DreamWorld", "icon": "", "is_abstract": false, @@ -15,14 +31,6 @@ list=[{ "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": "", @@ -31,6 +39,14 @@ list=[{ "language": &"GDScript", "path": "res://scripts/human_db.gd" }, { +"base": &"RefCounted", +"class": &"Orte", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/orte.gd" +}, { "base": &"CharacterBody3D", "class": &"Player", "icon": "", diff --git a/.godot/uid_cache.bin b/.godot/uid_cache.bin index bf363df..cf59258 100644 Binary files a/.godot/uid_cache.bin and b/.godot/uid_cache.bin differ diff --git a/README.md b/README.md index f0b6558..85c72a3 100644 --- a/README.md +++ b/README.md @@ -4,32 +4,38 @@ Ein 3D-Spiel in Godot 4.7 (GDScript). Alle Objekte bestehen aus Godot-Primitiven (Box, Zylinder, Kugel, Kapsel) — kein externes Asset. Du bist ein Wesen aus der Traumwelt. Menschen tragen Träume mit sich herum, die -sie allein nicht bewegt bekommen. Du wirst auf die Erde geschickt, um zu helfen. +sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt. ## Starten godot --path . Steuerung: **WASD** bewegen · **Maus** umsehen · **E** interagieren · -**Tab** Kodex · **Esc** Maus freigeben. +**J** Journal · **Esc** Maus freigeben. Auf der Erde zusätzlich **Leertaste** steigen und **Umschalt** sinken. ## Spielschleife -**Traumwelt** (Insel mit drei betretbaren Gebäuden) +**Journal** (Taste **J**, in beiden Welten dabei) -* **Bibliothek der Techniken** — zwölf echte Lösungswege. Jede Technik nennt - *Was* sie tut, *Wann* sie passt und *Warum* sie hilft. Lernen kostet 1 Einsicht. -* **Missionshalle** — vier Menschen mit ihrem Traum und dem, was im Weg steht. - Eine Mission annehmen. -* **Meditationskammer** — eine gelernte Technik für 1 Einsicht vertiefen (+3 EP). +* **Techniken** — zwölf echte Lösungswege. Jede nennt *Was* sie tut, *Wann* sie + passt und *Warum* sie hilft. Lernen kostet 1 Einsicht. Anwenden verbessert eine + Technik von allein; Vertiefen für 1 Einsicht geht zusätzlich. +* **Missionen** — vier Menschen mit ihrem Traum und dem, was im Weg steht. + +**Traumwelt** — eine schwebende Insel mit einem Dorf + +* Vier Wesen wohnen hier: Ailen die Traumweberin, Morr der Erinnerungssammler, + Ivo der Bote, Sela die Wächterin des Tors. +* Jedes hat ein eigenes Haus und einen eigenen Tagesablauf. Die Häuser lassen + sich betreten, auf der Lichtung treffen sich die Wesen. * **Tor zur Erde** — öffnet sich nur mit angenommener Mission. -**Erde** (Kleinstadt, ein Tag dauert 6 Minuten) +**Erde** — eine Kleinstadt, ein Tag dauert 6 Minuten * Du bist ein unsichtbarer Geist, schwebst und gehst durch Wände. * Vier Menschen leben ihren Tagesablauf: schlafen, arbeiten, grübeln, essen, - spazieren. Jeder Ort und jede Tätigkeit steht fest. + spazieren. * Nah herangehen, **E** drücken, Technik wählen. Das Panel zeigt für jede Technik die Erfolgschance in genau dieser Situation. * Die Säule im Park bringt dich zurück in die Traumwelt. @@ -49,24 +55,28 @@ bringen 3 Einsicht zurück. ## Aufbau - project.godot Projekt, Autoload `Game` - scenes/main.tscn Einstieg - scripts/game.gd Zustand, Regeln, Uhr (Autoload) - scripts/technique_db.gd die zwölf Techniken mit ihren Hinweisen - scripts/human_db.gd Orte, Menschen, Tagesabläufe, Wegfindung - scripts/main.gd Weltenwechsel, Fokus, Interaktion - scripts/player.gd Läufer in der Traumwelt, Geist auf der Erde - scripts/human.gd Mensch mit Tagesablauf und Reaktionen - scripts/dream_world.gd Traumwelt inklusive Innenräume - scripts/earth_world.gd Stadt mit Tag-Nacht-Wechsel - scripts/ui.gd HUD und alle Panels - scripts/prims.gd Bausteine aus Primitiven + project.godot Projekt, Autoload `Game` + scenes/main.tscn Einstieg + scripts/game.gd Zustand, Regeln, Uhr (Autoload) + scripts/technique_db.gd die zwölf Techniken mit ihren Hinweisen + scripts/human_db.gd Kleinstadt und Menschen + scripts/dream_db.gd Traumdorf und Traumwesen + scripts/orte.gd Orte, Türen, Wege, Tätigkeiten (beide Welten) + scripts/bewohner.gd Bewohner mit Tagesablauf und Reaktionen + scripts/main.gd Weltenwechsel, Fokus, Interaktion + scripts/player.gd Läufer in der Traumwelt, Geist auf der Erde + scripts/dream_world.gd Insel mit Dorf + scripts/earth_world.gd Stadt mit Tag-Nacht-Wechsel + scripts/ui.gd HUD, Journal und alle Panels + scripts/prims.gd Bausteine aus Primitiven ## Prüfen - godot --headless --path . tests/verify.tscn # 81 Prüfungen der Spiellogik - godot --path . tests/shots.tscn # 16 Screenshots nach user://shots/ + godot --headless --path . tests/verify.tscn # Prüfungen der Spiellogik + godot --path . tests/shots.tscn # 15 Screenshots nach user://shots/ -`verify` fährt das echte Spiel: Gebäude betreten, Technik lernen, Mission -annehmen, zur Erde reisen, Tagesabläufe über einen ganzen Tag, Erfolgschancen, -Mission abschließen, Tastensteuerung. Beendet sich mit Code 1, wenn etwas fehlt. +`verify` fährt das echte Spiel: Traumdorf mit vier Wesen und ihren Tagesabläufen, +Häuser betreten, Journal in beiden Welten, Technik lernen und vertiefen, +Verbessern allein durchs Anwenden, Mission annehmen, zur Erde reisen, +Tagesabläufe über einen ganzen Tag, Erfolgschancen, Mission abschließen, +Tastensteuerung. Beendet sich mit Code 1, wenn etwas fehlt. diff --git a/anforderungen.md b/anforderungen.md index 4b0b091..2b03834 100644 --- a/anforderungen.md +++ b/anforderungen.md @@ -1,14 +1,24 @@ -Funktionen Traumwelt -- Bewegen -- Gebäude betretten -- Techniken lernen, echte Lösungen für Probleme -- Techniken haben Hinweise, was sie machen, wann sie angewand werden und warum sie helfen -- Missionen annehmen, also einem Menschen helfen - -Funktionen Erde -- Menschen mit Techniken manipulieren, Erfolgschance hängt vom Skill ab -- Bewegung als Geist unsichtbar -- Menschen haben ihre Tagesabläufe als Slice of Life - Kern -- Wesen aus einer Traumwelt werden auf die Erde geschickt um Menschen helfen ihre Träume zu erfüllen \ No newline at end of file +- Wesen aus einer Traumwelt werden auf die Erde geschickt, + um Menschen zu helfen, ihre Träume zu erfüllen. + +Journal (immer dabei) +- Techniken lernen +- Techniken verbessern +- Missionen annehmen + +Traumwelt +- Bewegen +- Orte betreten +- Traumwesen leben ihren Tag + +Erde +- Bewegen als unsichtbarer Geist +- Menschen leben ihren Tag +- Menschen lesen +- Menschen beeinflussen + +Übergreifend +- Zwischen den Welten reisen +- Zeit vergeht +- Missionen gelingen oder scheitern diff --git a/scripts/human.gd b/scripts/bewohner.gd similarity index 75% rename from scripts/human.gd rename to scripts/bewohner.gd index 370b12f..30b6917 100644 --- a/scripts/human.gd +++ b/scripts/bewohner.gd @@ -1,15 +1,17 @@ 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. +class_name Bewohner +## Bewohner einer Welt: läuft seinen Tagesablauf ab. Auf der Erde kann er von +## einer Technik erreicht werden -- oder eben nicht. const GEHTEMPO := 6.0 var data: Dictionary var idx: int = 0 var place: String = "" -var activity: String = "schlaeft" +var activity: String = "" var resist_left: float = 0.0 +var places: Dictionary = {} +var mit_mission := false var _visual: Node3D var _label: Label3D @@ -22,21 +24,30 @@ var _bob: float = 0.0 var _offset: Vector3 -func setup(d: Dictionary, index: int) -> void: +func setup(d: Dictionary, index: int, orte: Dictionary) -> void: data = d idx = index - name = "Human_" + str(d["id"]) + places = orte + mit_mission = bool(d.get("mission", false)) + name = "Bewohner_" + 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 leuchtet := bool(d.get("leuchtet", false)) 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) + if leuchtet: + _koerper.material_override = Prims.mat(c, 0.9) + _kopf.material_override = Prims.mat(c.lightened(0.45), 0.8) + var reif := Prims.cyl(_visual, Vector3(0, hoehe * 0.5, 0), 0.62, 0.05, + c.lightened(0.3), false, 1.6) + reif.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _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 @@ -48,7 +59,7 @@ func setup(d: Dictionary, index: int) -> void: var start := _routine_at(Game.time_h) place = start[0] activity = start[1] - global_position = HumanDB.place_center(place) + _offset + global_position = Orte.center(places, place) + _offset func _routine_at(h: float) -> Array: @@ -66,7 +77,7 @@ func _process(delta: float) -> void: 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) + _path = Orte.route(places, 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] @@ -97,8 +108,8 @@ func _process(delta: float) -> void: _visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0, 1.0 - pow(0.01, delta)) - var erledigt: bool = Game.completed.has(data["id"]) - var aktiv: bool = Game.active_mission == data["id"] + var erledigt: bool = mit_mission and Game.completed.has(data["id"]) + var aktiv: bool = mit_mission and Game.active_mission == data["id"] _aura.visible = aktiv or erledigt _strahl.visible = aktiv if _aura.visible: @@ -113,13 +124,17 @@ func _process(delta: float) -> void: func beschriftung() -> String: - var s: String = data["name"] + "\n" + HumanDB.activity_text(activity) + var s: String = data["name"] + if data.has("rolle"): + s += ", " + str(data["rolle"]) + s += "\n" + Orte.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"])) + if mit_mission: + 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 diff --git a/scripts/bewohner.gd.uid b/scripts/bewohner.gd.uid new file mode 100644 index 0000000..5612fbf --- /dev/null +++ b/scripts/bewohner.gd.uid @@ -0,0 +1 @@ +uid://5jrbe4hct8jn diff --git a/scripts/dream_db.gd b/scripts/dream_db.gd new file mode 100644 index 0000000..9ca4d4d --- /dev/null +++ b/scripts/dream_db.gd @@ -0,0 +1,100 @@ +class_name DreamDB +## Die Traumwelt: ein Dorf. Jedes Wesen hat ein Haus und einen Tagesablauf. + +static var PLACES: Dictionary = { + "haus_ailen": { + "name": "Ailens Webstube", "pos": [-28.0, -22.0], "size": [12.0, 10.0], + "dir": "back", "color": Color(0.72, 0.62, 0.92), + }, + "haus_morr": { + "name": "Morrs Kammer", "pos": [28.0, -22.0], "size": [12.0, 10.0], + "dir": "back", "color": Color(0.58, 0.72, 0.90), + }, + "haus_ivo": { + "name": "Ivos Unterstand", "pos": [-28.0, 22.0], "size": [12.0, 10.0], + "dir": "forward", "color": Color(0.88, 0.66, 0.80), + }, + "haus_sela": { + "name": "Selas Turmstube", "pos": [28.0, 22.0], "size": [12.0, 10.0], + "dir": "forward", "color": Color(0.62, 0.82, 0.88), + }, + "lichtung": { + "name": "Die Lichtung", "pos": [0.0, 2.0], "size": [0.0, 0.0], + "dir": "none", "color": Color(0.60, 0.50, 0.85), + }, + "tor": { + "name": "Tor zur Erde", "pos": [0.0, -34.0], "size": [0.0, 0.0], + "dir": "none", "color": Color(0.50, 0.80, 1.0), + }, +} + +static var LIST: Array = [ + { + "id": "ailen", + "name": "Ailen", + "rolle": "Traumweberin", + "leuchtet": true, + "farbe": Color(0.78, 0.55, 0.95), + "routine": [ + [0, "haus_ailen", "webt"], + [6, "lichtung", "ruht"], + [8, "haus_ailen", "webt"], + [14, "lichtung", "singt"], + [16, "haus_ailen", "webt"], + [20, "haus_ailen", "traeumt"], + ], + }, + { + "id": "morr", + "name": "Morr", + "rolle": "Erinnerungssammler", + "leuchtet": true, + "farbe": Color(0.45, 0.70, 0.95), + "routine": [ + [0, "haus_morr", "traeumt"], + [5, "lichtung", "sammelt"], + [9, "haus_morr", "sortiert"], + [15, "lichtung", "sammelt"], + [19, "haus_morr", "sortiert"], + [22, "haus_morr", "traeumt"], + ], + }, + { + "id": "ivo", + "name": "Ivo", + "rolle": "Bote", + "leuchtet": true, + "farbe": Color(0.95, 0.60, 0.75), + "routine": [ + [0, "haus_ivo", "traeumt"], + [4, "tor", "beobachtet"], + [8, "lichtung", "wandert"], + [10, "haus_ivo", "ruht"], + [13, "tor", "beobachtet"], + [18, "lichtung", "singt"], + [21, "haus_ivo", "traeumt"], + ], + }, + { + "id": "sela", + "name": "Sela", + "rolle": "Wächterin des Tors", + "leuchtet": true, + "farbe": Color(0.55, 0.95, 0.90), + "routine": [ + [0, "tor", "wacht"], + [7, "lichtung", "ruht"], + [9, "tor", "wacht"], + [17, "haus_sela", "ruht"], + [19, "lichtung", "singt"], + [21, "haus_sela", "traeumt"], + ], + }, +] + + +static func by_id(id: String) -> Dictionary: + for w in LIST: + if w["id"] == id: + return w + return {} diff --git a/scripts/dream_db.gd.uid b/scripts/dream_db.gd.uid new file mode 100644 index 0000000..4f4a923 --- /dev/null +++ b/scripts/dream_db.gd.uid @@ -0,0 +1 @@ +uid://b1c86jtc5qd3h diff --git a/scripts/dream_world.gd b/scripts/dream_world.gd index 05be60a..b592e6d 100644 --- a/scripts/dream_world.gd +++ b/scripts/dream_world.gd @@ -1,24 +1,21 @@ extends Node3D class_name DreamWorld -## Die Traumwelt: Insel mit drei betretbaren Gebäuden und dem Tor zur Erde. +## Die Traumwelt: eine schwebende Insel mit einem Dorf. Vier Wesen wohnen hier, +## jedes in seinem Haus, jedes mit eigenem Tagesablauf. -const INNEN_Y := -300.0 +var spawn := Vector3(0, 2.0, 20.0) +var bewohner: Array = [] -var spawn := Vector3(0, 2.0, 14.0) var _deko: Array = [] -var _regale: Dictionary = {} # tech_id -> MeshInstance3D -var _stelen: Dictionary = {} # human_id -> Dictionary func _ready() -> void: _environment() _insel() - _gebaeude_bibliothek() - _gebaeude_missionshalle() - _gebaeude_meditation() + _haeuser() + _lichtung() _tor_zur_erde() - Game.state_changed.connect(_refresh) - _refresh() + _wesen() func _environment() -> void: @@ -35,19 +32,19 @@ func _environment() -> void: sky.sky_material = mat env.sky = sky env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY - env.ambient_light_energy = 1.8 + env.ambient_light_energy = 2.1 env.glow_enabled = true - env.glow_intensity = 0.5 - env.glow_bloom = 0.15 + env.glow_intensity = 0.32 + env.glow_bloom = 0.05 env.fog_enabled = true env.fog_light_color = Color(0.30, 0.18, 0.45) - env.fog_density = 0.006 + env.fog_density = 0.0015 we.environment = env add_child(we) var sun := DirectionalLight3D.new() sun.rotation_degrees = Vector3(-46, 25, 0) - sun.light_energy = 1.15 + sun.light_energy = 1.5 sun.light_color = Color(0.85, 0.80, 1.0) sun.shadow_enabled = true sun.shadow_blur = 2.0 @@ -55,24 +52,23 @@ func _environment() -> void: 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) + Prims.cyl(self, Vector3(0, -1.03, 0), 52.0, 2.0, Color(0.48, 0.40, 0.64)) + Prims.cyl(self, Vector3(0, -4.0, 0), 40.0, 4.0, Color(0.34, 0.26, 0.50), false) + Prims.cyl(self, Vector3(0, -9.0, 0), 22.0, 6.0, Color(0.24, 0.17, 0.38), false) + + # Ringweg, auf dem die Wesen gehen + for i in range(36): + var a := TAU * float(i) / 36.0 + Prims.box(self, Vector3(cos(a) * 15.0, 0.03, sin(a) * 15.0), + Vector3(2.6, 0.06, 2.6), Color(0.66, 0.58, 0.84), false) + # 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 r := rng.randf_range(18.0, 48.0) + var p := Vector3(cos(a) * r, rng.randf_range(5.0, 24.0), sin(a) * r) var s := rng.randf_range(0.6, 2.0) var c := Color(0.6 + rng.randf() * 0.3, 0.5 + rng.randf() * 0.4, 1.0) var wuerfel := Prims.box(self, p, Vector3(s, s, s), c, false, 1.1) @@ -80,175 +76,108 @@ func _insel() -> void: _deko.append(wuerfel) +func _haeuser() -> void: + for key in DreamDB.PLACES: + var pl: Dictionary = DreamDB.PLACES[key] + var s: Array = pl["size"] + if s[0] <= 0.0: + continue + var c := Orte.center(DreamDB.PLACES, key) + var groesse := Vector3(s[0], 3.2, s[1]) + var farbe: Color = pl["color"] + Prims.room(self, c, groesse, farbe, farbe.darkened(0.35), + Orte.DIR_VEC[pl["dir"]], 3.4, true) + Prims.label(self, c + Vector3(0, 5.0, 0), pl["name"], 0.6, Color(1, 0.95, 0.85)) + _einrichtung(c, groesse, farbe, key) + + var licht := OmniLight3D.new() + licht.position = c + Vector3(0, 2.4, 0) + licht.omni_range = 18.0 + licht.light_energy = 2.2 + licht.light_color = farbe.lightened(0.5) + add_child(licht) + + # Weg von der Tür zum Ring + var tuer := Orte.door(DreamDB.PLACES, key) + var weg := tuer.normalized() * 15.0 - tuer + var b := Prims.box(self, (tuer + tuer.normalized() * 15.0) * 0.5 + Vector3(0, 0.03, 0), + Vector3(2.4, 0.06, weg.length()), Color(0.66, 0.58, 0.84), false) + b.rotation.y = atan2(weg.x, weg.z) + + +func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void: + var w := groesse.x * 0.5 - 1.8 + var h := groesse.z * 0.5 - 1.8 + # Schlafnest hat jedes Haus + Prims.cyl(self, c + Vector3(-w, 0.3, -h), 1.4, 0.5, farbe.darkened(0.2), false) + match key: + "haus_ailen": # Webstuhl + Prims.box(self, c + Vector3(w, 1.1, 0), Vector3(0.3, 2.2, 3.0), farbe.darkened(0.45)) + for i in range(5): + Prims.box(self, c + Vector3(w, 1.1, -1.2 + i * 0.6), Vector3(0.1, 1.8, 0.06), + Color(0.9, 0.85, 1.0), false, 1.4) + "haus_morr": # Regale voller Fundstücke + for i in range(3): + Prims.box(self, c + Vector3(w, 0.6 + i * 0.7, 0), Vector3(1.2, 0.12, 4.0), + farbe.darkened(0.45)) + for j in range(4): + Prims.box(self, c + Vector3(w, 0.85 + i * 0.7, -1.5 + j * 1.0), + Vector3(0.4, 0.4, 0.4), Color(0.8, 0.9, 1.0), false, 0.8) + "haus_ivo": # Karten und ein Fernrohr + Prims.box(self, c + Vector3(w, 0.75, 0), Vector3(1.6, 0.12, 2.6), farbe.darkened(0.45)) + var rohr := Prims.cyl(self, c + Vector3(w, 1.6, 0), 0.18, 2.0, + Color(0.85, 0.9, 1.0), false, 0.6) + rohr.rotation.x = 0.9 + "haus_sela": # Schale mit Licht + Prims.cyl(self, c + Vector3(w, 0.5, 0), 1.0, 1.0, farbe.darkened(0.4), false) + Prims.sphere(self, c + Vector3(w, 1.4, 0), 0.55, Color(0.8, 1.0, 1.0), 2.0) + + +func _lichtung() -> void: + var c := Orte.center(DreamDB.PLACES, "lichtung") + Prims.cyl(self, c + Vector3(0, 0.04, 0), 11.0, 0.12, Color(0.60, 0.52, 0.78), false) + Prims.cyl(self, c + Vector3(0, 0.14, 0), 3.2, 0.2, Color(0.55, 0.80, 1.0, 0.5), false, 1.2) + for i in range(8): + var a := TAU * float(i) / 8.0 + var p := c + Vector3(cos(a) * 8.5, 0, sin(a) * 8.5) + Prims.cyl(self, p + Vector3(0, 1.6, 0), 0.28, 3.2, Color(0.72, 0.64, 0.88)) + Prims.sphere(self, p + Vector3(0, 3.6, 0), 0.42, Color(0.75, 0.85, 1.0), 2.0) + Prims.label(self, c + Vector3(0, 5.2, 0), DreamDB.PLACES["lichtung"]["name"], 0.6, + Color(1, 0.95, 0.85)) + + +func _tor_zur_erde() -> void: + var pos := Orte.center(DreamDB.PLACES, "tor") + Prims.cyl(self, pos + Vector3(0, 0.3, 0), 5.5, 0.6, Color(0.35, 0.30, 0.50)) + 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.8, Color(0.7, 0.95, 1)) + + var n := Node3D.new() + n.position = pos + Vector3(0, 1.6, 0) + add_child(n) + n.add_to_group("interactable") + n.set_meta("kind", "erde") + n.set_meta("arg", "") + n.set_meta("titel", "Tor zur Erde") + + +func _wesen() -> void: + for i in range(DreamDB.LIST.size()): + var w := Bewohner.new() + add_child(w) + w.setup(DreamDB.LIST[i], i, DreamDB.PLACES) + bewohner.append(w) + + 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/earth_world.gd b/scripts/earth_world.gd index 3da14eb..caa335a 100644 --- a/scripts/earth_world.gd +++ b/scripts/earth_world.gd @@ -44,7 +44,7 @@ func _environment() -> void: func _boden() -> void: - Prims.box(self, Vector3(0, -0.5, 0), Vector3(240, 1, 240), Color(0.36, 0.52, 0.30)) + Prims.box(self, Vector3(0, -0.53, 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 @@ -57,16 +57,16 @@ func _orte() -> void: if key == "park": continue var pl: Dictionary = HumanDB.PLACES[key] - var c := HumanDB.place_center(key) + var c := Orte.center(HumanDB.PLACES, 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"]] + var richtung: Vector3 = Orte.DIR_VEC[pl["dir"]] Prims.room(self, c, groesse, farbe, farbe.darkened(0.35), richtung, 3.4, true) Prims.label(self, c + Vector3(0, 5.0, 0), pl["name"], 0.6, Color(1, 1, 0.9)) _einrichtung(c, groesse, farbe, key) - var tuer := HumanDB.place_door(key) + var tuer := Orte.door(HumanDB.PLACES, key) var weg := tuer.normalized() * 15.0 - tuer var b := Prims.box(self, (tuer + tuer.normalized() * 15.0) * 0.5 + Vector3(0, 0.03, 0), Vector3(2.6, 0.06, weg.length()), Color(0.62, 0.58, 0.50), false) @@ -152,9 +152,9 @@ func _saeule() -> void: func _menschen() -> void: for i in range(HumanDB.LIST.size()): - var m := Human.new() + var m := Bewohner.new() add_child(m) - m.setup(HumanDB.LIST[i], i) + m.setup(HumanDB.LIST[i], i, HumanDB.PLACES) humans.append(m) diff --git a/scripts/game.gd b/scripts/game.gd index cc5c364..fbfa5b3 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -34,7 +34,7 @@ func _setup_input() -> void: "steigen": [KEY_SPACE], "sinken": [KEY_SHIFT], "interagieren": [KEY_E], - "kodex": [KEY_TAB], + "journal": [KEY_J, KEY_TAB], } for action in binds: if not InputMap.has_action(action): diff --git a/scripts/human.gd.uid b/scripts/human.gd.uid deleted file mode 100644 index ae2339f..0000000 --- a/scripts/human.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://let5f4ntq0j8 diff --git a/scripts/human_db.gd b/scripts/human_db.gd index b2f73ab..b01af66 100644 --- a/scripts/human_db.gd +++ b/scripts/human_db.gd @@ -1,5 +1,5 @@ class_name HumanDB -## Orte der Kleinstadt und die Menschen mit ihren Tagesabläufen. +## Die Kleinstadt auf der Erde 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. @@ -42,31 +42,6 @@ static var PLACES: Dictionary = { }, } -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", @@ -76,6 +51,7 @@ static var LIST: Array = [ "traum": "Ihr Bilderbuch endlich zu Ende zeichnen.", "problem": "Sie schiebt die letzten zwölf Seiten seit acht Monaten vor sich her. Jeder Entwurf kommt ihr zu schlecht vor, also fängt sie lieber gar nicht erst an.", "tags": ["prokrastination", "selbstzweifel", "ablenkung"], + "mission": true, "farbe": Color(0.85, 0.45, 0.55), "routine": [ [0, "haus_lena", "schlaeft"], @@ -100,6 +76,7 @@ static var LIST: Array = [ "traum": "Wieder Klavier spielen wie mit zwanzig.", "problem": "Das Klavier steht zugestellt im Flur. Nach der Frühschicht ist er zu müde, um auch nur den Deckel zu öffnen.", "tags": ["gewohnheit", "erschoepfung", "ablenkung"], + "mission": true, "farbe": Color(0.55, 0.45, 0.30), "routine": [ [0, "haus_bruno", "schlaeft"], @@ -122,6 +99,7 @@ static var LIST: Array = [ "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"], + "mission": true, "farbe": Color(0.35, 0.60, 0.85), "routine": [ [0, "haus_mira", "schlaeft"], @@ -145,6 +123,7 @@ static var LIST: Array = [ "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"], + "mission": true, "farbe": Color(0.40, 0.55, 0.45), "routine": [ [0, "haus_tobias", "schlaeft"], @@ -166,41 +145,3 @@ static func by_id(id: String) -> Dictionary: 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/main.gd b/scripts/main.gd index 6b25f28..f7f1bc3 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -100,18 +100,11 @@ func _fokus_suchen() -> void: 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)]) + [_fokus_mensch.data["name"], Orte.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": @@ -135,8 +128,8 @@ func _unhandled_input(event: InputEvent) -> void: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED player.look_enabled = true return - if event.is_action_pressed("kodex"): - ui.kodex_panel() + if event.is_action_pressed("journal"): + ui.journal_panel() get_viewport().set_input_as_handled() return if event.is_action_pressed("interagieren"): @@ -151,14 +144,7 @@ func _interagieren() -> void: 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( diff --git a/scripts/orte.gd b/scripts/orte.gd new file mode 100644 index 0000000..282ed85 --- /dev/null +++ b/scripts/orte.gd @@ -0,0 +1,75 @@ +class_name Orte +## Gemeinsame Geometrie für beide Welten: Orte, Türen, Wege, Tätigkeiten. + +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 = { + # Erde + "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", + # Traumwelt + "webt": "webt Traumfäden", + "sammelt": "sammelt Erinnerungen", + "sortiert": "sortiert, was verloren ging", + "beobachtet": "schaut zur Erde hinunter", + "wacht": "hält Wache am Tor", + "singt": "singt", + "wandert": "wandert über die Insel", + "traeumt": "träumt", +} + + +static func text(a: String) -> String: + return ACTIVITY_TEXT.get(a, a) + + +static func center(places: Dictionary, 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 door(places: Dictionary, key: String) -> Vector3: + var pl: Dictionary = places[key] + var c := center(places, 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 freien Platz, wieder hinein. +static func route(places: Dictionary, a: String, b: String, offset: Vector3) -> Array: + var pts: Array = [] + if a == b: + return pts + var da := door(places, a) + var db := door(places, 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(center(places, b) + offset) + return pts diff --git a/scripts/orte.gd.uid b/scripts/orte.gd.uid new file mode 100644 index 0000000..5376f43 --- /dev/null +++ b/scripts/orte.gd.uid @@ -0,0 +1 @@ +uid://ck43yu1d5s856 diff --git a/scripts/player.gd b/scripts/player.gd index 4e54dde..fde90bf 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -51,11 +51,15 @@ func _ready() -> void: 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 + arm.margin = 0.2 + var arm_form := SphereShape3D.new() + arm_form.radius = 0.45 # Kugel statt Strahl: die Kamera klemmt nicht in Wänden + arm.shape = arm_form pivot.add_child(arm) + arm.add_excluded_object(get_rid()) cam = Camera3D.new() + cam.position.x = 0.7 # Blick über die Schulter, am Ende des Arms cam.fov = 74.0 arm.add_child(cam) set_mode(Mode.TRAUM) diff --git a/scripts/prims.gd b/scripts/prims.gd index 101c3a4..70677fa 100644 --- a/scripts/prims.gd +++ b/scripts/prims.gd @@ -97,7 +97,7 @@ static func label(parent: Node, pos: Vector3, text: String, size: float = 0.35, l.billboard = BaseMaterial3D.BILLBOARD_ENABLED l.no_depth_test = false l.modulate = c - l.outline_size = 24 + l.outline_size = 10 l.outline_modulate = Color(0, 0, 0, 0.85) l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER parent.add_child(l) @@ -112,7 +112,7 @@ static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color, 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) + box(parent, center + Vector3(0, -0.1, 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 diff --git a/scripts/technique_db.gd b/scripts/technique_db.gd index 87e7422..58b7a76 100644 --- a/scripts/technique_db.gd +++ b/scripts/technique_db.gd @@ -154,7 +154,7 @@ static func tags_text(tags: Array) -> String: static func momente_text(momente: Array) -> String: var out: Array = [] for m in momente: - out.append(HumanDB.activity_text(m)) + out.append(Orte.text(m)) return ", ".join(out) diff --git a/scripts/ui.gd b/scripts/ui.gd index 756f0bf..25c11ce 100644 --- a/scripts/ui.gd +++ b/scripts/ui.gd @@ -18,6 +18,9 @@ var _titel: Label var _content: VBoxContainer var _scroll: ScrollContainer var _apply_human: Node3D +var _journal_seite := "techniken" +var _scroll_wunsch := 0 +var _scroll_frames := 0 var _on_travel: Callable @@ -45,7 +48,7 @@ func _baue_hud() -> void: add_child(_hud) var steuerung := Label.new() - steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · Tab Kodex · Esc Maus frei" + steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · J Journal · Esc Maus frei" steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT) steuerung.position = Vector2(-640, 16) steuerung.size = Vector2(620, 24) @@ -130,6 +133,9 @@ func _process(_delta: float) -> void: var wunsch := _content.get_combined_minimum_size().y var platz := get_viewport().get_visible_rect().size.y - 220.0 _scroll.custom_minimum_size.y = clampf(wunsch, 120.0, platz) + if _scroll_frames > 0: + _scroll.scroll_vertical = _scroll_wunsch + _scroll_frames -= 1 _panel.position = (get_viewport().get_visible_rect().size - _panel.size) * 0.5 @@ -148,7 +154,7 @@ func _hud_aktualisieren() -> void: 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]") + zeilen.append("[color=#aaaaaa]Keine Mission. Journal öffnen mit J.[/color]") _hud.text = "\n".join(zeilen) @@ -242,88 +248,96 @@ func _hinweis_block(t: Dictionary) -> String: [t["was"], t["wann"], t["warum"]]) -# -------------------------------------------------------- Bibliothek +# -------------------------------------------------------- Journal -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)]) +## Das Journal ist immer dabei. Hier werden Techniken gelernt und vertieft +## und Missionen angenommen. +func journal_panel(seite: String = "") -> void: + if seite != "": + _journal_seite = seite + var scroll_alt := _scroll.scroll_vertical if offen and _kind == "journal" else 0 + _leeren("Journal") + _reiter() + if _journal_seite == "missionen": + _journal_missionen() 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") + _journal_techniken() + _zeigen("journal") + _scroll_wunsch = scroll_alt + _scroll_frames = 4 -# -------------------------------------------------------- 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) +func _reiter() -> void: + var hb := HBoxContainer.new() + hb.add_theme_constant_override("separation", 8) + _content.add_child(hb) + for eintrag in [["techniken", "Techniken"], ["missionen", "Missionen"]]: + var b := Button.new() + b.text = eintrag[1] + b.custom_minimum_size = Vector2(0, 36) + b.size_flags_horizontal = Control.SIZE_EXPAND_FILL + b.add_theme_font_size_override("font_size", 17) + b.disabled = _journal_seite == eintrag[0] + if b.disabled: + b.add_theme_color_override("font_disabled_color", Color(1, 0.88, 0.55)) + hb.add_child(b) + if not b.disabled: + b.pressed.connect(func() -> void: journal_panel(eintrag[0])) + var mission := "keine" + if Game.active_mission != "": + mission = "%s (%d %%)" % [HumanDB.by_id(Game.active_mission)["name"], + int(Game.mission_progress(Game.active_mission))] + _text("[color=#aaaaaa]Einsicht: [b]%d[/b] · Mission: %s[/color]" % + [Game.einsicht, mission], 15) _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) +func _journal_techniken() -> void: + _text("Zwölf Wege, die Menschen wirklich weiterbringen. Jede Anwendung " + + "draußen macht eine Technik von allein besser. Vertiefen geht auch hier.", 15) _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) + if Game.knows(id): + _text("[b][color=#ffe3a0]%s[/color][/b] · Stufe %d (%s)" % + [t["name"], Game.skill_of(id), Game.xp_to_next(id)], 18) + else: + _text("[b][color=#9aa0b8]%s[/color][/b] · noch nicht gelernt" % t["name"], 18) _text(_hinweis_block(t), 15) _text("[color=#9fb6ff]Hilft bei:[/color] %s [color=#9fb6ff]Momente:[/color] %s" % [TechDB.tags_text(t["tags"]), TechDB.momente_text(t["momente"])], 14) + if Game.knows(id): + _knopf("Vertiefen (1 Einsicht)", func() -> void: + if Game.deepen(id): + journal_panel(), Game.einsicht >= 1) + else: + _knopf("Lernen (1 Einsicht)", func() -> void: + if Game.learn(id): + journal_panel(), Game.einsicht >= 1) + _trenner() + + +func _journal_missionen() -> void: + _text("Menschen, deren Traum feststeckt.", 15) + _trenner() + for h in HumanDB.LIST: + var id: String = h["id"] + _text("[b][color=#ffe3a0]%s[/color][/b], %d — %s" % + [h["name"], h["alter"], h["beruf"]], 18) + _text("[b]Traum:[/b] %s" % h["traum"], 16) + _text("[b]Was im Weg steht:[/b] %s" % h["problem"], 15) + _text("[color=#9fb6ff]Themen:[/color] %s" % TechDB.tags_text(h["tags"]), 14) + if Game.completed.has(id): + _text("[color=#88ff99]★ Der Traum ist erfüllt.[/color]", 15) + elif Game.active_mission == id: + _text("[color=#ffdd77]Aktive Mission — %d %%[/color]" % + int(Game.mission_progress(id)), 15) + else: + _text("Fortschritt: %d %%" % int(Game.mission_progress(id)), 15) + _knopf("Mission annehmen", func() -> void: + Game.accept_mission(id) + journal_panel()) _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 @@ -332,7 +346,7 @@ 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)]) + _leeren("%s — %s" % [h["name"], Orte.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) @@ -404,11 +418,14 @@ func intro_panel(on_close: Callable) -> void: _text("Du bist ein Wesen aus der Traumwelt. Menschen tragen Träume mit sich herum, " + "die sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt.") _trenner() + _text("[b]Dein Journal — Taste J[/b]\n" + + "· Es ist immer dabei, in beiden Welten.\n" + + "· [b]Techniken[/b] — lerne echte Lösungswege. Jede sagt dir, was sie tut, " + + "wann sie passt und warum sie hilft. Anwenden macht sie von allein besser.\n" + + "· [b]Missionen[/b] — nimm einen Menschen an, dem du helfen willst.") _text("[b]In der Traumwelt[/b]\n" + - "· [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" + + "· Ein Dorf auf einer schwebenden Insel. Vier Wesen wohnen hier, jedes in " + + "seinem Haus, jedes mit eigenem Tagesablauf. Die Häuser stehen offen.\n" + "· [b]Tor zur Erde[/b] — reise hinab, sobald du eine Mission hast.") _text("[b]Auf der Erde[/b]\n" + "· Du bist ein unsichtbarer Geist und schwebst durch Wände. Leertaste hoch, Umschalt runter.\n" + diff --git a/tests/shots.gd b/tests/shots.gd index 121b8a0..fb10aec 100644 --- a/tests/shots.gd +++ b/tests/shots.gd @@ -21,61 +21,78 @@ func _ready() -> void: await knipse("01_intro") main.ui.schliessen() - main.player.global_position = Vector3(0, 3.0, 26.0) + main.player.global_position = Vector3(0, 4.0, 34.0) main.player.yaw = 0.0 - main.player.pitch = -0.10 - await warte(20) + main.player.pitch = -0.14 + Game.time_h = 10.0 + await warte(30) await knipse("02_traumwelt") - main.player.global_position = Vector3(-24, 2.0, 4.0) - main.player.yaw = 0.0 - main.player.pitch = -0.02 + main.player.global_position = Vector3(0, 12.0, 30.0) + main.player.pitch = -0.5 await warte(20) - await knipse("03_bibliothek_aussen") + await knipse("03_traumdorf_ueberblick") - Game.teleport_lock = 0.0 - main.player.global_position = Vector3(0, -299.0, 2.0) + var haus := Orte.center(DreamDB.PLACES, "haus_ailen") + main.player.global_position = haus + Vector3(0, 2.0, 12.0) main.player.yaw = 0.0 - main.player.pitch = -0.12 - await warte(30) - await knipse("04_bibliothek_innen") + main.player.pitch = -0.18 + await warte(20) + await knipse("04_haus_aussen") - main.ui.technik_panel("pomodoro") + # ein Wesen in seinem Haus + var ailen: Bewohner = null + for w in main.welt.bewohner: + if w.data["id"] == "ailen": + ailen = w + for i in range(900): + main._process(0.05) + for k in main.welt.get_children(): + if k is Bewohner: + k._process(0.05) + Game.time_h = 10.0 + if ailen.place == "haus_ailen" and ailen._path.is_empty(): + break + main.player.global_position = ailen.global_position + Vector3(3.0, 0.6, 1.2) + main.player.yaw = -PI * 0.32 + main.player.pitch = -0.06 + await warte(20) + await knipse("05_haus_innen") + + main.ui.journal_panel("techniken") await warte(6) - await knipse("05_technik_hinweise") + await knipse("06_journal_techniken") 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") + main.ui.journal_panel("missionen") await warte(6) - await knipse("07_mission_panel") + await knipse("07_journal_missionen") 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) + # Lichtung, wenn sich die Wesen dort treffen + for i in range(1200): + main._process(0.05) + for k in main.welt.get_children(): + if k is Bewohner: + k._process(0.05) + Game.time_h = 19.5 + main.player.global_position = Vector3(0.0, 1.0, 22.0) main.player.yaw = 0.0 - main.player.pitch = -0.1 + main.player.pitch = -0.20 await warte(20) - await knipse("08b_meditation") - main.ui.vertiefen_panel() - await warte(6) - await knipse("08c_vertiefen") - main.ui.schliessen() + await knipse("08_lichtung") + + var tor := Orte.center(DreamDB.PLACES, "tor") + main.player.global_position = tor + Vector3(0, 2.5, 14.0) + main.player.yaw = 0.0 + main.player.pitch = -0.12 + await warte(20) + await knipse("09_tor_zur_erde") main._erde() Game.time_h = 10.5 @@ -84,16 +101,16 @@ func _ready() -> void: main.player.pitch = -0.62 main.player.yaw = 0.0 await warte(20) - await knipse("09_erde_uebersicht") + await knipse("10_erde_uebersicht") - var lena: Human = null + var lena: Bewohner = 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: + if k is Bewohner: k._process(0.05) Game.time_h = 10.5 if lena.place == "haus_lena" and lena._path.is_empty(): @@ -102,27 +119,27 @@ func _ready() -> void: main.player.yaw = PI main.player.pitch = -0.15 await warte(20) - await knipse("10_lena_zoegert") + await knipse("11_lena_zoegert") main.ui.anwenden_panel(lena) await warte(6) - await knipse("11_anwenden") + await knipse("12_anwenden") main.ui._anwenden("pomodoro") await warte(25) - await knipse("12_reaktion") + await knipse("13_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: + if k is Bewohner: 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") + await knipse("14_cafe_mittag") Game.time_h = 22.5 await warte(40) @@ -130,7 +147,7 @@ func _ready() -> void: main.player.yaw = 0.0 main.player.pitch = -0.5 await warte(20) - await knipse("14_nacht") + await knipse("15_nacht") print("Screenshots: %d" % n) print("Ordner: %s" % ProjectSettings.globalize_path(OUT)) diff --git a/tests/verify.gd b/tests/verify.gd index d24f801..a934e19 100644 --- a/tests/verify.gd +++ b/tests/verify.gd @@ -23,8 +23,9 @@ func _ready() -> void: await get_tree().process_frame await test_start() - await test_bibliothek() - await test_gebaeude_betreten() + await test_journal() + await test_traumdorf() + await test_haus_betreten() await test_mission_annehmen() await test_reise_zur_erde() await test_tagesablauf() @@ -32,7 +33,8 @@ func _ready() -> void: await test_geist() await test_technik_anwenden() await test_mission_abschliessen() - await test_ui_panels() + await test_techniken_verbessern_sich() + await test_journal_ueberall() await test_steuerung() print("\n=========================================") @@ -57,10 +59,23 @@ func schritte(n: int, dt: float = 0.05, zeit: float = -1.0) -> void: if welt().has_method("_process"): welt()._process(dt) for kind in welt().get_children(): - if kind is Human: + if kind is Bewohner: kind._process(dt) +func laufe(von: Vector3, yaw: float, frames: int) -> Vector3: + main.player.set_mode(Player.Mode.TRAUM) + main.player.look_enabled = true + main.player.global_position = von + main.player.velocity = Vector3.ZERO + main.player.yaw = yaw + Input.action_press("vor") + for i in range(frames): + await get_tree().physics_frame + Input.action_release("vor") + return main.player.global_position + + # -------------------------------------------------------------------- func test_start() -> void: @@ -71,31 +86,31 @@ func test_start() -> void: 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(inter.size() == 1 and inter[0].get_meta("kind") == "erde", + "Einzige Interaktion in der Traumwelt ist das Tor zur Erde (%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)") +func test_journal() -> void: + abschnitt("Journal") 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().journal_panel("techniken") + p(ui().offen and ui()._kind == "journal", "Journal öffnet sich") + var knoepfe := 0 + for c in ui()._content.get_children(): + if c is Button: + knoepfe += 1 + p(knoepfe >= TechDB.LIST.size(), "Jede der %d Techniken hat einen Knopf (%d)" % + [TechDB.LIST.size(), knoepfe]) + ui().journal_panel("missionen") + p(ui()._journal_seite == "missionen" and ui()._content.get_child_count() > 0, + "Missionsseite baut auf") ui().schliessen() p(Game.learn("pomodoro"), "Pomodoro gelernt") @@ -111,51 +126,96 @@ func test_bibliothek() -> void: 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) +func test_traumdorf() -> void: + abschnitt("Traumwesen leben ihren Tag") + var wesen: Array = welt().bewohner + p(wesen.size() == 4, "Vier Wesen wohnen in der Traumwelt (%d)" % wesen.size()) - # 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) + var haeuser := 0 + for key in DreamDB.PLACES: + var s: Array = DreamDB.PLACES[key]["size"] + if s[0] > 0.0: + haeuser += 1 + p(haeuser == 4, "Es gibt vier Häuser (%d)" % haeuser) - 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 + var eigene: Dictionary = {} + for w in wesen: + var haus := "haus_" + str(w.data["id"]) + p(DreamDB.PLACES.has(haus), "%s hat ein eigenes Haus (%s)" % [w.data["name"], haus]) + eigene[haus] = true + var kommt_vor := false + for e in w.data["routine"]: + if e[1] == haus: + kommt_vor = true + p(kommt_vor, "Die Routine von %s führt ins eigene Haus" % w.data["name"]) + p(eigene.size() == 4, "Jedes Wesen hat ein anderes Haus") + + var luecken := 0 + for w in wesen: + for h in range(24): + var r: Array = w._routine_at(float(h) + 0.5) + if not DreamDB.PLACES.has(r[0]) or not Orte.ACTIVITY_TEXT.has(r[1]): + luecken += 1 + p(luecken == 0, "Alle vier haben rund um die Uhr Ort und Tätigkeit") + + # Ein voller Tag am Stück: niemand bleibt hängen + Game.time_h = 0.0 + Game.day = 1 + var besucht: Dictionary = {} + for w in wesen: + besucht[w.data["id"]] = {} + for i in range(9000): + schritte(1) + for w in wesen: + if w._path.is_empty(): + besucht[w.data["id"]][w.place] = true + if Game.time_h >= 23.5: + break + var alle_ok := true + for w in wesen: + var soll: Dictionary = {} + for e in w.data["routine"]: + soll[e[1]] = true + var ist: int = besucht[w.data["id"]].size() + if ist < soll.size(): + alle_ok = false + p(false, "%s erreichte nur %d von %d Stationen" % [w.data["name"], ist, soll.size()]) + p(alle_ok, "Über einen ganzen Tag erreicht jedes Wesen jede Station seiner Routine") + + var zuhause := 0 + schritte(20, 0.05, 1.0) + for w in wesen: + var r: Array = w._routine_at(1.0) + if w.place == r[0]: + zuhause += 1 + p(zuhause == 4, "Um 01:00 sind alle vier an ihrem Nachtplatz (%d)" % zuhause) + + +func test_haus_betreten() -> void: + abschnitt("Orte betreten") + var haus := Orte.center(DreamDB.PLACES, "haus_ailen") + var tuer := Orte.door(DreamDB.PLACES, "haus_ailen") + + var ziel: Vector3 = await laufe(tuer + Vector3(0, 1.0, 1.5), 0.0, 80) + var drin: bool = absf(ziel.x - haus.x) < 6.0 and absf(ziel.z - haus.z) < 5.0 + p(drin, "Durch die Tür läuft man ins Haus (Ziel %.1f / %.1f, Haus %.1f / %.1f)" % + [ziel.x, ziel.z, haus.x, haus.z]) + + # Die Wand hält + var vor_wand := haus + Vector3(-11.0, 1.0, 0) + var gestoppt: Vector3 = await laufe(vor_wand, -PI * 0.5, 80) + p(gestoppt.x < haus.x - 5.5, + "Die Seitenwand hält den Spieler auf (x = %.1f, Wand bei %.1f)" % + [gestoppt.x, haus.x - 6.0]) + + main.player.set_mode(Player.Mode.TRAUM) + main.player.global_position = (welt() as DreamWorld).spawn 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().journal_panel("missionen") + p(ui().offen and ui()._journal_seite == "missionen", "Missionsseite des Journals offen") ui().schliessen() p(Game.accept_mission("lena"), "Mission Lena angenommen") p(Game.active_mission == "lena", "Mission ist aktiv") @@ -180,7 +240,7 @@ func test_reise_zur_erde() -> void: func test_tagesablauf() -> void: abschnitt("Tagesablauf der Menschen") - var lena: Human = null + var lena: Bewohner = null for m in welt().humans: if m.data["id"] == "lena": lena = m @@ -188,7 +248,7 @@ func test_tagesablauf() -> void: 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, + p(lena.global_position.distance_to(Orte.center(HumanDB.PLACES, "haus_lena")) < 4.0, "Sie schläft in ihrem Atelier") var gewandert := false @@ -199,9 +259,6 @@ func test_tagesablauf() -> void: 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): @@ -212,53 +269,57 @@ func test_tagesablauf() -> void: 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]): + if not HumanDB.PLACES.has(r[0]) or not Orte.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 = {} + Game.day = 1 + var besucht: Dictionary = {} for m in welt().humans: - besuchte[m.data["id"]] = {} - for i in range(7200): + besucht[m.data["id"]] = {} + for i in range(9000): 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: + besucht[m.data["id"]][m.place] = true + if Game.time_h >= 23.5: 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(): + var ist: int = besucht[m.data["id"]].size() + if ist < soll.size(): alle_ok = false - p(false, "%s erreichte nur %d von %d Orten" % [m.data["name"], orte, soll.size()]) + p(false, "%s erreichte nur %d von %d Stationen" % [m.data["name"], ist, 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") + _wege_pruefen("Erde", HumanDB.PLACES, HumanDB.LIST) + _wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST) + + +func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array) -> void: 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: + for m in liste: 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 dauer := _weg_laenge(places, von, nach) / Bewohner.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 @@ -270,13 +331,13 @@ func test_wege_machbar() -> void: 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) + p(alles_ok, "%s — jeder Weg passt, engster Fall: %s" % [welt_name, engste_text]) -func _weg_laenge(von: String, nach: String) -> float: - var pts: Array = HumanDB.route(von, nach, Vector3.ZERO) +func _weg_laenge(places: Dictionary, von: String, nach: String) -> float: + var pts: Array = Orte.route(places, von, nach, Vector3.ZERO) var l := 0.0 - var pos := HumanDB.place_center(von) + var pos := Orte.center(places, von) for pt in pts: l += pos.distance_to(pt) pos = pt @@ -293,11 +354,10 @@ func test_geist() -> void: 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(main.player.global_position.x > 0.5, "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 lena: Bewohner = welt().humans[0] var davor: Vector3 = lena.global_position main.player.global_position = lena.global_position + Vector3(0, 1, 0.5) schritte(10) @@ -306,18 +366,18 @@ func test_geist() -> void: func test_technik_anwenden() -> void: abschnitt("Techniken anwenden — Erfolgschance") - var lena: Human = null + var lena: Bewohner = 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) + schritte(1, 0.05, 10.2) if lena.place == "haus_lena" and lena._path.is_empty(): break p(lena.activity == "zoegert", "Lena zögert gerade") + main.player.set_mode(Player.Mode.GEIST) 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") @@ -329,27 +389,23 @@ func test_technik_anwenden() -> void: 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") + p(passend["chance"] > unpassend["chance"], "Passender Moment erhöht die Chance") + p(passend["tag"], "Pomodoro trifft Lenas Thema Aufschieben") 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") + p(Game.evaluate("pomodoro", "lena", "schlaeft", false)["blocked"], + "Schlafende Menschen sind nicht erreichbar") + p(Game.evaluate("pomodoro", "lena", "zoegert", true)["chance"] < passend["chance"], + "Wer abblockt, ist schwerer zu erreichen") + p(Game.evaluate("nein_sagen", "lena", "zoegert", false)["blocked"], + "Ungelernte Techniken lassen sich nicht anwenden") var vorher_p: float = Game.mission_progress("lena") var res := Game.apply_technique("pomodoro", "lena", "zoegert", false) @@ -360,7 +416,6 @@ func test_technik_anwenden() -> void: 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 @@ -390,34 +445,69 @@ func test_mission_abschliessen() -> void: 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] + var lena: Bewohner = 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") +func test_techniken_verbessern_sich() -> void: + abschnitt("Techniken verbessern sich durch Anwendung") + Game.einsicht = 2 + p(Game.learn("reframing"), "Technik im Journal gelernt") + p(Game.skill_of("reframing") == 1, "Start auf Stufe 1") + var einsicht_vorher := Game.einsicht + var anwendungen := 0 + while Game.skill_of("reframing") < 3 and anwendungen < 60: + Game.apply_technique("reframing", "mira", "gruebelt", false) + anwendungen += 1 + p(Game.skill_of("reframing") >= 3, + "Nach %d Anwendungen von allein auf Stufe %d" % [anwendungen, Game.skill_of("reframing")]) + p(Game.einsicht == einsicht_vorher, "Das kostet keine Einsicht") + + var vor_xp: int = Game.known["reframing"] + var res := Game.apply_technique("reframing", "mira", "essen", false) + p(Game.known["reframing"] > vor_xp, + "Auch ein %s bringt Erfahrung" % ("Erfolg" if res["success"] else "Fehlversuch")) + + var xp_vor: int = Game.known["reframing"] + p(Game.deepen("reframing"), "Vertiefen im Journal funktioniert") + p(Game.known["reframing"] == xp_vor + 3, "Vertiefen bringt 3 EP") + p(Game.einsicht == einsicht_vorher - 1, "Vertiefen kostet 1 Einsicht") + + +func test_journal_ueberall() -> void: + abschnitt("Journal ist immer dabei") + p(InputMap.has_action("journal"), "Eigene Aktion für das Journal") + var tasten: Array = [] + for ev in InputMap.action_get_events("journal"): + tasten.append(ev.physical_keycode) + p(tasten.has(KEY_J) and tasten.has(KEY_TAB), "Auf J und Tab gelegt") + + p(main.auf_erde, "Test läuft auf der Erde") + ui().journal_panel() + p(ui().offen, "Journal öffnet auf der Erde") ui().schliessen() - ui().vertiefen_panel() - p(ui()._content.get_child_count() > 0, "Meditationskammer listet Techniken") + + main._traumwelt() + await get_tree().process_frame + ui().journal_panel() + p(ui().offen, "Journal öffnet auch in der Traumwelt") 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") + + # Traumwesen sind keine Missionsziele + var wesen: Bewohner = welt().bewohner[0] + main.player.set_mode(Player.Mode.TRAUM) + main.player.global_position = wesen.global_position + Vector3(0, 1.0, 1.2) + main._process(0.05) + p(main._fokus_mensch == null, "Traumwesen lassen sich nicht mit Techniken bearbeiten") + p(not wesen.mit_mission, "Sie tragen keine Missionsmarkierung") + p(wesen.beschriftung().contains(str(wesen.data["rolle"])), + "Ihre Beschriftung nennt Name und Rolle: %s" % wesen.beschriftung().replace("\n", " / ")) func test_steuerung() -> void: abschnitt("Steuerung") var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "sinken", - "interagieren", "kodex"] + "interagieren", "journal"] var alle := true for a in aktionen: if not InputMap.has_action(a) or InputMap.action_get_events(a).is_empty(): @@ -425,7 +515,7 @@ func test_steuerung() -> void: p(false, "Aktion '%s' ist belegt" % a) p(alle, "Alle acht Steuerungsaktionen sind mit Tasten belegt") - main.ui.schliessen() + ui().schliessen() main.player.look_enabled = true main.player.set_mode(Player.Mode.GEIST) main.player.global_position = Vector3(0, 8.0, 0)