This commit is contained in:
team3
2026-08-30 11:50:09 +02:00
parent 4d771c9a6e
commit a46eba4145
23 changed files with 816 additions and 601 deletions

View File

@@ -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

View File

@@ -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": "",

Binary file not shown.

View File

@@ -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.

View File

@@ -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
- 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

View File

@@ -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

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

@@ -0,0 +1 @@
uid://5jrbe4hct8jn

100
scripts/dream_db.gd Normal file
View File

@@ -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 {}

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

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

View File

@@ -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"

View File

@@ -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)

View File

@@ -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):

View File

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

View File

@@ -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)

View File

@@ -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(

75
scripts/orte.gd Normal file
View File

@@ -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

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

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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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" +

View File

@@ -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))

View File

@@ -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)