first commit
This commit is contained in:
1
.godot/.gdignore
Normal file
1
.godot/.gdignore
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
21
.godot/editor/filesystem_cache10
Normal file
21
.godot/editor/filesystem_cache10
Normal file
@@ -0,0 +1,21 @@
|
||||
78098b89c88a74ebe8c20117b8754620
|
||||
::res://::1787989983
|
||||
anforderungen.md::TextFile::-1::1787989000::0::1::::<><><>0<>0<><>::
|
||||
::res://scenes/::1787989294
|
||||
main.tscn::PackedScene::-1::1787989294::0::1::::<><><>0<>0<><>::res://scripts/main.gd
|
||||
::res://scripts/::1787989847
|
||||
dream_world.gd::GDScript::8279158278942867258::1787991037::0::1::::DreamWorld<>Node3D<><>0<>0<><>::
|
||||
earth_world.gd::GDScript::6086818627272428934::1787990934::0::1::::EarthWorld<>Node3D<><>0<>0<><>::
|
||||
game.gd::GDScript::3600922541500868814::1787990324::0::1::::<>Node<><>0<>0<><>::
|
||||
human.gd::GDScript::781542295747092807::1787991037::0::1::::Human<>Node3D<><>0<>0<><>::
|
||||
human_db.gd::GDScript::412604892032291296::1787990324::0::1::::HumanDB<>RefCounted<><>0<>0<><>::
|
||||
main.gd::GDScript::1504009331368977934::1787990676::0::1::::<>Node3D<><>0<>0<><>::
|
||||
player.gd::GDScript::8724108548430371398::1787990898::0::1::::Player<>CharacterBody3D<><>0<>0<><>::
|
||||
prims.gd::GDScript::322632462553361584::1787990855::0::1::::Prims<>RefCounted<><>0<>0<><>::
|
||||
technique_db.gd::GDScript::4543286561661639701::1787991124::0::1::::TechDB<>RefCounted<><>0<>0<><>::
|
||||
ui.gd::GDScript::843471304695966286::1787991124::0::1::::UI<>CanvasLayer<><>0<>0<><>::
|
||||
::res://tests/::1787990698
|
||||
shots.gd::GDScript::5999651346126871724::1787990988::0::1::::<>Node<><>0<>0<><>::
|
||||
shots.tscn::PackedScene::-1::1787990528::0::1::::<><><>0<>0<><>::res://tests/shots.gd
|
||||
verify.gd::GDScript::4392606218392155269::1787991067::0::1::::<>Node<><>0<>0<><>::
|
||||
verify.tscn::PackedScene::-1::1787989983::0::1::::<><><>0<>0<><>::res://tests/verify.gd
|
||||
@@ -0,0 +1,6 @@
|
||||
[folding]
|
||||
|
||||
node_unfolds=[NodePath("."), PackedStringArray("Visibility")]
|
||||
resource_unfolds=[]
|
||||
nodes_folded=[]
|
||||
animation_groups_folded=[]
|
||||
11
.godot/editor/project_metadata.cfg
Normal file
11
.godot/editor/project_metadata.cfg
Normal file
@@ -0,0 +1,11 @@
|
||||
[game_view]
|
||||
|
||||
select_mode=0
|
||||
|
||||
[editor_metadata]
|
||||
|
||||
executable_path="/home/arbeit/bin/godot"
|
||||
|
||||
[recent_files]
|
||||
|
||||
scenes=["res://scenes/main.tscn"]
|
||||
65
.godot/global_script_class_cache.cfg
Normal file
65
.godot/global_script_class_cache.cfg
Normal file
@@ -0,0 +1,65 @@
|
||||
list=[{
|
||||
"base": &"Node3D",
|
||||
"class": &"DreamWorld",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/dream_world.gd"
|
||||
}, {
|
||||
"base": &"Node3D",
|
||||
"class": &"EarthWorld",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/earth_world.gd"
|
||||
}, {
|
||||
"base": &"Node3D",
|
||||
"class": &"Human",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/human.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"HumanDB",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/human_db.gd"
|
||||
}, {
|
||||
"base": &"CharacterBody3D",
|
||||
"class": &"Player",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/player.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"Prims",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/prims.gd"
|
||||
}, {
|
||||
"base": &"RefCounted",
|
||||
"class": &"TechDB",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/technique_db.gd"
|
||||
}, {
|
||||
"base": &"CanvasLayer",
|
||||
"class": &"UI",
|
||||
"icon": "",
|
||||
"is_abstract": false,
|
||||
"is_tool": false,
|
||||
"language": &"GDScript",
|
||||
"path": "res://scripts/ui.gd"
|
||||
}]
|
||||
BIN
.godot/uid_cache.bin
Normal file
BIN
.godot/uid_cache.bin
Normal file
Binary file not shown.
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Traumwandler
|
||||
|
||||
Ein 3D-Spiel in Godot 4.7 (GDScript). Alle Objekte bestehen aus Godot-Primitiven
|
||||
(Box, Zylinder, Kugel, Kapsel) — kein externes Asset.
|
||||
|
||||
Du bist ein Wesen aus der Traumwelt. Menschen tragen Träume mit sich herum, die
|
||||
sie allein nicht bewegt bekommen. Du wirst auf die Erde geschickt, um zu helfen.
|
||||
|
||||
## Starten
|
||||
|
||||
godot --path .
|
||||
|
||||
Steuerung: **WASD** bewegen · **Maus** umsehen · **E** interagieren ·
|
||||
**Tab** Kodex · **Esc** Maus freigeben.
|
||||
Auf der Erde zusätzlich **Leertaste** steigen und **Umschalt** sinken.
|
||||
|
||||
## Spielschleife
|
||||
|
||||
**Traumwelt** (Insel mit drei betretbaren Gebäuden)
|
||||
|
||||
* **Bibliothek der Techniken** — zwölf echte Lösungswege. Jede Technik nennt
|
||||
*Was* sie tut, *Wann* sie passt und *Warum* sie hilft. Lernen kostet 1 Einsicht.
|
||||
* **Missionshalle** — vier Menschen mit ihrem Traum und dem, was im Weg steht.
|
||||
Eine Mission annehmen.
|
||||
* **Meditationskammer** — eine gelernte Technik für 1 Einsicht vertiefen (+3 EP).
|
||||
* **Tor zur Erde** — öffnet sich nur mit angenommener Mission.
|
||||
|
||||
**Erde** (Kleinstadt, ein Tag dauert 6 Minuten)
|
||||
|
||||
* Du bist ein unsichtbarer Geist, schwebst und gehst durch Wände.
|
||||
* Vier Menschen leben ihren Tagesablauf: schlafen, arbeiten, grübeln, essen,
|
||||
spazieren. Jeder Ort und jede Tätigkeit steht fest.
|
||||
* Nah herangehen, **E** drücken, Technik wählen. Das Panel zeigt für jede
|
||||
Technik die Erfolgschance in genau dieser Situation.
|
||||
* Die Säule im Park bringt dich zurück in die Traumwelt.
|
||||
|
||||
## Erfolgschance
|
||||
|
||||
8 % + 11 % je Fertigkeitsstufe
|
||||
+ 25 % wenn die Technik das Thema des Menschen trifft
|
||||
+ 22 % wenn der Moment passt (z. B. Box-Atmung, während jemand angespannt ist)
|
||||
− 30 % wenn der Mensch gerade abblockt
|
||||
begrenzt auf 5 … 95 %
|
||||
|
||||
Schlafende sind nicht erreichbar. Erfolg bringt Fortschritt und 2 EP, ein
|
||||
Fehlversuch 1 EP und eine kurze Sperre. Dieselbe Technik wirkt bei Wiederholung
|
||||
schwächer — es lohnt sich, mehrere zu lernen. 100 % erfüllen den Traum und
|
||||
bringen 3 Einsicht zurück.
|
||||
|
||||
## Aufbau
|
||||
|
||||
project.godot Projekt, Autoload `Game`
|
||||
scenes/main.tscn Einstieg
|
||||
scripts/game.gd Zustand, Regeln, Uhr (Autoload)
|
||||
scripts/technique_db.gd die zwölf Techniken mit ihren Hinweisen
|
||||
scripts/human_db.gd Orte, Menschen, Tagesabläufe, Wegfindung
|
||||
scripts/main.gd Weltenwechsel, Fokus, Interaktion
|
||||
scripts/player.gd Läufer in der Traumwelt, Geist auf der Erde
|
||||
scripts/human.gd Mensch mit Tagesablauf und Reaktionen
|
||||
scripts/dream_world.gd Traumwelt inklusive Innenräume
|
||||
scripts/earth_world.gd Stadt mit Tag-Nacht-Wechsel
|
||||
scripts/ui.gd HUD und alle Panels
|
||||
scripts/prims.gd Bausteine aus Primitiven
|
||||
|
||||
## Prüfen
|
||||
|
||||
godot --headless --path . tests/verify.tscn # 81 Prüfungen der Spiellogik
|
||||
godot --path . tests/shots.tscn # 16 Screenshots nach user://shots/
|
||||
|
||||
`verify` fährt das echte Spiel: Gebäude betreten, Technik lernen, Mission
|
||||
annehmen, zur Erde reisen, Tagesabläufe über einen ganzen Tag, Erfolgschancen,
|
||||
Mission abschließen, Tastensteuerung. Beendet sich mit Code 1, wenn etwas fehlt.
|
||||
14
anforderungen.md
Normal file
14
anforderungen.md
Normal file
@@ -0,0 +1,14 @@
|
||||
Funktionen Traumwelt
|
||||
- Bewegen
|
||||
- Gebäude betretten
|
||||
- Techniken lernen, echte Lösungen für Probleme
|
||||
- Techniken haben Hinweise, was sie machen, wann sie angewand werden und warum sie helfen
|
||||
- Missionen annehmen, also einem Menschen helfen
|
||||
|
||||
Funktionen Erde
|
||||
- Menschen mit Techniken manipulieren, Erfolgschance hängt vom Skill ab
|
||||
- Bewegung als Geist unsichtbar
|
||||
- Menschen haben ihre Tagesabläufe als Slice of Life
|
||||
|
||||
Kern
|
||||
- Wesen aus einer Traumwelt werden auf die Erde geschickt um Menschen helfen ihre Träume zu erfüllen
|
||||
23
project.godot
Normal file
23
project.godot
Normal file
@@ -0,0 +1,23 @@
|
||||
; Engine configuration file.
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Traumwandler"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||
|
||||
[autoload]
|
||||
|
||||
Game="*res://scripts/game.gd"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1280
|
||||
window/size/viewport_height=720
|
||||
window/stretch/mode="canvas_items"
|
||||
|
||||
[rendering]
|
||||
|
||||
anti_aliasing/quality/msaa_3d=2
|
||||
6
scenes/main.tscn
Normal file
6
scenes/main.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/main.gd" id="1"]
|
||||
|
||||
[node name="Main" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
254
scripts/dream_world.gd
Normal file
254
scripts/dream_world.gd
Normal file
@@ -0,0 +1,254 @@
|
||||
extends Node3D
|
||||
class_name DreamWorld
|
||||
## Die Traumwelt: Insel mit drei betretbaren Gebäuden und dem Tor zur Erde.
|
||||
|
||||
const INNEN_Y := -300.0
|
||||
|
||||
var spawn := Vector3(0, 2.0, 14.0)
|
||||
var _deko: Array = []
|
||||
var _regale: Dictionary = {} # tech_id -> MeshInstance3D
|
||||
var _stelen: Dictionary = {} # human_id -> Dictionary
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_environment()
|
||||
_insel()
|
||||
_gebaeude_bibliothek()
|
||||
_gebaeude_missionshalle()
|
||||
_gebaeude_meditation()
|
||||
_tor_zur_erde()
|
||||
Game.state_changed.connect(_refresh)
|
||||
_refresh()
|
||||
|
||||
|
||||
func _environment() -> void:
|
||||
var we := WorldEnvironment.new()
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_SKY
|
||||
var sky := Sky.new()
|
||||
var mat := ProceduralSkyMaterial.new()
|
||||
mat.sky_top_color = Color(0.10, 0.05, 0.24)
|
||||
mat.sky_horizon_color = Color(0.42, 0.24, 0.55)
|
||||
mat.ground_bottom_color = Color(0.06, 0.03, 0.14)
|
||||
mat.ground_horizon_color = Color(0.32, 0.18, 0.45)
|
||||
mat.sun_angle_max = 40.0
|
||||
sky.sky_material = mat
|
||||
env.sky = sky
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||||
env.ambient_light_energy = 1.8
|
||||
env.glow_enabled = true
|
||||
env.glow_intensity = 0.5
|
||||
env.glow_bloom = 0.15
|
||||
env.fog_enabled = true
|
||||
env.fog_light_color = Color(0.30, 0.18, 0.45)
|
||||
env.fog_density = 0.006
|
||||
we.environment = env
|
||||
add_child(we)
|
||||
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.rotation_degrees = Vector3(-46, 25, 0)
|
||||
sun.light_energy = 1.15
|
||||
sun.light_color = Color(0.85, 0.80, 1.0)
|
||||
sun.shadow_enabled = true
|
||||
sun.shadow_blur = 2.0
|
||||
add_child(sun)
|
||||
|
||||
|
||||
func _insel() -> void:
|
||||
Prims.cyl(self, Vector3(0, -1.0, 0), 50.0, 2.0, Color(0.28, 0.20, 0.42))
|
||||
Prims.cyl(self, Vector3(0, -4.0, 0), 38.0, 4.0, Color(0.20, 0.14, 0.32), false)
|
||||
Prims.cyl(self, Vector3(0, -9.0, 0), 20.0, 6.0, Color(0.15, 0.10, 0.26), false)
|
||||
# Wege
|
||||
var wege: Array[Vector3] = [Vector3(-22, 0, -6), Vector3(0, 0, -12), Vector3(22, 0, -6),
|
||||
Vector3(0, 0, 8)]
|
||||
for w in wege:
|
||||
var d: Vector3 = w.normalized() * (w.length() * 0.5)
|
||||
var b := Prims.box(self, Vector3(d.x, 0.02, d.z),
|
||||
Vector3(3.0, 0.08, w.length()), Color(0.45, 0.35, 0.62), false)
|
||||
b.rotation.y = atan2(w.x, w.z)
|
||||
# schwebende Deko
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 7
|
||||
for i in range(34):
|
||||
var a := rng.randf() * TAU
|
||||
var r := rng.randf_range(16.0, 46.0)
|
||||
var p := Vector3(cos(a) * r, rng.randf_range(4.0, 22.0), sin(a) * r)
|
||||
var s := rng.randf_range(0.6, 2.0)
|
||||
var c := Color(0.6 + rng.randf() * 0.3, 0.5 + rng.randf() * 0.4, 1.0)
|
||||
var wuerfel := Prims.box(self, p, Vector3(s, s, s), c, false, 1.1)
|
||||
wuerfel.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
_deko.append(wuerfel)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for i in range(_deko.size()):
|
||||
var n: Node3D = _deko[i]
|
||||
n.rotation.y += delta * (0.2 + 0.05 * (i % 5))
|
||||
n.rotation.x += delta * 0.13
|
||||
n.position.y += sin(Time.get_ticks_msec() * 0.0007 + i) * delta * 0.6
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Bausteine
|
||||
|
||||
## Erzeugt Außenbau + Innenraum und verbindet beide mit Tür-Zonen.
|
||||
func _gebaeude(titel: String, a_center: Vector3, a_size: Vector3,
|
||||
i_center: Vector3, i_size: Vector3, farbe: Color) -> Node3D:
|
||||
Prims.room(self, a_center, a_size, farbe, farbe.darkened(0.3), Vector3.BACK, 3.6, false)
|
||||
Prims.box(self, a_center + Vector3(0, a_size.y + 0.25, 0),
|
||||
Vector3(a_size.x + 1.0, 0.5, a_size.z + 1.0), farbe.lightened(0.15))
|
||||
Prims.box(self, a_center + Vector3(0, a_size.y + 1.1, 0),
|
||||
Vector3(a_size.x * 0.5, 1.2, a_size.z * 0.5), farbe.lightened(0.3))
|
||||
Prims.label(self, a_center + Vector3(0, a_size.y + 3.0, 0), titel, 1.4,
|
||||
Color(1, 0.95, 0.8))
|
||||
|
||||
var aussen_tuer := a_center + Vector3(0, 0, a_size.z * 0.5)
|
||||
var draussen := a_center + Vector3(0, 0.2, a_size.z * 0.5 + 3.2)
|
||||
|
||||
var innen := Node3D.new()
|
||||
innen.name = titel.replace(" ", "_")
|
||||
add_child(innen)
|
||||
Prims.room(innen, i_center, i_size, farbe.lightened(0.12), farbe.darkened(0.25),
|
||||
Vector3.BACK, 3.6, true)
|
||||
Prims.box(innen, i_center + Vector3(0, i_size.y + 0.2, 0),
|
||||
Vector3(i_size.x, 0.4, i_size.z), farbe.darkened(0.4))
|
||||
for sx: float in [-1.0, 1.0]:
|
||||
for sz: float in [-1.0, 1.0]:
|
||||
var l := OmniLight3D.new()
|
||||
l.position = i_center + Vector3(sx * i_size.x * 0.28, i_size.y - 0.8, sz * i_size.z * 0.28)
|
||||
l.omni_range = 34.0
|
||||
l.light_energy = 4.5
|
||||
l.light_color = Color(0.90, 0.90, 1.0)
|
||||
innen.add_child(l)
|
||||
|
||||
var innen_spawn := i_center + Vector3(0, 0.3, i_size.z * 0.5 - 3.0)
|
||||
_tuer(self, aussen_tuer + Vector3(0, 1.2, 0), innen_spawn)
|
||||
_tuer(innen, i_center + Vector3(0, 1.2, i_size.z * 0.5), draussen)
|
||||
Prims.label(innen, i_center + Vector3(0, i_size.y - 0.9, i_size.z * 0.5 - 0.35), "Ausgang", 0.45,
|
||||
Color(1, 0.85, 0.6))
|
||||
return innen
|
||||
|
||||
|
||||
func _tuer(parent: Node, pos: Vector3, ziel: Vector3) -> void:
|
||||
var a := Area3D.new()
|
||||
a.position = pos
|
||||
var cs := CollisionShape3D.new()
|
||||
var sh := BoxShape3D.new()
|
||||
sh.size = Vector3(3.4, 2.4, 1.6)
|
||||
cs.shape = sh
|
||||
a.add_child(cs)
|
||||
parent.add_child(a)
|
||||
a.body_entered.connect(func(body: Node3D) -> void:
|
||||
if body is Player and Game.teleport_lock <= 0.0:
|
||||
Game.teleport_lock = 0.9
|
||||
body.global_position = ziel
|
||||
body.velocity = Vector3.ZERO)
|
||||
|
||||
|
||||
func _interaktion(parent: Node, pos: Vector3, kind: String, arg: String, titel: String) -> Node3D:
|
||||
var n := Node3D.new()
|
||||
n.position = pos
|
||||
parent.add_child(n)
|
||||
n.add_to_group("interactable")
|
||||
n.set_meta("kind", kind)
|
||||
n.set_meta("arg", arg)
|
||||
n.set_meta("titel", titel)
|
||||
return n
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Gebäude
|
||||
|
||||
func _gebaeude_bibliothek() -> void:
|
||||
var innen := _gebaeude("Bibliothek der Techniken",
|
||||
Vector3(-24, 0, -16), Vector3(16, 5, 14),
|
||||
Vector3(0, INNEN_Y, 0), Vector3(34, 6, 24),
|
||||
Color(0.58, 0.50, 0.80))
|
||||
Prims.label(innen, Vector3(0, INNEN_Y + 5.0, -10.5),
|
||||
"Berühre eine Technik, um ihre Hinweise zu lesen.", 0.4, Color(0.9, 0.9, 1))
|
||||
var ids := TechDB.ids()
|
||||
for i in range(ids.size()):
|
||||
var reihe := i / 6
|
||||
var spalte := i % 6
|
||||
var p := Vector3(-12.5 + spalte * 5.0, INNEN_Y, -6.5 + reihe * 7.0)
|
||||
var t := TechDB.by_id(ids[i])
|
||||
Prims.cyl(innen, p + Vector3(0, 0.5, 0), 0.8, 1.0, Color(0.30, 0.26, 0.45))
|
||||
var kristall := Prims.box(innen, p + Vector3(0, 1.8, 0), Vector3(0.9, 0.9, 0.9),
|
||||
t["farbe"], false, 1.4)
|
||||
kristall.rotation = Vector3(0.6, 0.8, 0.0)
|
||||
_regale[ids[i]] = kristall
|
||||
Prims.label(innen, p + Vector3(0, 3.2 + float(reihe) * 0.6, 0), t["name"], 0.36, Color(1, 1, 1))
|
||||
_interaktion(innen, p + Vector3(0, 1.2, 0), "technik", ids[i], t["name"])
|
||||
|
||||
|
||||
func _gebaeude_missionshalle() -> void:
|
||||
var innen := _gebaeude("Missionshalle",
|
||||
Vector3(0, 0, -28), Vector3(18, 5, 14),
|
||||
Vector3(90, INNEN_Y, 0), Vector3(26, 6, 20),
|
||||
Color(0.74, 0.55, 0.48))
|
||||
Prims.label(innen, Vector3(90, INNEN_Y + 5.0, -8.5),
|
||||
"Menschen, deren Traum feststeckt.", 0.42, Color(1, 0.92, 0.8))
|
||||
for i in range(HumanDB.LIST.size()):
|
||||
var h: Dictionary = HumanDB.LIST[i]
|
||||
var p := Vector3(90.0 + (float(i) - 1.5) * 7.0, INNEN_Y, -3.0)
|
||||
Prims.cyl(innen, p + Vector3(0, 0.4, 0), 1.2, 0.8, Color(0.35, 0.28, 0.26))
|
||||
var saeule := Prims.box(innen, p + Vector3(0, 2.0, 0), Vector3(1.4, 3.2, 1.4),
|
||||
h["farbe"], false, 0.8)
|
||||
var sphere := Prims.sphere(innen, p + Vector3(0, 4.1, 0), 0.55,
|
||||
(h["farbe"] as Color).lightened(0.4), 1.6)
|
||||
var lab := Prims.label(innen, p + Vector3(0, 5.2, 0), h["name"], 0.32, Color(1, 1, 1))
|
||||
_stelen[h["id"]] = {"saeule": saeule, "kugel": sphere, "label": lab}
|
||||
_interaktion(innen, p + Vector3(0, 1.5, 0), "mission", h["id"], h["name"])
|
||||
|
||||
|
||||
func _gebaeude_meditation() -> void:
|
||||
var innen := _gebaeude("Meditationskammer",
|
||||
Vector3(24, 0, -16), Vector3(14, 5, 14),
|
||||
Vector3(-90, INNEN_Y, 0), Vector3(20, 6, 18),
|
||||
Color(0.46, 0.66, 0.74))
|
||||
Prims.label(innen, Vector3(-90, INNEN_Y + 5.0, -7.5),
|
||||
"Übung vertieft, was du schon kannst.", 0.42, Color(0.85, 1, 1))
|
||||
Prims.cyl(innen, Vector3(-90, INNEN_Y + 0.5, -1.0), 3.0, 1.0, Color(0.24, 0.34, 0.40))
|
||||
var kugel := Prims.sphere(innen, Vector3(-90, INNEN_Y + 2.6, -1.0), 1.2,
|
||||
Color(0.55, 0.95, 1.0), 1.8)
|
||||
kugel.name = "Meditationskugel"
|
||||
_interaktion(innen, Vector3(-90, INNEN_Y + 1.5, -1.0), "vertiefen", "", "Meditationskugel")
|
||||
|
||||
|
||||
func _tor_zur_erde() -> void:
|
||||
var pos := Vector3(0, 0, 18)
|
||||
Prims.cyl(self, pos + Vector3(0, 0.3, 0), 5.0, 0.6, Color(0.35, 0.30, 0.50))
|
||||
for i in range(10):
|
||||
var a := PI * float(i) / 9.0
|
||||
Prims.box(self, pos + Vector3(cos(a) * 4.2, sin(a) * 4.2 + 0.6, 0),
|
||||
Vector3(0.7, 0.7, 0.7), Color(0.5, 0.85, 1.0), false, 1.8)
|
||||
var scheibe := Prims.cyl(self, pos + Vector3(0, 2.6, 0), 3.0, 0.2,
|
||||
Color(0.45, 0.75, 1.0, 0.28), false, 1.0)
|
||||
scheibe.rotation.x = PI * 0.5
|
||||
Prims.label(self, pos + Vector3(0, 6.4, 0), "Tor zur Erde", 0.7, Color(0.7, 0.95, 1))
|
||||
_interaktion(self, pos + Vector3(0, 1.6, 0), "erde", "", "Tor zur Erde")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Aktualisieren
|
||||
|
||||
func _refresh() -> void:
|
||||
for id in _regale:
|
||||
var m: MeshInstance3D = _regale[id]
|
||||
var t := TechDB.by_id(id)
|
||||
if Game.knows(id):
|
||||
m.material_override = Prims.mat(t["farbe"], 2.2)
|
||||
m.scale = Vector3.ONE * (1.0 + 0.12 * Game.skill_of(id))
|
||||
else:
|
||||
m.material_override = Prims.mat((t["farbe"] as Color).darkened(0.35), 0.5)
|
||||
m.scale = Vector3.ONE * 0.75
|
||||
for id in _stelen:
|
||||
var s: Dictionary = _stelen[id]
|
||||
var h := HumanDB.by_id(id)
|
||||
var lab: Label3D = s["label"]
|
||||
if Game.completed.has(id):
|
||||
s["kugel"].material_override = Prims.mat(Color(0.5, 1.0, 0.6), 2.4)
|
||||
lab.text = h["name"] + "\n★ erfüllt"
|
||||
elif Game.active_mission == id:
|
||||
s["kugel"].material_override = Prims.mat(Color(1, 0.85, 0.35), 2.4)
|
||||
lab.text = h["name"] + "\naktive Mission · %d %%" % int(Game.mission_progress(id))
|
||||
else:
|
||||
s["kugel"].material_override = Prims.mat((h["farbe"] as Color).lightened(0.4), 1.2)
|
||||
lab.text = h["name"] + "\nwartet"
|
||||
1
scripts/dream_world.gd.uid
Normal file
1
scripts/dream_world.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dp7qr6rgsihfu
|
||||
185
scripts/earth_world.gd
Normal file
185
scripts/earth_world.gd
Normal file
@@ -0,0 +1,185 @@
|
||||
extends Node3D
|
||||
class_name EarthWorld
|
||||
## Die Erde: eine Kleinstadt mit vier Menschen, die ihren Tag leben.
|
||||
|
||||
var spawn := Vector3(0, 3.0, 12.0)
|
||||
var humans: Array = []
|
||||
|
||||
var _sun: DirectionalLight3D
|
||||
var _sky: ProceduralSkyMaterial
|
||||
var _env: Environment
|
||||
var _nachtlichter: Array = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_environment()
|
||||
_boden()
|
||||
_orte()
|
||||
_park()
|
||||
_saeule()
|
||||
_menschen()
|
||||
_himmel_aktualisieren()
|
||||
|
||||
|
||||
func _environment() -> void:
|
||||
var we := WorldEnvironment.new()
|
||||
_env = Environment.new()
|
||||
_env.background_mode = Environment.BG_SKY
|
||||
var sky := Sky.new()
|
||||
_sky = ProceduralSkyMaterial.new()
|
||||
sky.sky_material = _sky
|
||||
_env.sky = sky
|
||||
_env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||||
_env.glow_enabled = true
|
||||
_env.glow_intensity = 0.35
|
||||
_env.fog_enabled = true
|
||||
_env.fog_density = 0.0012
|
||||
we.environment = _env
|
||||
add_child(we)
|
||||
|
||||
_sun = DirectionalLight3D.new()
|
||||
_sun.shadow_enabled = true
|
||||
_sun.shadow_blur = 1.5
|
||||
add_child(_sun)
|
||||
|
||||
|
||||
func _boden() -> void:
|
||||
Prims.box(self, Vector3(0, -0.5, 0), Vector3(240, 1, 240), Color(0.36, 0.52, 0.30))
|
||||
# Ringweg, auf dem die Menschen gehen
|
||||
for i in range(36):
|
||||
var a := TAU * float(i) / 36.0
|
||||
Prims.box(self, Vector3(cos(a) * 15.0, 0.03, sin(a) * 15.0),
|
||||
Vector3(2.8, 0.06, 2.8), Color(0.62, 0.58, 0.50), false)
|
||||
|
||||
|
||||
func _orte() -> void:
|
||||
for key in HumanDB.PLACES:
|
||||
if key == "park":
|
||||
continue
|
||||
var pl: Dictionary = HumanDB.PLACES[key]
|
||||
var c := HumanDB.place_center(key)
|
||||
var s: Array = pl["size"]
|
||||
var groesse := Vector3(s[0], 2.3, s[1])
|
||||
var farbe: Color = pl["color"]
|
||||
var richtung: Vector3 = HumanDB.DIR_VEC[pl["dir"]]
|
||||
Prims.room(self, c, groesse, farbe, farbe.darkened(0.35), richtung, 3.4, true)
|
||||
Prims.label(self, c + Vector3(0, 5.0, 0), pl["name"], 0.6, Color(1, 1, 0.9))
|
||||
_einrichtung(c, groesse, farbe, key)
|
||||
|
||||
var tuer := HumanDB.place_door(key)
|
||||
var weg := tuer.normalized() * 15.0 - tuer
|
||||
var b := Prims.box(self, (tuer + tuer.normalized() * 15.0) * 0.5 + Vector3(0, 0.03, 0),
|
||||
Vector3(2.6, 0.06, weg.length()), Color(0.62, 0.58, 0.50), false)
|
||||
b.rotation.y = atan2(weg.x, weg.z)
|
||||
|
||||
var l := OmniLight3D.new()
|
||||
l.position = c + Vector3(0, 2.0, 0)
|
||||
l.omni_range = 20.0
|
||||
l.light_color = Color(1.0, 0.85, 0.6)
|
||||
l.light_energy = 0.0
|
||||
add_child(l)
|
||||
_nachtlichter.append(l)
|
||||
|
||||
|
||||
func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
|
||||
var h := groesse.z * 0.5 - 1.6
|
||||
var w := groesse.x * 0.5 - 1.6
|
||||
if key.begins_with("haus") or key == "wohnheim":
|
||||
Prims.box(self, c + Vector3(-w + 1.0, 0.35, -h + 1.2), Vector3(2.0, 0.7, 3.4),
|
||||
Color(0.85, 0.85, 0.90)) # Bett
|
||||
Prims.box(self, c + Vector3(w - 1.0, 0.75, 0), Vector3(1.6, 0.15, 2.6),
|
||||
farbe.darkened(0.5)) # Schreibtisch
|
||||
Prims.box(self, c + Vector3(w - 1.0, 1.05, 0), Vector3(0.9, 0.6, 0.1),
|
||||
Color(0.2, 0.25, 0.3)) # Bildschirm
|
||||
elif key == "cafe":
|
||||
for i in range(3):
|
||||
Prims.cyl(self, c + Vector3(-3.0 + i * 3.0, 0.4, 0.5), 0.8, 0.8,
|
||||
Color(0.55, 0.40, 0.30))
|
||||
Prims.box(self, c + Vector3(0, 0.55, -3.2), Vector3(groesse.x - 3.0, 1.1, 0.8),
|
||||
Color(0.45, 0.32, 0.24))
|
||||
elif key == "buero":
|
||||
for i in range(4):
|
||||
Prims.box(self, c + Vector3(-4.5 + i * 3.0, 0.75, -1.0), Vector3(2.2, 0.15, 1.4),
|
||||
Color(0.75, 0.75, 0.78))
|
||||
elif key == "baeckerei":
|
||||
Prims.box(self, c + Vector3(0, 0.55, 2.0), Vector3(groesse.x - 3.0, 1.1, 1.0),
|
||||
Color(0.70, 0.55, 0.35))
|
||||
Prims.box(self, c + Vector3(0, 0.85, -3.0), Vector3(3.0, 1.7, 1.5), Color(0.4, 0.4, 0.42))
|
||||
elif key == "uni":
|
||||
for i in range(3):
|
||||
Prims.box(self, c + Vector3(0, 0.5, -2.0 + i * 2.2), Vector3(groesse.x - 4.0, 0.2, 0.8),
|
||||
Color(0.66, 0.62, 0.58))
|
||||
Prims.box(self, c + Vector3(0, 1.1, -4.0), Vector3(5.0, 1.6, 0.15), Color(0.15, 0.25, 0.20))
|
||||
|
||||
|
||||
func _park() -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 3
|
||||
Prims.cyl(self, Vector3(-6, 0.05, 2), 4.0, 0.12, Color(0.30, 0.50, 0.72), false)
|
||||
for i in range(14):
|
||||
var a := rng.randf() * TAU
|
||||
var r := rng.randf_range(5.0, 12.0)
|
||||
var p := Vector3(cos(a) * r, 0, sin(a) * r + 2.0)
|
||||
if p.length() < 4.0:
|
||||
continue
|
||||
var hoehe := rng.randf_range(2.4, 4.2)
|
||||
Prims.cyl(self, p + Vector3(0, hoehe * 0.5, 0), 0.3, hoehe, Color(0.35, 0.26, 0.18))
|
||||
Prims.sphere(self, p + Vector3(0, hoehe + 0.8, 0), rng.randf_range(1.2, 1.9),
|
||||
Color(0.25, 0.5 + rng.randf() * 0.2, 0.22))
|
||||
for i in range(4):
|
||||
var a := TAU * float(i) / 4.0 + 0.4
|
||||
var p := Vector3(cos(a) * 8.0, 0.45, sin(a) * 8.0 + 2.0)
|
||||
var b := Prims.box(self, p, Vector3(2.4, 0.15, 0.7), Color(0.55, 0.40, 0.28))
|
||||
b.rotation.y = -a
|
||||
Prims.label(self, Vector3(0, 5.0, 10.0), "Stadtpark", 0.55, Color(1, 1, 0.9))
|
||||
|
||||
|
||||
func _saeule() -> void:
|
||||
var pos := Vector3(0, 0, 12.0)
|
||||
Prims.cyl(self, pos + Vector3(0, 0.2, 0), 2.6, 0.4, Color(0.50, 0.48, 0.55), false)
|
||||
Prims.cyl(self, pos + Vector3(0, 2.2, 0), 0.7, 4.0, Color(0.55, 0.80, 1.0), false, 1.6)
|
||||
Prims.sphere(self, pos + Vector3(0, 4.6, 0), 0.9, Color(0.75, 0.92, 1.0), 2.2)
|
||||
Prims.label(self, pos + Vector3(0, 6.0, 0), "Rückkehr in die Traumwelt", 0.4,
|
||||
Color(0.75, 0.95, 1))
|
||||
var n := Node3D.new()
|
||||
n.position = pos + Vector3(0, 1.6, 0)
|
||||
add_child(n)
|
||||
n.add_to_group("interactable")
|
||||
n.set_meta("kind", "traum")
|
||||
n.set_meta("arg", "")
|
||||
n.set_meta("titel", "Rückkehr in die Traumwelt")
|
||||
|
||||
|
||||
func _menschen() -> void:
|
||||
for i in range(HumanDB.LIST.size()):
|
||||
var m := Human.new()
|
||||
add_child(m)
|
||||
m.setup(HumanDB.LIST[i], i)
|
||||
humans.append(m)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_himmel_aktualisieren()
|
||||
|
||||
|
||||
func _himmel_aktualisieren() -> void:
|
||||
var t: float = Game.time_h
|
||||
var sonne := clampf(sin((t - 6.0) / 12.0 * PI), 0.0, 1.0)
|
||||
_sun.rotation_degrees = Vector3(-((t - 6.0) / 12.0) * 180.0, -35.0, 0.0)
|
||||
_sun.light_energy = 0.25 + sonne * 1.0
|
||||
_sun.light_color = Color(1.0, 0.75 + sonne * 0.25, 0.55 + sonne * 0.4)
|
||||
|
||||
var tag_top := Color(0.30, 0.55, 0.90)
|
||||
var nacht_top := Color(0.03, 0.04, 0.12)
|
||||
var tag_hor := Color(0.75, 0.85, 0.95)
|
||||
var nacht_hor := Color(0.10, 0.10, 0.25)
|
||||
_sky.sky_top_color = nacht_top.lerp(tag_top, sonne)
|
||||
_sky.sky_horizon_color = nacht_hor.lerp(tag_hor, sonne)
|
||||
_sky.ground_horizon_color = nacht_hor.lerp(Color(0.55, 0.60, 0.55), sonne)
|
||||
_sky.ground_bottom_color = nacht_top.lerp(Color(0.30, 0.38, 0.28), sonne)
|
||||
_env.ambient_light_energy = 0.45 + sonne * 0.8
|
||||
_env.fog_light_color = nacht_hor.lerp(tag_hor, sonne)
|
||||
|
||||
var nacht := 1.0 - sonne
|
||||
for l in _nachtlichter:
|
||||
l.light_energy = clampf(nacht * 2.4 - 0.3, 0.0, 2.2)
|
||||
1
scripts/earth_world.gd.uid
Normal file
1
scripts/earth_world.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://csyrapuo8qnac
|
||||
223
scripts/game.gd
Normal file
223
scripts/game.gd
Normal file
@@ -0,0 +1,223 @@
|
||||
extends Node
|
||||
## Globaler Spielzustand: Wissen, Fertigkeiten, Missionen, Uhrzeit.
|
||||
|
||||
signal state_changed
|
||||
signal notified(text: String, color: Color)
|
||||
signal mission_done(human_id: String)
|
||||
|
||||
const DAY_SECONDS := 360.0 # eine Erdenrunde in Echtzeit-Sekunden
|
||||
const XP_LEVELS := [0, 4, 10, 18, 28]
|
||||
const MISSION_ZIEL := 100.0
|
||||
const RESIST_SEKUNDEN := 18.0
|
||||
|
||||
var einsicht: int = 3
|
||||
var known: Dictionary = {} # tech_id -> xp
|
||||
var active_mission: String = "" # human_id oder ""
|
||||
var progress: Dictionary = {} # human_id -> 0..100
|
||||
var used: Dictionary = {} # "human:tech" -> Anzahl
|
||||
var completed: Array = []
|
||||
var time_h: float = 7.0
|
||||
var day: int = 1
|
||||
var teleport_lock: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_input()
|
||||
|
||||
|
||||
func _setup_input() -> void:
|
||||
var binds := {
|
||||
"vor": [KEY_W, KEY_UP],
|
||||
"zurueck": [KEY_S, KEY_DOWN],
|
||||
"links": [KEY_A, KEY_LEFT],
|
||||
"rechts": [KEY_D, KEY_RIGHT],
|
||||
"steigen": [KEY_SPACE],
|
||||
"sinken": [KEY_SHIFT],
|
||||
"interagieren": [KEY_E],
|
||||
"kodex": [KEY_TAB],
|
||||
}
|
||||
for action in binds:
|
||||
if not InputMap.has_action(action):
|
||||
InputMap.add_action(action)
|
||||
for code in binds[action]:
|
||||
var ev := InputEventKey.new()
|
||||
ev.physical_keycode = code
|
||||
InputMap.action_add_event(action, ev)
|
||||
|
||||
|
||||
func advance_time(delta: float) -> void:
|
||||
time_h += delta * 24.0 / DAY_SECONDS
|
||||
while time_h >= 24.0:
|
||||
time_h -= 24.0
|
||||
day += 1
|
||||
|
||||
|
||||
func time_string() -> String:
|
||||
var h := int(time_h)
|
||||
var m := int((time_h - h) * 60.0)
|
||||
return "Tag %d · %02d:%02d" % [day, h, m]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Fertigkeiten
|
||||
|
||||
func knows(tech_id: String) -> bool:
|
||||
return known.has(tech_id)
|
||||
|
||||
|
||||
func skill_of(tech_id: String) -> int:
|
||||
if not known.has(tech_id):
|
||||
return 0
|
||||
var xp: int = known[tech_id]
|
||||
var lvl := 1
|
||||
for i in range(XP_LEVELS.size()):
|
||||
if xp >= XP_LEVELS[i]:
|
||||
lvl = i + 1
|
||||
return lvl
|
||||
|
||||
|
||||
func xp_to_next(tech_id: String) -> String:
|
||||
var lvl := skill_of(tech_id)
|
||||
if lvl >= XP_LEVELS.size():
|
||||
return "Meisterschaft"
|
||||
var need: int = XP_LEVELS[lvl]
|
||||
return "%d / %d EP" % [known[tech_id], need]
|
||||
|
||||
|
||||
func learn(tech_id: String) -> bool:
|
||||
if known.has(tech_id):
|
||||
return false
|
||||
if einsicht < 1:
|
||||
notified.emit("Dafür fehlt dir Einsicht. Hilf erst einem Menschen.", Color(1, 0.6, 0.4))
|
||||
return false
|
||||
einsicht -= 1
|
||||
known[tech_id] = 0
|
||||
var t := TechDB.by_id(tech_id)
|
||||
notified.emit("Technik gelernt: %s" % t["name"], Color(0.6, 1, 0.7))
|
||||
state_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func add_xp(tech_id: String, amount: int) -> void:
|
||||
if not known.has(tech_id):
|
||||
return
|
||||
var before := skill_of(tech_id)
|
||||
known[tech_id] = int(known[tech_id]) + amount
|
||||
if skill_of(tech_id) > before:
|
||||
var t := TechDB.by_id(tech_id)
|
||||
notified.emit("%s ist jetzt Stufe %d." % [t["name"], skill_of(tech_id)], Color(0.7, 0.9, 1))
|
||||
state_changed.emit()
|
||||
|
||||
|
||||
func deepen(tech_id: String) -> bool:
|
||||
if not known.has(tech_id):
|
||||
return false
|
||||
if einsicht < 1:
|
||||
notified.emit("Keine Einsicht übrig.", Color(1, 0.6, 0.4))
|
||||
return false
|
||||
einsicht -= 1
|
||||
add_xp(tech_id, 3)
|
||||
notified.emit("Vertieft: %s (+3 EP)" % TechDB.by_id(tech_id)["name"], Color(0.7, 0.9, 1))
|
||||
return true
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Missionen
|
||||
|
||||
func accept_mission(human_id: String) -> bool:
|
||||
if completed.has(human_id):
|
||||
return false
|
||||
if active_mission == human_id:
|
||||
return false
|
||||
active_mission = human_id
|
||||
if not progress.has(human_id):
|
||||
progress[human_id] = 0.0
|
||||
var h := HumanDB.by_id(human_id)
|
||||
notified.emit("Mission angenommen: %s" % h["name"], Color(1, 0.9, 0.5))
|
||||
state_changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func mission_progress(human_id: String) -> float:
|
||||
return progress.get(human_id, 0.0)
|
||||
|
||||
|
||||
# ----------------------------------------------------- Technik auf Mensch anwenden
|
||||
|
||||
## Bewertet eine Technik in genau dieser Situation, ohne sie anzuwenden.
|
||||
func evaluate(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary:
|
||||
var t := TechDB.by_id(tech_id)
|
||||
var h := HumanDB.by_id(human_id)
|
||||
var skill := skill_of(tech_id)
|
||||
var res := {
|
||||
"chance": 0.0, "tag": false, "moment": false, "blocked": false,
|
||||
"grund": "", "skill": skill,
|
||||
}
|
||||
if activity == "schlaeft":
|
||||
res["blocked"] = true
|
||||
res["grund"] = "Sie oder er schläft. Techniken erreichen nur wache Menschen."
|
||||
return res
|
||||
if skill == 0:
|
||||
res["blocked"] = true
|
||||
res["grund"] = "Technik nicht gelernt."
|
||||
return res
|
||||
|
||||
for tag in t["tags"]:
|
||||
if h["tags"].has(tag):
|
||||
res["tag"] = true
|
||||
break
|
||||
res["moment"] = t["momente"].has(activity)
|
||||
|
||||
var c := 0.08 + 0.11 * float(skill)
|
||||
if res["tag"]:
|
||||
c += 0.25
|
||||
if res["moment"]:
|
||||
c += 0.22
|
||||
if resisting:
|
||||
c -= 0.30
|
||||
res["grund"] = "Der Mensch macht gerade dicht."
|
||||
res["chance"] = clampf(c, 0.05, 0.95)
|
||||
return res
|
||||
|
||||
|
||||
## Wendet die Technik wirklich an: würfelt, verteilt EP und Fortschritt.
|
||||
func apply_technique(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary:
|
||||
var ev := evaluate(tech_id, human_id, activity, resisting)
|
||||
if ev["blocked"]:
|
||||
return {"ok": false, "grund": ev["grund"]}
|
||||
|
||||
var roll := randf()
|
||||
var success: bool = roll < float(ev["chance"])
|
||||
var key := "%s:%s" % [human_id, tech_id]
|
||||
var wiederholung: int = used.get(key, 0)
|
||||
used[key] = wiederholung + 1
|
||||
|
||||
var gain := 0.0
|
||||
if success:
|
||||
var base := 8.0 + 2.0 * float(ev["skill"])
|
||||
if ev["tag"]:
|
||||
base += 5.0
|
||||
if ev["moment"]:
|
||||
base += 5.0
|
||||
gain = base / (1.0 + 0.6 * float(wiederholung))
|
||||
add_xp(tech_id, 2)
|
||||
else:
|
||||
add_xp(tech_id, 1)
|
||||
|
||||
var done := false
|
||||
if human_id == active_mission and gain > 0.0:
|
||||
var p: float = progress.get(human_id, 0.0) + gain
|
||||
progress[human_id] = minf(p, MISSION_ZIEL)
|
||||
if progress[human_id] >= MISSION_ZIEL and not completed.has(human_id):
|
||||
completed.append(human_id)
|
||||
active_mission = ""
|
||||
einsicht += 3
|
||||
done = true
|
||||
mission_done.emit(human_id)
|
||||
notified.emit("%s hat es geschafft. (+3 Einsicht)" % HumanDB.by_id(human_id)["name"],
|
||||
Color(1, 0.85, 0.4))
|
||||
state_changed.emit()
|
||||
|
||||
return {
|
||||
"ok": true, "success": success, "chance": ev["chance"], "gain": gain,
|
||||
"wiederholung": wiederholung, "mission_done": done,
|
||||
"tag": ev["tag"], "moment": ev["moment"],
|
||||
}
|
||||
1
scripts/game.gd.uid
Normal file
1
scripts/game.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://brkkwynipq0by
|
||||
147
scripts/human.gd
Normal file
147
scripts/human.gd
Normal file
@@ -0,0 +1,147 @@
|
||||
extends Node3D
|
||||
class_name Human
|
||||
## Ein Mensch auf der Erde. Läuft seinen Tagesablauf ab und kann von einer
|
||||
## Technik erreicht werden -- oder eben nicht.
|
||||
|
||||
const GEHTEMPO := 6.0
|
||||
|
||||
var data: Dictionary
|
||||
var idx: int = 0
|
||||
var place: String = ""
|
||||
var activity: String = "schlaeft"
|
||||
var resist_left: float = 0.0
|
||||
|
||||
var _visual: Node3D
|
||||
var _label: Label3D
|
||||
var _koerper: MeshInstance3D
|
||||
var _kopf: MeshInstance3D
|
||||
var _aura: MeshInstance3D
|
||||
var _strahl: MeshInstance3D
|
||||
var _path: Array = []
|
||||
var _bob: float = 0.0
|
||||
var _offset: Vector3
|
||||
|
||||
|
||||
func setup(d: Dictionary, index: int) -> void:
|
||||
data = d
|
||||
idx = index
|
||||
name = "Human_" + str(d["id"])
|
||||
var winkel := float(index) * TAU / 4.0
|
||||
_offset = Vector3(cos(winkel), 0, sin(winkel)) * 2.2
|
||||
|
||||
_visual = Node3D.new()
|
||||
add_child(_visual)
|
||||
var c: Color = d["farbe"]
|
||||
var hoehe := 1.05 + float(index % 3) * 0.08
|
||||
_koerper = Prims.capsule(_visual, Vector3(0, hoehe * 0.62, 0), 0.32, hoehe, c)
|
||||
_kopf = Prims.sphere(_visual, Vector3(0, hoehe * 1.18 + 0.1, 0), 0.24, c.lightened(0.45))
|
||||
Prims.box(_visual, Vector3(0, hoehe * 0.95, 0), Vector3(0.9, 0.14, 0.3), c.darkened(0.25), false)
|
||||
Prims.box(_visual, Vector3(0, 0.06, 0), Vector3(0.5, 0.12, 0.7), c.darkened(0.45), false)
|
||||
|
||||
_aura = Prims.cyl(self, Vector3(0, 0.05, 0), 0.95, 0.05, Color(1, 0.85, 0.35), false, 2.0)
|
||||
_aura.visible = false
|
||||
_strahl = Prims.cyl(self, Vector3(0, 11.0, 0), 0.14, 16.0, Color(1, 0.85, 0.35, 0.22), false, 1.2)
|
||||
_strahl.visible = false
|
||||
|
||||
_label = Prims.label(self, Vector3(0, 2.9, 0), d["name"], 0.30)
|
||||
|
||||
var start := _routine_at(Game.time_h)
|
||||
place = start[0]
|
||||
activity = start[1]
|
||||
global_position = HumanDB.place_center(place) + _offset
|
||||
|
||||
|
||||
func _routine_at(h: float) -> Array:
|
||||
var r: Array = data["routine"]
|
||||
var cur: Array = r[r.size() - 1]
|
||||
for eintrag in r:
|
||||
if h >= float(eintrag[0]):
|
||||
cur = eintrag
|
||||
return [cur[1], cur[2]]
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
resist_left = maxf(0.0, resist_left - delta)
|
||||
|
||||
var soll := _routine_at(Game.time_h)
|
||||
if soll[0] != place:
|
||||
var war_unterwegs := not _path.is_empty()
|
||||
_path = HumanDB.route(place, soll[0], _offset)
|
||||
if war_unterwegs and _path.size() > 1:
|
||||
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
|
||||
place = soll[0]
|
||||
activity = "unterwegs"
|
||||
elif _path.is_empty() and activity != soll[1]:
|
||||
activity = soll[1]
|
||||
|
||||
if not _path.is_empty():
|
||||
var ziel: Vector3 = _path[0]
|
||||
ziel.y = 0.0
|
||||
var hier := global_position
|
||||
hier.y = 0.0
|
||||
var d := ziel - hier
|
||||
if d.length() < 0.4:
|
||||
_path.remove_at(0)
|
||||
if _path.is_empty():
|
||||
activity = soll[1]
|
||||
else:
|
||||
var schritt := d.normalized() * GEHTEMPO * delta
|
||||
global_position += schritt
|
||||
_visual.rotation.y = lerp_angle(_visual.rotation.y, atan2(-d.x, -d.z), 1.0 - pow(0.002, delta))
|
||||
_bob += delta * 9.0
|
||||
_visual.position.y = absf(sin(_bob)) * 0.09
|
||||
|
||||
if _path.is_empty():
|
||||
_visual.position.y = lerpf(_visual.position.y, 0.0, 1.0 - pow(0.01, delta))
|
||||
|
||||
_visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0,
|
||||
1.0 - pow(0.01, delta))
|
||||
|
||||
var erledigt: bool = Game.completed.has(data["id"])
|
||||
var aktiv: bool = Game.active_mission == data["id"]
|
||||
_aura.visible = aktiv or erledigt
|
||||
_strahl.visible = aktiv
|
||||
if _aura.visible:
|
||||
_aura.material_override = Prims.mat(
|
||||
Color(0.4, 1.0, 0.5) if erledigt else Color(1, 0.85, 0.35), 2.0)
|
||||
_aura.scale.x = 1.0 + sin(Time.get_ticks_msec() * 0.003) * 0.08
|
||||
_aura.scale.z = _aura.scale.x
|
||||
|
||||
_label.text = beschriftung()
|
||||
_label.modulate = Color(0.55, 0.85, 0.6) if erledigt else (
|
||||
Color(1, 0.9, 0.6) if aktiv else Color(1, 1, 1))
|
||||
|
||||
|
||||
func beschriftung() -> String:
|
||||
var s: String = data["name"] + "\n" + HumanDB.activity_text(activity)
|
||||
if resist_left > 0.0:
|
||||
s += " (macht dicht)"
|
||||
if Game.completed.has(data["id"]):
|
||||
s += "\n★ Traum erfüllt"
|
||||
elif Game.active_mission == data["id"]:
|
||||
s += "\nTraum: %d %%" % int(Game.mission_progress(data["id"]))
|
||||
return s
|
||||
|
||||
|
||||
## Sichtbare Reaktion auf eine angewandte Technik.
|
||||
func reagiere(success: bool, text: String) -> void:
|
||||
var l := Prims.label(self, Vector3(0, 3.5, 0), text, 0.34,
|
||||
Color(0.55, 1.0, 0.6) if success else Color(1, 0.55, 0.45))
|
||||
var tw := l.create_tween()
|
||||
tw.set_parallel(true)
|
||||
tw.tween_property(l, "position:y", 5.2, 2.4)
|
||||
tw.tween_property(l, "modulate:a", 0.0, 2.4).set_delay(0.9)
|
||||
tw.chain().tween_callback(l.queue_free)
|
||||
if not success:
|
||||
resist_left = Game.RESIST_SEKUNDEN
|
||||
var blitz := Prims.mat(
|
||||
Color(0.6, 1.0, 0.7) if success else Color(1, 0.5, 0.4), 1.6)
|
||||
var orig := _koerper.material_override
|
||||
_koerper.material_override = blitz
|
||||
_kopf.material_override = blitz
|
||||
var t2 := create_tween()
|
||||
t2.tween_interval(0.35)
|
||||
t2.tween_callback(func() -> void:
|
||||
if is_instance_valid(_koerper):
|
||||
_koerper.material_override = orig
|
||||
_kopf.material_override = Prims.mat((data["farbe"] as Color).lightened(0.45)))
|
||||
1
scripts/human.gd.uid
Normal file
1
scripts/human.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://let5f4ntq0j8
|
||||
206
scripts/human_db.gd
Normal file
206
scripts/human_db.gd
Normal file
@@ -0,0 +1,206 @@
|
||||
class_name HumanDB
|
||||
## Orte der Kleinstadt und die Menschen mit ihren Tagesabläufen.
|
||||
|
||||
## Gebäude liegen im Ring um den Park. Alle Türen zeigen zur Mitte.
|
||||
## pos = Standplatz drinnen, size = Grundfläche, dir = Richtung der Türlücke.
|
||||
static var PLACES: Dictionary = {
|
||||
"uni": {
|
||||
"name": "Hochschule", "pos": [-28.0, -27.0], "size": [14.0, 10.0],
|
||||
"dir": "back", "color": Color(0.72, 0.70, 0.66),
|
||||
},
|
||||
"cafe": {
|
||||
"name": "Café Nadel", "pos": [0.0, -27.0], "size": [12.0, 10.0],
|
||||
"dir": "back", "color": Color(0.82, 0.62, 0.45),
|
||||
},
|
||||
"buero": {
|
||||
"name": "Büro Ostwind", "pos": [28.0, -27.0], "size": [14.0, 10.0],
|
||||
"dir": "back", "color": Color(0.62, 0.66, 0.72),
|
||||
},
|
||||
"baeckerei": {
|
||||
"name": "Bäckerei Keller", "pos": [-31.0, 0.0], "size": [10.0, 12.0],
|
||||
"dir": "right", "color": Color(0.85, 0.72, 0.50),
|
||||
},
|
||||
"haus_tobias": {
|
||||
"name": "Wohnung Frank", "pos": [31.0, 0.0], "size": [10.0, 12.0],
|
||||
"dir": "left", "color": Color(0.70, 0.58, 0.55),
|
||||
},
|
||||
"haus_lena": {
|
||||
"name": "Atelier Vogt", "pos": [-28.0, 27.0], "size": [12.0, 10.0],
|
||||
"dir": "forward", "color": Color(0.66, 0.58, 0.72),
|
||||
},
|
||||
"haus_mira": {
|
||||
"name": "Wohnheim", "pos": [0.0, 27.0], "size": [12.0, 10.0],
|
||||
"dir": "forward", "color": Color(0.60, 0.68, 0.62),
|
||||
},
|
||||
"haus_bruno": {
|
||||
"name": "Haus Keller", "pos": [28.0, 27.0], "size": [12.0, 10.0],
|
||||
"dir": "forward", "color": Color(0.78, 0.66, 0.58),
|
||||
},
|
||||
"park": {
|
||||
"name": "Stadtpark", "pos": [0.0, 4.0], "size": [0.0, 0.0],
|
||||
"dir": "none", "color": Color(0.45, 0.65, 0.38),
|
||||
},
|
||||
}
|
||||
|
||||
static var DIR_VEC: Dictionary = {
|
||||
"back": Vector3(0, 0, 1),
|
||||
"forward": Vector3(0, 0, -1),
|
||||
"left": Vector3(-1, 0, 0),
|
||||
"right": Vector3(1, 0, 0),
|
||||
"none": Vector3(0, 0, 0),
|
||||
}
|
||||
|
||||
static var ACTIVITY_TEXT: Dictionary = {
|
||||
"schlaeft": "schläft",
|
||||
"morgenroutine": "Morgenroutine",
|
||||
"arbeitet": "arbeitet",
|
||||
"pause": "macht Pause",
|
||||
"essen": "isst",
|
||||
"gruebelt": "grübelt",
|
||||
"ruht": "ruht sich aus",
|
||||
"angespannt": "ist angespannt",
|
||||
"abendroutine": "Abendroutine",
|
||||
"aufraeumen": "räumt auf",
|
||||
"wartet": "wartet",
|
||||
"muede": "ist müde",
|
||||
"unterwegs": "geht spazieren",
|
||||
"zoegert": "zögert",
|
||||
}
|
||||
|
||||
static var LIST: Array = [
|
||||
{
|
||||
"id": "lena",
|
||||
"name": "Lena Vogt",
|
||||
"alter": 29,
|
||||
"beruf": "Illustratorin",
|
||||
"traum": "Ihr Bilderbuch endlich zu Ende zeichnen.",
|
||||
"problem": "Sie schiebt die letzten zwölf Seiten seit acht Monaten vor sich her. Jeder Entwurf kommt ihr zu schlecht vor, also fängt sie lieber gar nicht erst an.",
|
||||
"tags": ["prokrastination", "selbstzweifel", "ablenkung"],
|
||||
"farbe": Color(0.85, 0.45, 0.55),
|
||||
"routine": [
|
||||
[0, "haus_lena", "schlaeft"],
|
||||
[7, "haus_lena", "morgenroutine"],
|
||||
[8, "cafe", "essen"],
|
||||
[10, "haus_lena", "zoegert"],
|
||||
[12, "haus_lena", "arbeitet"],
|
||||
[13, "park", "pause"],
|
||||
[15, "haus_lena", "arbeitet"],
|
||||
[18, "park", "unterwegs"],
|
||||
[20, "haus_lena", "aufraeumen"],
|
||||
[21, "haus_lena", "gruebelt"],
|
||||
[22, "haus_lena", "abendroutine"],
|
||||
[23, "haus_lena", "schlaeft"],
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "bruno",
|
||||
"name": "Bruno Keller",
|
||||
"alter": 54,
|
||||
"beruf": "Bäcker",
|
||||
"traum": "Wieder Klavier spielen wie mit zwanzig.",
|
||||
"problem": "Das Klavier steht zugestellt im Flur. Nach der Frühschicht ist er zu müde, um auch nur den Deckel zu öffnen.",
|
||||
"tags": ["gewohnheit", "erschoepfung", "ablenkung"],
|
||||
"farbe": Color(0.55, 0.45, 0.30),
|
||||
"routine": [
|
||||
[0, "haus_bruno", "schlaeft"],
|
||||
[3, "baeckerei", "arbeitet"],
|
||||
[10, "cafe", "pause"],
|
||||
[12, "haus_bruno", "essen"],
|
||||
[13, "haus_bruno", "ruht"],
|
||||
[15, "haus_bruno", "zoegert"],
|
||||
[16, "park", "unterwegs"],
|
||||
[18, "haus_bruno", "muede"],
|
||||
[20, "haus_bruno", "abendroutine"],
|
||||
[21, "haus_bruno", "schlaeft"],
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "mira",
|
||||
"name": "Mira Sahin",
|
||||
"alter": 22,
|
||||
"beruf": "Studentin",
|
||||
"traum": "Ihre Abschlussarbeit vortragen, ohne zu erstarren.",
|
||||
"problem": "Die Arbeit ist längst fertig. Aber vor jedem Vortrag wird ihr schwarz vor Augen, und sie meldet sich wieder ab.",
|
||||
"tags": ["angst", "ueberforderung", "selbstzweifel"],
|
||||
"farbe": Color(0.35, 0.60, 0.85),
|
||||
"routine": [
|
||||
[0, "haus_mira", "schlaeft"],
|
||||
[8, "haus_mira", "morgenroutine"],
|
||||
[9, "uni", "arbeitet"],
|
||||
[13, "cafe", "angespannt"],
|
||||
[15, "uni", "arbeitet"],
|
||||
[17, "uni", "wartet"],
|
||||
[18, "haus_mira", "gruebelt"],
|
||||
[20, "park", "unterwegs"],
|
||||
[21, "haus_mira", "ruht"],
|
||||
[22, "haus_mira", "abendroutine"],
|
||||
[23, "haus_mira", "schlaeft"],
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tobias",
|
||||
"name": "Tobias Frank",
|
||||
"alter": 41,
|
||||
"beruf": "Sachbearbeiter",
|
||||
"traum": "Abends wieder Zeit für seine Tochter haben.",
|
||||
"problem": "Er sagt zu jeder Bitte ja und sitzt dafür jeden Abend bis acht im Büro. Zu Hause ist er nur noch müde.",
|
||||
"tags": ["stress", "grenzen", "erschoepfung"],
|
||||
"farbe": Color(0.40, 0.55, 0.45),
|
||||
"routine": [
|
||||
[0, "haus_tobias", "schlaeft"],
|
||||
[6, "haus_tobias", "morgenroutine"],
|
||||
[7, "buero", "arbeitet"],
|
||||
[12, "cafe", "essen"],
|
||||
[14, "buero", "arbeitet"],
|
||||
[19, "buero", "angespannt"],
|
||||
[20, "haus_tobias", "essen"],
|
||||
[21, "haus_tobias", "muede"],
|
||||
[23, "haus_tobias", "abendroutine"],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
static func by_id(id: String) -> Dictionary:
|
||||
for h in LIST:
|
||||
if h["id"] == id:
|
||||
return h
|
||||
return {}
|
||||
|
||||
|
||||
static func place_center(key: String) -> Vector3:
|
||||
var p: Array = PLACES[key]["pos"]
|
||||
return Vector3(p[0], 0.0, p[1])
|
||||
|
||||
|
||||
## Punkt direkt vor der Türlücke, außerhalb des Gebäudes.
|
||||
static func place_door(key: String) -> Vector3:
|
||||
var pl: Dictionary = PLACES[key]
|
||||
var c := place_center(key)
|
||||
var d: Vector3 = DIR_VEC[pl["dir"]]
|
||||
if d == Vector3.ZERO:
|
||||
return c
|
||||
var size: Array = pl["size"]
|
||||
var half: float = size[1] * 0.5 if absf(d.z) > 0.0 else size[0] * 0.5
|
||||
return c + d * (half + 2.5)
|
||||
|
||||
|
||||
## Route von Ort a nach Ort b: aus der Tür, über den Platz, zur neuen Tür, hinein.
|
||||
static func route(a: String, b: String, offset: Vector3) -> Array:
|
||||
var pts: Array = []
|
||||
if a == b:
|
||||
return pts
|
||||
var da := place_door(a)
|
||||
var db := place_door(b)
|
||||
pts.append(da)
|
||||
if da.length() > 17.0:
|
||||
pts.append(da.normalized() * 15.0)
|
||||
if db.length() > 17.0:
|
||||
pts.append(db.normalized() * 15.0)
|
||||
pts.append(db)
|
||||
pts.append(place_center(b) + offset)
|
||||
return pts
|
||||
|
||||
|
||||
static func activity_text(a: String) -> String:
|
||||
return ACTIVITY_TEXT.get(a, a)
|
||||
1
scripts/human_db.gd.uid
Normal file
1
scripts/human_db.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://f44so5e502ga
|
||||
169
scripts/main.gd
Normal file
169
scripts/main.gd
Normal file
@@ -0,0 +1,169 @@
|
||||
extends Node3D
|
||||
## Wurzelknoten: hält Spieler und Oberfläche, wechselt zwischen den Welten
|
||||
## und wickelt Interaktionen ab.
|
||||
|
||||
const REICHWEITE_OBJEKT := 4.2
|
||||
const REICHWEITE_MENSCH := 3.6
|
||||
|
||||
var player: Player
|
||||
var ui: UI
|
||||
var welt: Node3D
|
||||
var auf_erde := false
|
||||
var _fokus: Node3D = null
|
||||
var _fokus_mensch: Node3D = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
ui = UI.new()
|
||||
add_child(ui)
|
||||
ui.panel_opened.connect(func() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
player.look_enabled = false)
|
||||
ui.panel_closed.connect(func() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
player.look_enabled = true)
|
||||
|
||||
player = Player.new()
|
||||
add_child(player)
|
||||
|
||||
Game.mission_done.connect(func(_id: String) -> void:
|
||||
Game.notified.emit("Kehre zur Traumwelt zurück — die Säule im Park bringt dich hin.",
|
||||
Color(0.8, 0.95, 1)))
|
||||
|
||||
_traumwelt()
|
||||
ui.intro_panel(func() -> void: pass)
|
||||
|
||||
|
||||
func _traumwelt() -> void:
|
||||
auf_erde = false
|
||||
_setze_welt(DreamWorld.new())
|
||||
player.set_mode(Player.Mode.TRAUM)
|
||||
player.global_position = (welt as DreamWorld).spawn
|
||||
player.yaw = 0.0
|
||||
player.pitch = -0.18
|
||||
|
||||
|
||||
func _erde() -> void:
|
||||
auf_erde = true
|
||||
_setze_welt(EarthWorld.new())
|
||||
player.set_mode(Player.Mode.GEIST)
|
||||
player.global_position = (welt as EarthWorld).spawn
|
||||
player.yaw = 0.0
|
||||
player.pitch = -0.42
|
||||
|
||||
|
||||
func _setze_welt(neu: Node3D) -> void:
|
||||
if welt != null:
|
||||
remove_child(welt)
|
||||
welt.queue_free()
|
||||
welt = neu
|
||||
add_child(welt)
|
||||
move_child(welt, 0)
|
||||
Game.teleport_lock = 1.0
|
||||
_fokus = null
|
||||
_fokus_mensch = null
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
Game.advance_time(delta)
|
||||
Game.teleport_lock = maxf(0.0, Game.teleport_lock - delta)
|
||||
if ui.offen:
|
||||
ui.setze_hinweis("")
|
||||
return
|
||||
_fokus_suchen()
|
||||
_hinweis_setzen()
|
||||
|
||||
|
||||
func _fokus_suchen() -> void:
|
||||
_fokus = null
|
||||
_fokus_mensch = null
|
||||
var p := player.global_position
|
||||
var best := REICHWEITE_OBJEKT
|
||||
for n in get_tree().get_nodes_in_group("interactable"):
|
||||
var d: float = p.distance_to((n as Node3D).global_position)
|
||||
if d < best:
|
||||
best = d
|
||||
_fokus = n
|
||||
if auf_erde:
|
||||
var bestm := REICHWEITE_MENSCH
|
||||
for m in (welt as EarthWorld).humans:
|
||||
var d: float = p.distance_to((m as Node3D).global_position + Vector3(0, 1.0, 0))
|
||||
if d < bestm:
|
||||
bestm = d
|
||||
_fokus_mensch = m
|
||||
if _fokus_mensch != null and (_fokus == null or bestm < best):
|
||||
_fokus = null
|
||||
elif _fokus != null:
|
||||
_fokus_mensch = null
|
||||
|
||||
|
||||
func _hinweis_setzen() -> void:
|
||||
if _fokus_mensch != null:
|
||||
ui.setze_hinweis("[E] %s ansprechen — %s" %
|
||||
[_fokus_mensch.data["name"], HumanDB.activity_text(_fokus_mensch.activity)])
|
||||
elif _fokus != null:
|
||||
var kind: String = _fokus.get_meta("kind")
|
||||
var titel: String = _fokus.get_meta("titel")
|
||||
match kind:
|
||||
"technik":
|
||||
ui.setze_hinweis("[E] %s%s" % [titel,
|
||||
"" if not Game.knows(_fokus.get_meta("arg")) else " (gelernt)"])
|
||||
"mission":
|
||||
ui.setze_hinweis("[E] %s — Mission ansehen" % titel)
|
||||
"vertiefen":
|
||||
ui.setze_hinweis("[E] Meditieren")
|
||||
"erde":
|
||||
ui.setze_hinweis("[E] Zur Erde reisen")
|
||||
"traum":
|
||||
ui.setze_hinweis("[E] Zurück in die Traumwelt")
|
||||
_:
|
||||
ui.setze_hinweis("[E] %s" % titel)
|
||||
else:
|
||||
ui.setze_hinweis("")
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel") and not ui.offen:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if \
|
||||
Input.mouse_mode == Input.MOUSE_MODE_CAPTURED else Input.MOUSE_MODE_CAPTURED
|
||||
player.look_enabled = Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if ui.offen:
|
||||
return
|
||||
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_VISIBLE:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
player.look_enabled = true
|
||||
return
|
||||
if event.is_action_pressed("kodex"):
|
||||
ui.kodex_panel()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.is_action_pressed("interagieren"):
|
||||
_interagieren()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _interagieren() -> void:
|
||||
if _fokus_mensch != null:
|
||||
ui.anwenden_panel(_fokus_mensch)
|
||||
return
|
||||
if _fokus == null:
|
||||
return
|
||||
var kind: String = _fokus.get_meta("kind")
|
||||
var arg: String = _fokus.get_meta("arg")
|
||||
match kind:
|
||||
"technik":
|
||||
ui.technik_panel(arg)
|
||||
"mission":
|
||||
ui.mission_panel(arg)
|
||||
"vertiefen":
|
||||
ui.vertiefen_panel()
|
||||
"erde":
|
||||
if Game.active_mission == "" and Game.completed.size() < HumanDB.LIST.size():
|
||||
Game.notified.emit(
|
||||
"Das Tor öffnet sich nur mit einer angenommenen Mission.", Color(1, 0.8, 0.5))
|
||||
else:
|
||||
_erde()
|
||||
"traum":
|
||||
_traumwelt()
|
||||
1
scripts/main.gd.uid
Normal file
1
scripts/main.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vos2w6psagbu
|
||||
150
scripts/player.gd
Normal file
150
scripts/player.gd
Normal file
@@ -0,0 +1,150 @@
|
||||
extends CharacterBody3D
|
||||
class_name Player
|
||||
## Ein Wesen aus der Traumwelt. In der Traumwelt läuft es, auf der Erde schwebt
|
||||
## es als unsichtbarer Geist durch Wände.
|
||||
|
||||
enum Mode { TRAUM, GEIST }
|
||||
|
||||
const SPEED_TRAUM := 8.0
|
||||
const SPEED_GEIST := 11.0
|
||||
const GRAVITY := 24.0
|
||||
const JUMP := 9.0
|
||||
const MAUS := 0.0026
|
||||
|
||||
var mode: int = Mode.TRAUM
|
||||
var yaw := 0.0
|
||||
var pitch := -0.25
|
||||
var look_enabled := true
|
||||
|
||||
var pivot: Node3D
|
||||
var arm: SpringArm3D
|
||||
var cam: Camera3D
|
||||
var body: Node3D
|
||||
var _kern: MeshInstance3D
|
||||
var _ringe: Array = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var cs := CollisionShape3D.new()
|
||||
var shape := CapsuleShape3D.new()
|
||||
shape.radius = 0.45
|
||||
shape.height = 1.9
|
||||
cs.shape = shape
|
||||
cs.position.y = 0.95
|
||||
add_child(cs)
|
||||
|
||||
body = Node3D.new()
|
||||
add_child(body)
|
||||
_kern = Prims.capsule(body, Vector3(0, 1.0, 0), 0.42, 1.6, Color(0.55, 0.85, 1.0))
|
||||
_kern.material_override = Prims.mat(Color(0.55, 0.85, 1.0), 0.9)
|
||||
var kopf := Prims.sphere(body, Vector3(0, 2.0, 0), 0.28, Color(0.80, 0.92, 1.0), 0.6)
|
||||
kopf.name = "Kopf"
|
||||
for i in range(2):
|
||||
var r := Prims.cyl(body, Vector3(0, 0.5 + i * 0.9, 0), 0.75 + i * 0.15, 0.06,
|
||||
Color(0.6, 0.9, 1.0), false, 1.6)
|
||||
r.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
_ringe.append(r)
|
||||
# kleiner Blick nach vorn, damit man die Laufrichtung sieht
|
||||
Prims.box(body, Vector3(0, 1.55, -0.36), Vector3(0.5, 0.12, 0.12), Color(1, 1, 1), false, 2.0)
|
||||
|
||||
pivot = Node3D.new()
|
||||
pivot.position.y = 1.5
|
||||
add_child(pivot)
|
||||
arm = SpringArm3D.new()
|
||||
arm.position.x = 0.75
|
||||
arm.spring_length = 6.5
|
||||
arm.margin = 0.3
|
||||
pivot.add_child(arm)
|
||||
cam = Camera3D.new()
|
||||
cam.fov = 74.0
|
||||
arm.add_child(cam)
|
||||
set_mode(Mode.TRAUM)
|
||||
|
||||
|
||||
func set_mode(m: int) -> void:
|
||||
mode = m
|
||||
if m == Mode.GEIST:
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
arm.collision_mask = 0
|
||||
arm.spring_length = 5.0
|
||||
pivot.position.y = 2.1
|
||||
body.scale = Vector3.ONE * 0.72
|
||||
_kern.material_override = Prims.mat(Color(0.65, 0.9, 1.0, 0.26), 0.5)
|
||||
for r in _ringe:
|
||||
r.material_override = Prims.mat(Color(0.7, 0.95, 1.0, 0.20), 0.8)
|
||||
r.scale = Vector3(0.55, 1.0, 0.55)
|
||||
else:
|
||||
collision_layer = 1
|
||||
collision_mask = 1
|
||||
arm.collision_mask = 1
|
||||
arm.spring_length = 6.5
|
||||
pivot.position.y = 1.5
|
||||
body.scale = Vector3.ONE
|
||||
_kern.material_override = Prims.mat(Color(0.55, 0.85, 1.0), 0.9)
|
||||
for r in _ringe:
|
||||
r.material_override = Prims.mat(Color(0.6, 0.9, 1.0), 1.6)
|
||||
r.scale = Vector3.ONE
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not look_enabled:
|
||||
return
|
||||
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
yaw -= event.relative.x * MAUS
|
||||
pitch = clampf(pitch - event.relative.y * MAUS, -1.3, 0.6)
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
pivot.rotation.y = yaw
|
||||
arm.rotation.x = pitch
|
||||
|
||||
for i in range(_ringe.size()):
|
||||
var r: MeshInstance3D = _ringe[i]
|
||||
r.rotation.y += delta * (0.8 + 0.5 * i)
|
||||
r.position.y = 0.5 + i * 0.9 + sin(Time.get_ticks_msec() * 0.001 * (1.2 + i * 0.4)) * 0.12
|
||||
|
||||
var input := Vector2.ZERO
|
||||
if look_enabled:
|
||||
input.x = Input.get_axis("links", "rechts")
|
||||
input.y = Input.get_axis("vor", "zurueck")
|
||||
var dir := Vector3.ZERO
|
||||
if input.length() > 0.01:
|
||||
var basis_yaw := Basis(Vector3.UP, yaw)
|
||||
dir = (basis_yaw * Vector3(input.x, 0, input.y)).normalized()
|
||||
|
||||
if mode == Mode.GEIST:
|
||||
var v := dir * SPEED_GEIST
|
||||
if look_enabled:
|
||||
if Input.is_action_pressed("steigen"):
|
||||
v.y += SPEED_GEIST * 0.75
|
||||
if Input.is_action_pressed("sinken"):
|
||||
v.y -= SPEED_GEIST * 0.75
|
||||
velocity = velocity.lerp(v, 1.0 - pow(0.001, delta))
|
||||
move_and_slide()
|
||||
if global_position.y < 0.4:
|
||||
global_position.y = 0.4
|
||||
if global_position.y > 45.0:
|
||||
global_position.y = 45.0
|
||||
else:
|
||||
velocity.x = lerpf(velocity.x, dir.x * SPEED_TRAUM, 1.0 - pow(0.0005, delta))
|
||||
velocity.z = lerpf(velocity.z, dir.z * SPEED_TRAUM, 1.0 - pow(0.0005, delta))
|
||||
if is_on_floor():
|
||||
if look_enabled and Input.is_action_pressed("steigen"):
|
||||
velocity.y = JUMP
|
||||
else:
|
||||
velocity.y = -1.0
|
||||
else:
|
||||
velocity.y -= GRAVITY * delta
|
||||
move_and_slide()
|
||||
if global_position.y < -400.0:
|
||||
global_position.y = 2.0
|
||||
|
||||
if dir.length() > 0.01:
|
||||
var ziel := atan2(-dir.x, -dir.z)
|
||||
body.rotation.y = lerp_angle(body.rotation.y, ziel, 1.0 - pow(0.0005, delta))
|
||||
|
||||
|
||||
func blickrichtung() -> Vector3:
|
||||
return -Vector3(sin(yaw), 0, cos(yaw))
|
||||
1
scripts/player.gd.uid
Normal file
1
scripts/player.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dwka176k6h36q
|
||||
134
scripts/prims.gd
Normal file
134
scripts/prims.gd
Normal file
@@ -0,0 +1,134 @@
|
||||
class_name Prims
|
||||
## Kleine Helfer, um die Welt aus Godot-Primitiven zusammenzusetzen.
|
||||
|
||||
static func mat(c: Color, emis: float = 0.0, rough: float = 0.85) -> StandardMaterial3D:
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = c
|
||||
m.roughness = rough
|
||||
m.metallic = 0.0
|
||||
if emis > 0.0:
|
||||
m.emission_enabled = true
|
||||
m.emission = c
|
||||
m.emission_energy_multiplier = emis
|
||||
if c.a < 1.0:
|
||||
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
return m
|
||||
|
||||
|
||||
static func box(parent: Node, pos: Vector3, size: Vector3, c: Color,
|
||||
collide: bool = true, emis: float = 0.0) -> MeshInstance3D:
|
||||
var mi := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = size
|
||||
mi.mesh = bm
|
||||
mi.material_override = mat(c, emis)
|
||||
mi.position = pos
|
||||
parent.add_child(mi)
|
||||
if collide:
|
||||
_add_box_body(mi, size)
|
||||
return mi
|
||||
|
||||
|
||||
static func _add_box_body(mi: MeshInstance3D, size: Vector3) -> void:
|
||||
var sb := StaticBody3D.new()
|
||||
var cs := CollisionShape3D.new()
|
||||
var sh := BoxShape3D.new()
|
||||
sh.size = size
|
||||
cs.shape = sh
|
||||
sb.add_child(cs)
|
||||
mi.add_child(sb)
|
||||
|
||||
|
||||
static func cyl(parent: Node, pos: Vector3, radius: float, height: float, c: Color,
|
||||
collide: bool = true, emis: float = 0.0) -> MeshInstance3D:
|
||||
var mi := MeshInstance3D.new()
|
||||
var cm := CylinderMesh.new()
|
||||
cm.top_radius = radius
|
||||
cm.bottom_radius = radius
|
||||
cm.height = height
|
||||
mi.mesh = cm
|
||||
mi.material_override = mat(c, emis)
|
||||
mi.position = pos
|
||||
parent.add_child(mi)
|
||||
if collide:
|
||||
var sb := StaticBody3D.new()
|
||||
var cs := CollisionShape3D.new()
|
||||
var sh := CylinderShape3D.new()
|
||||
sh.radius = radius
|
||||
sh.height = height
|
||||
cs.shape = sh
|
||||
sb.add_child(cs)
|
||||
mi.add_child(sb)
|
||||
return mi
|
||||
|
||||
|
||||
static func sphere(parent: Node, pos: Vector3, radius: float, c: Color,
|
||||
emis: float = 0.0) -> MeshInstance3D:
|
||||
var mi := MeshInstance3D.new()
|
||||
var sm := SphereMesh.new()
|
||||
sm.radius = radius
|
||||
sm.height = radius * 2.0
|
||||
mi.mesh = sm
|
||||
mi.material_override = mat(c, emis)
|
||||
mi.position = pos
|
||||
parent.add_child(mi)
|
||||
return mi
|
||||
|
||||
|
||||
static func capsule(parent: Node, pos: Vector3, radius: float, height: float, c: Color) -> MeshInstance3D:
|
||||
var mi := MeshInstance3D.new()
|
||||
var cm := CapsuleMesh.new()
|
||||
cm.radius = radius
|
||||
cm.height = height
|
||||
mi.mesh = cm
|
||||
mi.material_override = mat(c)
|
||||
mi.position = pos
|
||||
parent.add_child(mi)
|
||||
return mi
|
||||
|
||||
|
||||
static func label(parent: Node, pos: Vector3, text: String, size: float = 0.35,
|
||||
c: Color = Color.WHITE) -> Label3D:
|
||||
var l := Label3D.new()
|
||||
l.text = text
|
||||
l.font_size = 96
|
||||
l.pixel_size = size / 96.0
|
||||
l.position = pos
|
||||
l.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
l.no_depth_test = false
|
||||
l.modulate = c
|
||||
l.outline_size = 24
|
||||
l.outline_modulate = Color(0, 0, 0, 0.85)
|
||||
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
parent.add_child(l)
|
||||
return l
|
||||
|
||||
|
||||
## Ein rechteckiger Raum aus vier Wänden mit einer Türlücke.
|
||||
## door_dir: Vector3.FORWARD/BACK/LEFT/RIGHT -- auf welcher Seite die Lücke sitzt.
|
||||
static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color,
|
||||
floor_c: Color, door_dir: Vector3, door_w: float = 3.0,
|
||||
with_floor: bool = true) -> void:
|
||||
var t := 0.4
|
||||
var h := size.y
|
||||
if with_floor:
|
||||
box(parent, center + Vector3(0, 0.04, 0), Vector3(size.x, 0.2, size.z), floor_c)
|
||||
var seiten: Array[Vector3] = [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]
|
||||
for d in seiten:
|
||||
var quer := absf(d.z) > 0.0 # Wand liegt entlang der X-Achse
|
||||
var along: Vector3 = Vector3(1, 0, 0) if quer else Vector3(0, 0, 1)
|
||||
var len_along: float = size.x if quer else size.z
|
||||
var off: Vector3 = Vector3(d.x * (size.x * 0.5 - t * 0.5), h * 0.5,
|
||||
d.z * (size.z * 0.5 - t * 0.5))
|
||||
var voll: Vector3 = Vector3(size.x, h, t) if quer else Vector3(t, h, size.z)
|
||||
if d.is_equal_approx(door_dir) and door_w > 0.0 and door_w < len_along:
|
||||
var seg := (len_along - door_w) * 0.5
|
||||
var seg_size: Vector3 = Vector3(seg, h, t) if quer else Vector3(t, h, seg)
|
||||
for s: float in [-1.0, 1.0]:
|
||||
var seg_pos: Vector3 = center + off + along * (s * (door_w * 0.5 + seg * 0.5))
|
||||
box(parent, seg_pos, seg_size, wall_c)
|
||||
if h >= 3.0:
|
||||
var sturz: Vector3 = Vector3(door_w, h * 0.3, t) if quer else Vector3(t, h * 0.3, door_w)
|
||||
box(parent, center + off + Vector3(0, h * 0.35, 0), sturz, wall_c)
|
||||
else:
|
||||
box(parent, center + off, voll, wall_c)
|
||||
1
scripts/prims.gd.uid
Normal file
1
scripts/prims.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://eujyehasisce
|
||||
172
scripts/technique_db.gd
Normal file
172
scripts/technique_db.gd
Normal file
@@ -0,0 +1,172 @@
|
||||
class_name TechDB
|
||||
## Techniken sind echte, in der Praxis erprobte Lösungsansätze.
|
||||
## Jede trägt drei Hinweise: was sie tut, wann man sie anwendet, warum sie hilft.
|
||||
|
||||
static var LIST: Array = [
|
||||
{
|
||||
"id": "pomodoro",
|
||||
"name": "Pomodoro-Technik",
|
||||
"was": "25 Minuten am Stück an einer Sache arbeiten, dann 5 Minuten Pause. Nach vier Runden eine lange Pause.",
|
||||
"wann": "Wenn eine Aufgabe zu groß wirkt und der Anfang immer wieder verschoben wird.",
|
||||
"warum": "Ein klar begrenzter Zeitraum senkt die Einstiegshürde. Der Kopf muss nur 25 Minuten durchhalten, nicht den ganzen Tag.",
|
||||
"tags": ["prokrastination", "fokus"],
|
||||
"momente": ["arbeitet", "zoegert"],
|
||||
"farbe": Color(0.95, 0.45, 0.30),
|
||||
},
|
||||
{
|
||||
"id": "zwei_minuten",
|
||||
"name": "Zwei-Minuten-Regel",
|
||||
"was": "Was unter zwei Minuten dauert, sofort erledigen. Große Vorhaben auf einen Zwei-Minuten-Anfang schrumpfen.",
|
||||
"wann": "Bei einem Berg von Kleinkram oder wenn schon der erste Schritt blockiert.",
|
||||
"warum": "Der Start kostet die meiste Energie. Zwei Minuten sind zu klein zum Ablehnen und ziehen meist die ganze Aufgabe nach sich.",
|
||||
"tags": ["prokrastination", "ueberforderung"],
|
||||
"momente": ["zoegert", "aufraeumen", "arbeitet"],
|
||||
"farbe": Color(0.98, 0.72, 0.25),
|
||||
},
|
||||
{
|
||||
"id": "wenn_dann",
|
||||
"name": "Wenn-Dann-Plan",
|
||||
"was": "Den Vorsatz an einen konkreten Auslöser binden: \"Wenn ich X tue, dann mache ich Y.\"",
|
||||
"wann": "Wenn ein Vorhaben im Alltag jedes Mal untergeht.",
|
||||
"warum": "Der Plan hängt an einer Situation statt an Willenskraft. Der Auslöser erinnert von allein.",
|
||||
"tags": ["gewohnheit", "prokrastination"],
|
||||
"momente": ["morgenroutine", "pause"],
|
||||
"farbe": Color(0.45, 0.80, 0.55),
|
||||
},
|
||||
{
|
||||
"id": "habit_stack",
|
||||
"name": "Gewohnheiten stapeln",
|
||||
"was": "Eine neue, kleine Gewohnheit direkt an eine bestehende anhängen.",
|
||||
"wann": "Wenn eine neue Routine nie einen Platz im Tag findet.",
|
||||
"warum": "Die alte Gewohnheit ist der Anker. Es muss kein neuer Zeitpunkt erfunden werden.",
|
||||
"tags": ["gewohnheit"],
|
||||
"momente": ["morgenroutine", "essen", "abendroutine"],
|
||||
"farbe": Color(0.40, 0.70, 0.85),
|
||||
},
|
||||
{
|
||||
"id": "zerlegen",
|
||||
"name": "Aufgabe zerlegen",
|
||||
"was": "Ein großes Ziel in benennbare Einzelschritte teilen, bis der nächste Schritt trivial ist.",
|
||||
"wann": "Wenn jemand vor einem Vorhaben erstarrt und nicht weiß, wo er anfangen soll.",
|
||||
"warum": "Unklarheit lähmt stärker als Aufwand. Ein sichtbarer nächster Schritt macht Handeln wieder möglich.",
|
||||
"tags": ["ueberforderung", "prokrastination", "ziel"],
|
||||
"momente": ["gruebelt", "arbeitet"],
|
||||
"farbe": Color(0.75, 0.55, 0.95),
|
||||
},
|
||||
{
|
||||
"id": "umgebung",
|
||||
"name": "Umgebung gestalten",
|
||||
"was": "Das gewünschte Verhalten leichter machen, das störende schwerer. Handy in einen anderen Raum, Instrument sichtbar aufstellen.",
|
||||
"wann": "Wenn gute Vorsätze regelmäßig an Ablenkung scheitern.",
|
||||
"warum": "Verhalten folgt dem geringsten Widerstand. Die Umgebung wirkt zuverlässiger als Disziplin.",
|
||||
"tags": ["ablenkung", "gewohnheit"],
|
||||
"momente": ["aufraeumen", "arbeitet", "ruht"],
|
||||
"farbe": Color(0.55, 0.85, 0.40),
|
||||
},
|
||||
{
|
||||
"id": "reframing",
|
||||
"name": "Gedanken umdeuten",
|
||||
"was": "Einen belastenden Gedanken prüfen und durch eine ebenso wahre, aber tragfähigere Sicht ersetzen.",
|
||||
"wann": "Wenn jemand in einem Gedanken feststeckt, der ihn klein hält.",
|
||||
"warum": "Nicht das Ereignis belastet, sondern seine Deutung. Eine andere Deutung verändert Gefühl und Handlung.",
|
||||
"tags": ["selbstzweifel", "angst"],
|
||||
"momente": ["gruebelt", "ruht", "angespannt"],
|
||||
"farbe": Color(0.95, 0.55, 0.75),
|
||||
},
|
||||
{
|
||||
"id": "box_atmung",
|
||||
"name": "Box-Atmung",
|
||||
"was": "Vier Sekunden einatmen, vier halten, vier ausatmen, vier halten. Mehrere Runden lang.",
|
||||
"wann": "Bei akuter Anspannung, kurz vor einem gefürchteten Gespräch oder Auftritt.",
|
||||
"warum": "Langes Ausatmen bremst den Alarmzustand des Körpers. Der Puls sinkt, der Kopf wird wieder klar.",
|
||||
"tags": ["angst", "stress"],
|
||||
"momente": ["angespannt", "wartet", "pause"],
|
||||
"farbe": Color(0.40, 0.85, 0.95),
|
||||
},
|
||||
{
|
||||
"id": "selbstmitgefuehl",
|
||||
"name": "Selbstmitgefühl",
|
||||
"was": "Mit sich selbst so sprechen wie mit einem guten Freund in derselben Lage.",
|
||||
"wann": "Nach einem Fehler oder Rückschlag, wenn die innere Kritik lauter wird als der Vorsatz.",
|
||||
"warum": "Harte Selbstkritik erhöht die Vermeidung. Freundlichkeit hält handlungsfähig und senkt die Abbruchquote.",
|
||||
"tags": ["selbstzweifel", "rueckschlag"],
|
||||
"momente": ["gruebelt", "ruht", "angespannt"],
|
||||
"farbe": Color(1.0, 0.80, 0.60),
|
||||
},
|
||||
{
|
||||
"id": "schlaffenster",
|
||||
"name": "Schlaffenster festlegen",
|
||||
"was": "Feste Aufsteh- und Schlafenszeit, die letzte Stunde ohne Bildschirm, Zimmer dunkel und kühl.",
|
||||
"wann": "Bei Dauermüdigkeit, Reizbarkeit und wegbrechender Konzentration.",
|
||||
"warum": "Der innere Rhythmus braucht feste Anker. Erholter Schlaf ist die Grundlage für jede andere Veränderung.",
|
||||
"tags": ["erschoepfung", "gesundheit"],
|
||||
"momente": ["abendroutine", "muede", "ruht"],
|
||||
"farbe": Color(0.45, 0.50, 0.90),
|
||||
},
|
||||
{
|
||||
"id": "hilfe_bitten",
|
||||
"name": "Konkret um Hilfe bitten",
|
||||
"was": "Eine klare, kleine Bitte an eine konkrete Person richten, statt allgemein zu hoffen.",
|
||||
"wann": "Wenn jemand allein an etwas hängen bleibt, das andere leicht lösen könnten.",
|
||||
"warum": "Vage Bitten werden überhört. Menschen helfen gern, wenn sie genau wissen, was zu tun ist.",
|
||||
"tags": ["einsamkeit", "ueberforderung"],
|
||||
"momente": ["wartet", "pause", "unterwegs"],
|
||||
"farbe": Color(0.90, 0.90, 0.45),
|
||||
},
|
||||
{
|
||||
"id": "nein_sagen",
|
||||
"name": "Nein sagen mit Alternative",
|
||||
"was": "Absagen und zugleich benennen, was ginge: \"Heute nicht, am Donnerstag gern.\"",
|
||||
"wann": "Wenn der eigene Kalender von fremden Anliegen aufgebraucht wird.",
|
||||
"warum": "Ein nacktes Nein fühlt sich riskant an. Mit Alternative bleibt die Beziehung heil und die Zeit geschützt.",
|
||||
"tags": ["stress", "grenzen"],
|
||||
"momente": ["arbeitet", "angespannt", "unterwegs"],
|
||||
"farbe": Color(0.85, 0.35, 0.45),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
static var TAG_TEXT: Dictionary = {
|
||||
"prokrastination": "Aufschieben",
|
||||
"fokus": "Konzentration",
|
||||
"ueberforderung": "Überforderung",
|
||||
"gewohnheit": "Gewohnheiten",
|
||||
"ziel": "Ziele",
|
||||
"ablenkung": "Ablenkung",
|
||||
"selbstzweifel": "Selbstzweifel",
|
||||
"angst": "Angst",
|
||||
"stress": "Stress",
|
||||
"rueckschlag": "Rückschläge",
|
||||
"erschoepfung": "Erschöpfung",
|
||||
"gesundheit": "Gesundheit",
|
||||
"einsamkeit": "Einsamkeit",
|
||||
"grenzen": "Grenzen setzen",
|
||||
}
|
||||
|
||||
|
||||
static func tags_text(tags: Array) -> String:
|
||||
var out: Array = []
|
||||
for t in tags:
|
||||
out.append(TAG_TEXT.get(t, t))
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
static func momente_text(momente: Array) -> String:
|
||||
var out: Array = []
|
||||
for m in momente:
|
||||
out.append(HumanDB.activity_text(m))
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
static func by_id(id: String) -> Dictionary:
|
||||
for t in LIST:
|
||||
if t["id"] == id:
|
||||
return t
|
||||
return {}
|
||||
|
||||
|
||||
static func ids() -> Array:
|
||||
var out: Array = []
|
||||
for t in LIST:
|
||||
out.append(t["id"])
|
||||
return out
|
||||
1
scripts/technique_db.gd.uid
Normal file
1
scripts/technique_db.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b5y2jbmp3ix38
|
||||
431
scripts/ui.gd
Normal file
431
scripts/ui.gd
Normal file
@@ -0,0 +1,431 @@
|
||||
extends CanvasLayer
|
||||
class_name UI
|
||||
|
||||
signal panel_opened
|
||||
signal panel_closed
|
||||
|
||||
const BREITE := 900
|
||||
|
||||
var offen := false
|
||||
var _kind := ""
|
||||
|
||||
var _hud: RichTextLabel
|
||||
var _hinweis: Label
|
||||
var _log: VBoxContainer
|
||||
var _dim: ColorRect
|
||||
var _panel: PanelContainer
|
||||
var _titel: Label
|
||||
var _content: VBoxContainer
|
||||
var _scroll: ScrollContainer
|
||||
var _apply_human: Node3D
|
||||
var _on_travel: Callable
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
layer = 10
|
||||
_baue_hud()
|
||||
_baue_panel()
|
||||
Game.notified.connect(_notiz)
|
||||
Game.state_changed.connect(_hud_aktualisieren)
|
||||
_hud_aktualisieren()
|
||||
|
||||
|
||||
func _baue_hud() -> void:
|
||||
_hud = RichTextLabel.new()
|
||||
_hud.bbcode_enabled = true
|
||||
_hud.fit_content = true
|
||||
_hud.scroll_active = false
|
||||
_hud.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_hud.position = Vector2(18, 14)
|
||||
_hud.custom_minimum_size = Vector2(420, 130)
|
||||
_hud.size = Vector2(420, 130)
|
||||
_hud.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_hud.add_theme_font_size_override("normal_font_size", 17)
|
||||
_hud.add_theme_font_size_override("bold_font_size", 17)
|
||||
add_child(_hud)
|
||||
|
||||
var steuerung := Label.new()
|
||||
steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · Tab Kodex · Esc Maus frei"
|
||||
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
steuerung.position = Vector2(-640, 16)
|
||||
steuerung.size = Vector2(620, 24)
|
||||
steuerung.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
steuerung.add_theme_font_size_override("font_size", 14)
|
||||
steuerung.add_theme_color_override("font_color", Color(1, 1, 1, 0.65))
|
||||
steuerung.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(steuerung)
|
||||
|
||||
_hinweis = Label.new()
|
||||
_hinweis.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
||||
_hinweis.position = Vector2(-300, -110)
|
||||
_hinweis.size = Vector2(600, 30)
|
||||
_hinweis.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_hinweis.add_theme_font_size_override("font_size", 19)
|
||||
_hinweis.add_theme_color_override("font_color", Color(1, 0.95, 0.75))
|
||||
_hinweis.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||
_hinweis.add_theme_constant_override("outline_size", 6)
|
||||
_hinweis.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_hinweis)
|
||||
|
||||
_log = VBoxContainer.new()
|
||||
_log.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
_log.position = Vector2(-520, -230)
|
||||
_log.size = Vector2(500, 200)
|
||||
_log.alignment = BoxContainer.ALIGNMENT_END
|
||||
_log.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(_log)
|
||||
|
||||
|
||||
func _baue_panel() -> void:
|
||||
_dim = ColorRect.new()
|
||||
_dim.color = Color(0, 0, 0, 0.62)
|
||||
_dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_dim.visible = false
|
||||
add_child(_dim)
|
||||
|
||||
_panel = PanelContainer.new()
|
||||
_panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||
_panel.custom_minimum_size = Vector2(BREITE, 200)
|
||||
_panel.visible = false
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = Color(0.10, 0.09, 0.16, 0.98)
|
||||
sb.border_color = Color(0.55, 0.70, 1.0, 0.8)
|
||||
sb.set_border_width_all(2)
|
||||
sb.set_corner_radius_all(10)
|
||||
sb.set_content_margin_all(22)
|
||||
_panel.add_theme_stylebox_override("panel", sb)
|
||||
add_child(_panel)
|
||||
|
||||
var vb := VBoxContainer.new()
|
||||
vb.add_theme_constant_override("separation", 10)
|
||||
_panel.add_child(vb)
|
||||
|
||||
_titel = Label.new()
|
||||
_titel.add_theme_font_size_override("font_size", 26)
|
||||
_titel.add_theme_color_override("font_color", Color(1, 0.93, 0.75))
|
||||
vb.add_child(_titel)
|
||||
|
||||
_scroll = ScrollContainer.new()
|
||||
_scroll.custom_minimum_size = Vector2(BREITE - 44, 140)
|
||||
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
vb.add_child(_scroll)
|
||||
var scroll := _scroll
|
||||
|
||||
_content = VBoxContainer.new()
|
||||
_content.custom_minimum_size.x = BREITE - 70
|
||||
_content.add_theme_constant_override("separation", 10)
|
||||
scroll.add_child(_content)
|
||||
|
||||
var fuss := Label.new()
|
||||
fuss.text = "Esc schließt"
|
||||
fuss.add_theme_font_size_override("font_size", 14)
|
||||
fuss.add_theme_color_override("font_color", Color(1, 1, 1, 0.5))
|
||||
vb.add_child(fuss)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not offen:
|
||||
_hud_aktualisieren()
|
||||
else:
|
||||
var wunsch := _content.get_combined_minimum_size().y
|
||||
var platz := get_viewport().get_visible_rect().size.y - 220.0
|
||||
_scroll.custom_minimum_size.y = clampf(wunsch, 120.0, platz)
|
||||
_panel.position = (get_viewport().get_visible_rect().size - _panel.size) * 0.5
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ HUD
|
||||
|
||||
func _hud_aktualisieren() -> void:
|
||||
var zeilen: Array = []
|
||||
zeilen.append("[b]%s[/b]" % Game.time_string())
|
||||
zeilen.append("Einsicht: [b]%d[/b] Techniken: [b]%d / %d[/b]" %
|
||||
[Game.einsicht, Game.known.size(), TechDB.LIST.size()])
|
||||
if Game.active_mission != "":
|
||||
var h := HumanDB.by_id(Game.active_mission)
|
||||
zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %%" %
|
||||
[h["name"], int(Game.mission_progress(Game.active_mission))])
|
||||
zeilen.append("[color=#aaaaaa]%s[/color]" % h["traum"])
|
||||
elif Game.completed.size() >= HumanDB.LIST.size():
|
||||
zeilen.append("[color=#88ff99]Alle Träume erfüllt.[/color]")
|
||||
else:
|
||||
zeilen.append("[color=#aaaaaa]Keine Mission. Missionshalle in der Traumwelt.[/color]")
|
||||
_hud.text = "\n".join(zeilen)
|
||||
|
||||
|
||||
func setze_hinweis(text: String) -> void:
|
||||
_hinweis.text = text
|
||||
|
||||
|
||||
func _notiz(text: String, farbe: Color) -> void:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
l.custom_minimum_size.x = 500
|
||||
l.add_theme_font_size_override("font_size", 16)
|
||||
l.add_theme_color_override("font_color", farbe)
|
||||
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||
l.add_theme_constant_override("outline_size", 5)
|
||||
_log.add_child(l)
|
||||
while _log.get_child_count() > 6:
|
||||
var alt := _log.get_child(0)
|
||||
_log.remove_child(alt)
|
||||
alt.queue_free()
|
||||
var tw := l.create_tween()
|
||||
tw.tween_interval(6.0)
|
||||
tw.tween_property(l, "modulate:a", 0.0, 1.5)
|
||||
tw.tween_callback(l.queue_free)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Panel
|
||||
|
||||
func _leeren(titel: String) -> void:
|
||||
for c in _content.get_children():
|
||||
_content.remove_child(c)
|
||||
c.queue_free()
|
||||
_titel.text = titel
|
||||
|
||||
|
||||
func _text(bb: String, groesse: int = 17) -> RichTextLabel:
|
||||
var r := RichTextLabel.new()
|
||||
r.bbcode_enabled = true
|
||||
r.fit_content = true
|
||||
r.scroll_active = false
|
||||
r.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
r.custom_minimum_size.x = BREITE - 80
|
||||
r.text = bb
|
||||
r.add_theme_font_size_override("normal_font_size", groesse)
|
||||
r.add_theme_font_size_override("bold_font_size", groesse)
|
||||
_content.add_child(r)
|
||||
return r
|
||||
|
||||
|
||||
func _knopf(beschriftung: String, cb: Callable, aktiv: bool = true) -> Button:
|
||||
var b := Button.new()
|
||||
b.text = beschriftung
|
||||
b.disabled = not aktiv
|
||||
b.custom_minimum_size = Vector2(0, 38)
|
||||
b.add_theme_font_size_override("font_size", 17)
|
||||
_content.add_child(b)
|
||||
if aktiv:
|
||||
b.pressed.connect(cb)
|
||||
return b
|
||||
|
||||
|
||||
func _trenner() -> void:
|
||||
var s := HSeparator.new()
|
||||
_content.add_child(s)
|
||||
|
||||
|
||||
func _zeigen(kind: String) -> void:
|
||||
_kind = kind
|
||||
offen = true
|
||||
_dim.visible = true
|
||||
_panel.visible = true
|
||||
_panel.reset_size()
|
||||
panel_opened.emit()
|
||||
|
||||
|
||||
func schliessen() -> void:
|
||||
if not offen:
|
||||
return
|
||||
offen = false
|
||||
_kind = ""
|
||||
_apply_human = null
|
||||
_dim.visible = false
|
||||
_panel.visible = false
|
||||
panel_closed.emit()
|
||||
|
||||
|
||||
func _hinweis_block(t: Dictionary) -> String:
|
||||
return ("[b]Was:[/b] %s\n[b]Wann:[/b] %s\n[b]Warum:[/b] %s" %
|
||||
[t["was"], t["wann"], t["warum"]])
|
||||
|
||||
|
||||
# -------------------------------------------------------- Bibliothek
|
||||
|
||||
func technik_panel(id: String) -> void:
|
||||
var t := TechDB.by_id(id)
|
||||
_leeren(t["name"])
|
||||
_text(_hinweis_block(t))
|
||||
_trenner()
|
||||
_text("[color=#9fb6ff]Hilft bei:[/color] %s\n[color=#9fb6ff]Gute Momente:[/color] %s" %
|
||||
[TechDB.tags_text(t["tags"]), TechDB.momente_text(t["momente"])], 15)
|
||||
if Game.knows(id):
|
||||
_text("[color=#88ff99]Gelernt — Stufe %d (%s)[/color]" %
|
||||
[Game.skill_of(id), Game.xp_to_next(id)])
|
||||
else:
|
||||
_text("[color=#ffcc66]Noch nicht gelernt. Kosten: 1 Einsicht (du hast %d).[/color]" %
|
||||
Game.einsicht)
|
||||
_knopf("Technik lernen (1 Einsicht)", func() -> void:
|
||||
if Game.learn(id):
|
||||
technik_panel(id), Game.einsicht >= 1)
|
||||
_zeigen("technik")
|
||||
|
||||
|
||||
# -------------------------------------------------------- Missionshalle
|
||||
|
||||
func mission_panel(id: String) -> void:
|
||||
var h := HumanDB.by_id(id)
|
||||
_leeren("%s, %d — %s" % [h["name"], h["alter"], h["beruf"]])
|
||||
_text("[b]Traum:[/b] %s" % h["traum"])
|
||||
_text("[b]Was im Weg steht:[/b] %s" % h["problem"])
|
||||
_text("[color=#9fb6ff]Themen:[/color] %s" % TechDB.tags_text(h["tags"]), 15)
|
||||
_trenner()
|
||||
if Game.completed.has(id):
|
||||
_text("[color=#88ff99]★ Der Traum ist erfüllt.[/color]")
|
||||
elif Game.active_mission == id:
|
||||
_text("[color=#ffdd77]Aktive Mission — %d %%[/color]" % int(Game.mission_progress(id)))
|
||||
else:
|
||||
_text("Fortschritt: %d %%" % int(Game.mission_progress(id)))
|
||||
_knopf("Mission annehmen", func() -> void:
|
||||
Game.accept_mission(id)
|
||||
mission_panel(id))
|
||||
_zeigen("mission")
|
||||
|
||||
|
||||
# -------------------------------------------------------- Meditation
|
||||
|
||||
func vertiefen_panel() -> void:
|
||||
_leeren("Meditationskammer")
|
||||
_text("Übung kostet 1 Einsicht und bringt 3 Erfahrungspunkte. Du hast [b]%d[/b] Einsicht." %
|
||||
Game.einsicht)
|
||||
_trenner()
|
||||
if Game.known.is_empty():
|
||||
_text("[color=#ffcc66]Du kennst noch keine Technik. Geh in die Bibliothek.[/color]")
|
||||
for id in TechDB.ids():
|
||||
if not Game.knows(id):
|
||||
continue
|
||||
var t := TechDB.by_id(id)
|
||||
_text("[b]%s[/b] — Stufe %d (%s)" % [t["name"], Game.skill_of(id), Game.xp_to_next(id)], 16)
|
||||
_knopf("Vertiefen (1 Einsicht)", func() -> void:
|
||||
if Game.deepen(id):
|
||||
vertiefen_panel(), Game.einsicht >= 1)
|
||||
_zeigen("vertiefen")
|
||||
|
||||
|
||||
# -------------------------------------------------------- Kodex
|
||||
|
||||
func kodex_panel() -> void:
|
||||
_leeren("Kodex der Techniken")
|
||||
if Game.known.is_empty():
|
||||
_text("Du kennst noch keine Technik. In der Bibliothek der Traumwelt liegen zwölf.")
|
||||
for id in TechDB.ids():
|
||||
if not Game.knows(id):
|
||||
continue
|
||||
var t := TechDB.by_id(id)
|
||||
_text("[b][color=#ffe3a0]%s[/color][/b] · Stufe %d (%s)" %
|
||||
[t["name"], Game.skill_of(id), Game.xp_to_next(id)], 18)
|
||||
_text(_hinweis_block(t), 15)
|
||||
_text("[color=#9fb6ff]Hilft bei:[/color] %s [color=#9fb6ff]Momente:[/color] %s" %
|
||||
[TechDB.tags_text(t["tags"]), TechDB.momente_text(t["momente"])], 14)
|
||||
_trenner()
|
||||
var fehlend := TechDB.LIST.size() - Game.known.size()
|
||||
if fehlend > 0:
|
||||
_text("[color=#aaaaaa]%d weitere Techniken warten in der Bibliothek.[/color]" % fehlend, 15)
|
||||
_zeigen("kodex")
|
||||
|
||||
|
||||
# -------------------------------------------------------- Anwenden auf der Erde
|
||||
|
||||
func anwenden_panel(mensch: Node3D) -> void:
|
||||
_apply_human = mensch
|
||||
var h: Dictionary = mensch.data
|
||||
var aktiv: String = mensch.activity
|
||||
_leeren("%s — %s" % [h["name"], HumanDB.activity_text(aktiv)])
|
||||
_text("[b]Traum:[/b] %s" % h["traum"], 16)
|
||||
if Game.active_mission == h["id"]:
|
||||
_text("Fortschritt: [b]%d %%[/b]" % int(Game.mission_progress(h["id"])), 16)
|
||||
elif Game.completed.has(h["id"]):
|
||||
_text("[color=#88ff99]★ Traum erfüllt.[/color]", 16)
|
||||
else:
|
||||
_text("[color=#ffcc66]Keine aktive Mission für diesen Menschen. " +
|
||||
"Fortschritt zählt nur mit angenommener Mission.[/color]", 15)
|
||||
_trenner()
|
||||
|
||||
if Game.known.is_empty():
|
||||
_text("[color=#ffcc66]Du kennst keine Technik.[/color]")
|
||||
_zeigen("anwenden")
|
||||
return
|
||||
|
||||
for id in TechDB.ids():
|
||||
if not Game.knows(id):
|
||||
continue
|
||||
var t := TechDB.by_id(id)
|
||||
var ev := Game.evaluate(id, h["id"], aktiv, mensch.resist_left > 0.0)
|
||||
var marker: Array = []
|
||||
if ev["tag"]:
|
||||
marker.append("[color=#88ff99]passt zum Thema[/color]")
|
||||
if ev["moment"]:
|
||||
marker.append("[color=#88ff99]passender Moment[/color]")
|
||||
if ev["blocked"]:
|
||||
marker.append("[color=#ff8877]%s[/color]" % ev["grund"])
|
||||
elif mensch.resist_left > 0.0:
|
||||
marker.append("[color=#ff8877]macht gerade dicht[/color]")
|
||||
if marker.is_empty():
|
||||
marker.append("[color=#aaaaaa]weder Thema noch Moment passen[/color]")
|
||||
_text("[b]%s[/b] · Stufe %d\n[color=#cccccc]%s[/color]\n%s" %
|
||||
[t["name"], Game.skill_of(id), t["wann"], " · ".join(marker)], 16)
|
||||
var beschriftung := "Blockiert" if ev["blocked"] else "Anwenden — %d %% Erfolgschance" % int(round(float(ev["chance"]) * 100.0))
|
||||
_knopf(beschriftung, func() -> void: _anwenden(id), not ev["blocked"])
|
||||
_trenner()
|
||||
_zeigen("anwenden")
|
||||
|
||||
|
||||
func _anwenden(id: String) -> void:
|
||||
if _apply_human == null or not is_instance_valid(_apply_human):
|
||||
schliessen()
|
||||
return
|
||||
var mensch := _apply_human
|
||||
var h: Dictionary = mensch.data
|
||||
var res := Game.apply_technique(id, h["id"], mensch.activity, mensch.resist_left > 0.0)
|
||||
schliessen()
|
||||
if not res["ok"]:
|
||||
Game.notified.emit(res["grund"], Color(1, 0.6, 0.4))
|
||||
return
|
||||
var t := TechDB.by_id(id)
|
||||
if res["success"]:
|
||||
mensch.reagiere(true, "%s wirkt\n+%d %%" % [t["name"], int(round(float(res["gain"])))])
|
||||
Game.notified.emit("%s erreicht %s. (+%d %%)" %
|
||||
[t["name"], h["name"], int(round(float(res["gain"])))], Color(0.6, 1, 0.7))
|
||||
if res["wiederholung"] >= 2:
|
||||
Game.notified.emit("Immer dieselbe Technik wirkt schwächer. Probier eine andere.",
|
||||
Color(0.85, 0.85, 0.6))
|
||||
else:
|
||||
mensch.reagiere(false, "%s prallt ab" % t["name"])
|
||||
Game.notified.emit("%s dringt gerade nicht durch (%d %% Chance)." %
|
||||
[t["name"], int(round(float(res["chance"]) * 100.0))], Color(1, 0.6, 0.5))
|
||||
|
||||
|
||||
# -------------------------------------------------------- Intro
|
||||
|
||||
func intro_panel(on_close: Callable) -> void:
|
||||
_leeren("Traumwandler")
|
||||
_text("Du bist ein Wesen aus der Traumwelt. Menschen tragen Träume mit sich herum, " +
|
||||
"die sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt.")
|
||||
_trenner()
|
||||
_text("[b]In der Traumwelt[/b]\n" +
|
||||
"· [b]Bibliothek der Techniken[/b] — lerne echte Lösungswege. Jede Technik sagt dir, " +
|
||||
"was sie tut, wann sie passt und warum sie hilft.\n" +
|
||||
"· [b]Missionshalle[/b] — nimm einen Menschen an, dem du helfen willst.\n" +
|
||||
"· [b]Meditationskammer[/b] — vertiefe, was du schon kannst.\n" +
|
||||
"· [b]Tor zur Erde[/b] — reise hinab, sobald du eine Mission hast.")
|
||||
_text("[b]Auf der Erde[/b]\n" +
|
||||
"· Du bist ein unsichtbarer Geist und schwebst durch Wände. Leertaste hoch, Umschalt runter.\n" +
|
||||
"· Die Menschen leben ihren Tagesablauf: schlafen, arbeiten, grübeln, spazieren.\n" +
|
||||
"· Geh nah heran und drücke [b]E[/b]. Deine Erfolgschance hängt von deiner Stufe ab — " +
|
||||
"und davon, ob Technik und Moment zum Menschen passen.\n" +
|
||||
"· Schlafende erreichst du nicht. Wer abblockt, braucht eine Weile Ruhe.")
|
||||
_trenner()
|
||||
_text("[color=#aaaaaa]Du beginnst mit 3 Einsicht. Jede Technik kostet 1. " +
|
||||
"Ein erfüllter Traum bringt 3 zurück.[/color]", 15)
|
||||
_knopf("Losgehen", func() -> void:
|
||||
schliessen()
|
||||
on_close.call())
|
||||
_zeigen("intro")
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if offen and event.is_action_pressed("ui_cancel"):
|
||||
schliessen()
|
||||
get_viewport().set_input_as_handled()
|
||||
1
scripts/ui.gd.uid
Normal file
1
scripts/ui.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://mat34vhiwx2m
|
||||
150
tests/shots.gd
Normal file
150
tests/shots.gd
Normal file
@@ -0,0 +1,150 @@
|
||||
extends Node
|
||||
## Fährt das Spiel mit Fenster und legt Screenshots ab, um die Welt
|
||||
## auch optisch zu prüfen.
|
||||
|
||||
const OUT := "user://shots/"
|
||||
var main: Node3D
|
||||
var n := 0
|
||||
|
||||
|
||||
func _process(_d: float) -> void:
|
||||
# Maus des Nutzers nicht einfangen, während die Bilder entstehen
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUT))
|
||||
main = load("res://scenes/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
await knipse("01_intro")
|
||||
|
||||
main.ui.schliessen()
|
||||
main.player.global_position = Vector3(0, 3.0, 26.0)
|
||||
main.player.yaw = 0.0
|
||||
main.player.pitch = -0.10
|
||||
await warte(20)
|
||||
await knipse("02_traumwelt")
|
||||
|
||||
main.player.global_position = Vector3(-24, 2.0, 4.0)
|
||||
main.player.yaw = 0.0
|
||||
main.player.pitch = -0.02
|
||||
await warte(20)
|
||||
await knipse("03_bibliothek_aussen")
|
||||
|
||||
Game.teleport_lock = 0.0
|
||||
main.player.global_position = Vector3(0, -299.0, 2.0)
|
||||
main.player.yaw = 0.0
|
||||
main.player.pitch = -0.12
|
||||
await warte(30)
|
||||
await knipse("04_bibliothek_innen")
|
||||
|
||||
main.ui.technik_panel("pomodoro")
|
||||
await warte(6)
|
||||
await knipse("05_technik_hinweise")
|
||||
main.ui.schliessen()
|
||||
|
||||
Game.learn("pomodoro")
|
||||
Game.learn("zerlegen")
|
||||
Game.learn("box_atmung")
|
||||
|
||||
main.player.global_position = Vector3(90, -299.0, 2.0)
|
||||
main.player.yaw = 0.0
|
||||
main.player.pitch = -0.08
|
||||
await warte(20)
|
||||
await knipse("06_missionshalle")
|
||||
|
||||
main.ui.mission_panel("lena")
|
||||
await warte(6)
|
||||
await knipse("07_mission_panel")
|
||||
main.ui.schliessen()
|
||||
Game.accept_mission("lena")
|
||||
|
||||
main.ui.kodex_panel()
|
||||
await warte(6)
|
||||
await knipse("08_kodex")
|
||||
main.ui.schliessen()
|
||||
|
||||
Game.teleport_lock = 0.0
|
||||
main.player.global_position = Vector3(-90, -299.0, 5.0)
|
||||
main.player.yaw = 0.0
|
||||
main.player.pitch = -0.1
|
||||
await warte(20)
|
||||
await knipse("08b_meditation")
|
||||
main.ui.vertiefen_panel()
|
||||
await warte(6)
|
||||
await knipse("08c_vertiefen")
|
||||
main.ui.schliessen()
|
||||
|
||||
main._erde()
|
||||
Game.time_h = 10.5
|
||||
await warte(40)
|
||||
main.player.global_position = Vector3(0, 26.0, 42.0)
|
||||
main.player.pitch = -0.62
|
||||
main.player.yaw = 0.0
|
||||
await warte(20)
|
||||
await knipse("09_erde_uebersicht")
|
||||
|
||||
var lena: Human = null
|
||||
for m in main.welt.humans:
|
||||
if m.data["id"] == "lena":
|
||||
lena = m
|
||||
for i in range(900):
|
||||
main._process(0.05)
|
||||
for k in main.welt.get_children():
|
||||
if k is Human:
|
||||
k._process(0.05)
|
||||
Game.time_h = 10.5
|
||||
if lena.place == "haus_lena" and lena._path.is_empty():
|
||||
break
|
||||
main.player.global_position = lena.global_position + Vector3(0, 1.2, -4.5)
|
||||
main.player.yaw = PI
|
||||
main.player.pitch = -0.15
|
||||
await warte(20)
|
||||
await knipse("10_lena_zoegert")
|
||||
|
||||
main.ui.anwenden_panel(lena)
|
||||
await warte(6)
|
||||
await knipse("11_anwenden")
|
||||
main.ui._anwenden("pomodoro")
|
||||
await warte(25)
|
||||
await knipse("12_reaktion")
|
||||
|
||||
Game.time_h = 13.5
|
||||
for i in range(1200):
|
||||
main._process(0.05)
|
||||
for k in main.welt.get_children():
|
||||
if k is Human:
|
||||
k._process(0.05)
|
||||
Game.time_h = 13.5
|
||||
main.player.global_position = Vector3(0, 11.0, -14.0)
|
||||
main.player.yaw = PI
|
||||
main.player.pitch = -0.5
|
||||
await warte(20)
|
||||
await knipse("13_cafe_mittag")
|
||||
|
||||
Game.time_h = 22.5
|
||||
await warte(40)
|
||||
main.player.global_position = Vector3(0, 18.0, 34.0)
|
||||
main.player.yaw = 0.0
|
||||
main.player.pitch = -0.5
|
||||
await warte(20)
|
||||
await knipse("14_nacht")
|
||||
|
||||
print("Screenshots: %d" % n)
|
||||
print("Ordner: %s" % ProjectSettings.globalize_path(OUT))
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func warte(frames: int) -> void:
|
||||
for i in range(frames):
|
||||
await get_tree().process_frame
|
||||
|
||||
|
||||
func knipse(bezeichnung: String) -> void:
|
||||
await RenderingServer.frame_post_draw
|
||||
var img := get_viewport().get_texture().get_image()
|
||||
img.save_png(OUT + bezeichnung + ".png")
|
||||
n += 1
|
||||
print(" Bild: " + bezeichnung)
|
||||
1
tests/shots.gd.uid
Normal file
1
tests/shots.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://crqjm7tf6h8mi
|
||||
6
tests/shots.tscn
Normal file
6
tests/shots.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/shots.gd" id="1"]
|
||||
|
||||
[node name="Shots" type="Node"]
|
||||
script = ExtResource("1")
|
||||
468
tests/verify.gd
Normal file
468
tests/verify.gd
Normal file
@@ -0,0 +1,468 @@
|
||||
extends Node
|
||||
## Fährt das echte Spiel headless und prüft die Implementierungen.
|
||||
|
||||
var fehler := 0
|
||||
var main: Node3D
|
||||
|
||||
|
||||
func p(ok: bool, text: String) -> void:
|
||||
print((" OK " if ok else " FEHLT ") + text)
|
||||
if not ok:
|
||||
fehler += 1
|
||||
|
||||
|
||||
func abschnitt(t: String) -> void:
|
||||
print("\n== " + t)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
seed(20240)
|
||||
main = load("res://scenes/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
|
||||
await test_start()
|
||||
await test_bibliothek()
|
||||
await test_gebaeude_betreten()
|
||||
await test_mission_annehmen()
|
||||
await test_reise_zur_erde()
|
||||
await test_tagesablauf()
|
||||
await test_wege_machbar()
|
||||
await test_geist()
|
||||
await test_technik_anwenden()
|
||||
await test_mission_abschliessen()
|
||||
await test_ui_panels()
|
||||
await test_steuerung()
|
||||
|
||||
print("\n=========================================")
|
||||
print("Fehlende Prüfungen: %d" % fehler)
|
||||
print("=========================================")
|
||||
get_tree().quit(1 if fehler > 0 else 0)
|
||||
|
||||
|
||||
func ui() -> UI:
|
||||
return main.ui
|
||||
|
||||
|
||||
func welt() -> Node3D:
|
||||
return main.welt
|
||||
|
||||
|
||||
func schritte(n: int, dt: float = 0.05, zeit: float = -1.0) -> void:
|
||||
for i in range(n):
|
||||
if zeit >= 0.0:
|
||||
Game.time_h = zeit
|
||||
main._process(dt)
|
||||
if welt().has_method("_process"):
|
||||
welt()._process(dt)
|
||||
for kind in welt().get_children():
|
||||
if kind is Human:
|
||||
kind._process(dt)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
func test_start() -> void:
|
||||
abschnitt("Start")
|
||||
p(ui().offen, "Intro-Panel ist offen")
|
||||
ui().schliessen()
|
||||
p(not ui().offen, "Intro lässt sich schließen")
|
||||
p(welt() is DreamWorld, "Startwelt ist die Traumwelt")
|
||||
p(main.player != null and main.player.mode == Player.Mode.TRAUM, "Spieler läuft (kein Geist)")
|
||||
var inter := get_tree().get_nodes_in_group("interactable")
|
||||
p(inter.size() == 12 + 4 + 1 + 1,
|
||||
"18 Interaktionspunkte (12 Techniken, 4 Missionen, Meditation, Tor) — gefunden: %d" % inter.size())
|
||||
p(Game.einsicht == 3, "Startkapital 3 Einsicht")
|
||||
p(Game.known.is_empty(), "Noch keine Technik gelernt")
|
||||
|
||||
|
||||
func test_bibliothek() -> void:
|
||||
abschnitt("Techniken lernen (Bibliothek)")
|
||||
for t in TechDB.LIST:
|
||||
var vollstaendig: bool = t["was"] != "" and t["wann"] != "" and t["warum"] != ""
|
||||
if not vollstaendig:
|
||||
p(false, "Technik %s hat alle drei Hinweise" % t["id"])
|
||||
p(true, "Alle %d Techniken tragen Was/Wann/Warum" % TechDB.LIST.size())
|
||||
|
||||
# Über den echten Interaktionsweg: Spieler an ein Pult stellen
|
||||
var ziel: Node3D = null
|
||||
for n in get_tree().get_nodes_in_group("interactable"):
|
||||
if n.get_meta("kind") == "technik" and n.get_meta("arg") == "pomodoro":
|
||||
ziel = n
|
||||
p(ziel != null, "Pult für die Pomodoro-Technik existiert")
|
||||
main.player.global_position = ziel.global_position + Vector3(0, 0.5, 1.0)
|
||||
main._process(0.05)
|
||||
p(main._fokus == ziel, "Spieler erfasst das Pult als Fokus")
|
||||
main._interagieren()
|
||||
p(ui().offen and ui()._kind == "technik", "Technik-Panel öffnet sich")
|
||||
ui().schliessen()
|
||||
|
||||
p(Game.learn("pomodoro"), "Pomodoro gelernt")
|
||||
p(Game.learn("zerlegen"), "Aufgabe zerlegen gelernt")
|
||||
p(Game.learn("umgebung"), "Umgebung gestalten gelernt")
|
||||
p(Game.einsicht == 0, "Einsicht auf 0 verbraucht")
|
||||
p(not Game.learn("reframing"), "Ohne Einsicht keine weitere Technik")
|
||||
p(Game.skill_of("pomodoro") == 1, "Frisch gelernte Technik ist Stufe 1")
|
||||
Game.add_xp("pomodoro", 4)
|
||||
p(Game.skill_of("pomodoro") == 2, "4 EP heben auf Stufe 2")
|
||||
Game.add_xp("pomodoro", 24)
|
||||
p(Game.skill_of("pomodoro") == 5, "28 EP erreichen Stufe 5")
|
||||
Game.known["pomodoro"] = 0
|
||||
|
||||
|
||||
func test_gebaeude_betreten() -> void:
|
||||
abschnitt("Gebäude betreten")
|
||||
var vorher: Vector3 = main.player.global_position
|
||||
main.player.global_position = Vector3(-24, 1.0, -4.0)
|
||||
Game.teleport_lock = 0.0
|
||||
for i in range(12):
|
||||
await get_tree().physics_frame
|
||||
main.player.global_position = Vector3(-24, 1.0, -9.0)
|
||||
for i in range(20):
|
||||
await get_tree().physics_frame
|
||||
var drin: bool = main.player.global_position.y < -250.0
|
||||
p(drin, "Tür der Bibliothek versetzt in den Innenraum (y=%.1f)" % main.player.global_position.y)
|
||||
|
||||
# drinnen stehen die Pulte
|
||||
var innen_pulte := 0
|
||||
for n in get_tree().get_nodes_in_group("interactable"):
|
||||
if n.get_meta("kind") == "technik" and n.global_position.y < -250.0:
|
||||
innen_pulte += 1
|
||||
p(innen_pulte == 12, "12 Technik-Pulte stehen im Innenraum (%d)" % innen_pulte)
|
||||
|
||||
Game.teleport_lock = 0.0
|
||||
main.player.global_position = Vector3(0, -299.0, 6.0)
|
||||
for i in range(10):
|
||||
await get_tree().physics_frame
|
||||
main.player.global_position = Vector3(0, -299.0, 12.0)
|
||||
for i in range(20):
|
||||
await get_tree().physics_frame
|
||||
p(main.player.global_position.y > -100.0,
|
||||
"Ausgang bringt zurück nach draußen (y=%.1f)" % main.player.global_position.y)
|
||||
main.player.global_position = vorher
|
||||
|
||||
|
||||
func test_mission_annehmen() -> void:
|
||||
abschnitt("Mission annehmen")
|
||||
var ziel: Node3D = null
|
||||
for n in get_tree().get_nodes_in_group("interactable"):
|
||||
if n.get_meta("kind") == "mission" and n.get_meta("arg") == "lena":
|
||||
ziel = n
|
||||
p(ziel != null, "Stele für Lena Vogt existiert")
|
||||
Game.teleport_lock = 1.0
|
||||
main.player.global_position = ziel.global_position + Vector3(0, 0, 1.2)
|
||||
main._process(0.05)
|
||||
p(main._fokus == ziel, "Stele wird als Fokus erkannt")
|
||||
main._interagieren()
|
||||
p(ui().offen and ui()._kind == "mission", "Missions-Panel öffnet sich")
|
||||
ui().schliessen()
|
||||
p(Game.accept_mission("lena"), "Mission Lena angenommen")
|
||||
p(Game.active_mission == "lena", "Mission ist aktiv")
|
||||
|
||||
|
||||
func test_reise_zur_erde() -> void:
|
||||
abschnitt("Reise zur Erde")
|
||||
var tor: Node3D = null
|
||||
for n in get_tree().get_nodes_in_group("interactable"):
|
||||
if n.get_meta("kind") == "erde":
|
||||
tor = n
|
||||
p(tor != null, "Tor zur Erde existiert")
|
||||
Game.teleport_lock = 1.0
|
||||
main.player.global_position = tor.global_position + Vector3(0, 0, 1.5)
|
||||
main._process(0.05)
|
||||
p(main._fokus == tor, "Tor wird als Fokus erkannt")
|
||||
main._interagieren()
|
||||
await get_tree().process_frame
|
||||
p(main.auf_erde and welt() is EarthWorld, "Spieler ist auf der Erde")
|
||||
p(welt().humans.size() == 4, "Vier Menschen leben in der Stadt")
|
||||
|
||||
|
||||
func test_tagesablauf() -> void:
|
||||
abschnitt("Tagesablauf der Menschen")
|
||||
var lena: Human = null
|
||||
for m in welt().humans:
|
||||
if m.data["id"] == "lena":
|
||||
lena = m
|
||||
p(lena != null, "Lena existiert auf der Erde")
|
||||
|
||||
schritte(30, 0.05, 2.0)
|
||||
p(lena.activity == "schlaeft", "Um 02:00 schläft Lena (%s)" % lena.activity)
|
||||
p(lena.global_position.distance_to(HumanDB.place_center("haus_lena")) < 4.0,
|
||||
"Sie schläft in ihrem Atelier")
|
||||
|
||||
var gewandert := false
|
||||
for i in range(600):
|
||||
schritte(1, 0.05, 8.1)
|
||||
if lena.place == "cafe" and lena._path.is_empty():
|
||||
gewandert = true
|
||||
break
|
||||
p(gewandert, "Um 08:00 geht Lena ins Café und kommt dort an")
|
||||
p(lena.activity == "essen", "Im Café isst sie (%s)" % lena.activity)
|
||||
p(lena.global_position.distance_to(HumanDB.place_center("cafe")) < 4.0,
|
||||
"Sie steht im Café (Abstand %.1f)" %
|
||||
lena.global_position.distance_to(HumanDB.place_center("cafe")))
|
||||
|
||||
var daheim := false
|
||||
for i in range(600):
|
||||
schritte(1, 0.05, 10.2)
|
||||
if lena.place == "haus_lena" and lena._path.is_empty():
|
||||
daheim = true
|
||||
break
|
||||
p(daheim, "Um 10:00 ist sie zurück im Atelier")
|
||||
p(lena.activity == "zoegert", "Dort zögert sie vor der Arbeit (%s)" % lena.activity)
|
||||
|
||||
# alle vier haben zu jeder Stunde einen definierten Ort
|
||||
var luecken := 0
|
||||
for m in welt().humans:
|
||||
for h in range(24):
|
||||
var r: Array = m._routine_at(float(h) + 0.5)
|
||||
if not HumanDB.PLACES.has(r[0]) or not HumanDB.ACTIVITY_TEXT.has(r[1]):
|
||||
luecken += 1
|
||||
p(luecken == 0, "Alle 4 Menschen haben rund um die Uhr Ort und Tätigkeit")
|
||||
|
||||
# ein voller Tag am Stück, mit echter Uhr: niemand bleibt hängen
|
||||
Game.time_h = 0.0
|
||||
var besuchte: Dictionary = {}
|
||||
for m in welt().humans:
|
||||
besuchte[m.data["id"]] = {}
|
||||
for i in range(7200):
|
||||
schritte(1)
|
||||
for m in welt().humans:
|
||||
if m._path.is_empty():
|
||||
besuchte[m.data["id"]][m.place] = true
|
||||
if Game.time_h >= 23.5 and Game.day >= 1:
|
||||
break
|
||||
var alle_ok := true
|
||||
for m in welt().humans:
|
||||
var orte: int = besuchte[m.data["id"]].size()
|
||||
var soll: Dictionary = {}
|
||||
for e in m.data["routine"]:
|
||||
soll[e[1]] = true
|
||||
if orte < soll.size():
|
||||
alle_ok = false
|
||||
p(false, "%s erreichte nur %d von %d Orten" % [m.data["name"], orte, soll.size()])
|
||||
p(alle_ok, "Über einen ganzen Tag erreicht jeder Mensch jede Station seiner Routine")
|
||||
|
||||
|
||||
func test_wege_machbar() -> void:
|
||||
abschnitt("Wege passen in die Zeitfenster")
|
||||
var sek_pro_stunde := Game.DAY_SECONDS / 24.0
|
||||
var alles_ok := true
|
||||
var engste := 999.0
|
||||
var engste_text := ""
|
||||
for m in HumanDB.LIST:
|
||||
var r: Array = m["routine"]
|
||||
for i in range(r.size()):
|
||||
var von: String = String(r[i - 1][1]) if i > 0 else String(r[r.size() - 1][1])
|
||||
var nach: String = String(r[i][1])
|
||||
if von == nach:
|
||||
continue
|
||||
var dauer := _weg_laenge(von, nach) / Human.GEHTEMPO
|
||||
var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() \
|
||||
else (24.0 - float(r[i][0]))
|
||||
var fenster := slot_h * sek_pro_stunde
|
||||
if dauer >= fenster:
|
||||
alles_ok = false
|
||||
p(false, "%s: %s -> %s braucht %.0f s, Fenster nur %.0f s" %
|
||||
[m["name"], von, nach, dauer, fenster])
|
||||
elif fenster - dauer < engste:
|
||||
engste = fenster - dauer
|
||||
engste_text = ("%s: %s -> %s (%.0f s Weg, %.0f s Fenster)"
|
||||
% [m["name"], von, nach, dauer, fenster])
|
||||
p(alles_ok, "Jeder Weg passt in sein Zeitfenster — engster Fall: " + engste_text)
|
||||
|
||||
|
||||
func _weg_laenge(von: String, nach: String) -> float:
|
||||
var pts: Array = HumanDB.route(von, nach, Vector3.ZERO)
|
||||
var l := 0.0
|
||||
var pos := HumanDB.place_center(von)
|
||||
for pt in pts:
|
||||
l += pos.distance_to(pt)
|
||||
pos = pt
|
||||
return l
|
||||
|
||||
|
||||
func test_geist() -> void:
|
||||
abschnitt("Geist-Bewegung")
|
||||
p(main.player.mode == Player.Mode.GEIST, "Spieler ist auf der Erde ein Geist")
|
||||
p(main.player.collision_layer == 0 and main.player.collision_mask == 0,
|
||||
"Der Geist kollidiert mit nichts — er schwebt durch Wände")
|
||||
var vor: Vector3 = main.player.global_position
|
||||
main.player.global_position = Vector3(0, 8.0, 0)
|
||||
main.player.velocity = Vector3(6, 0, 0)
|
||||
for i in range(30):
|
||||
await get_tree().physics_frame
|
||||
p(main.player.global_position.x > vor.x, "Der Geist bewegt sich frei durch den Raum")
|
||||
p(absf(main.player.global_position.y - 8.0) < 0.5, "Ohne Schwerkraft fällt er nicht")
|
||||
|
||||
# keiner der Menschen reagiert auf ihn
|
||||
var lena: Human = welt().humans[0]
|
||||
var davor: Vector3 = lena.global_position
|
||||
main.player.global_position = lena.global_position + Vector3(0, 1, 0.5)
|
||||
schritte(10)
|
||||
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
|
||||
|
||||
|
||||
func test_technik_anwenden() -> void:
|
||||
abschnitt("Techniken anwenden — Erfolgschance")
|
||||
var lena: Human = null
|
||||
for m in welt().humans:
|
||||
if m.data["id"] == "lena":
|
||||
lena = m
|
||||
|
||||
Game.time_h = 9.5
|
||||
for i in range(900):
|
||||
schritte(1)
|
||||
if lena.place == "haus_lena" and lena._path.is_empty():
|
||||
break
|
||||
p(lena.activity == "zoegert", "Lena zögert gerade")
|
||||
|
||||
main.player.global_position = lena.global_position + Vector3(0, 1.0, 1.5)
|
||||
main._process(0.05)
|
||||
p(main._fokus_mensch == lena, "Der Geist erfasst Lena in Reichweite")
|
||||
main._interagieren()
|
||||
p(ui().offen and ui()._kind == "anwenden", "Anwenden-Panel öffnet sich")
|
||||
ui().schliessen()
|
||||
|
||||
Game.known["pomodoro"] = 0
|
||||
var s1: float = Game.evaluate("pomodoro", "lena", "zoegert", false)["chance"]
|
||||
Game.known["pomodoro"] = 28
|
||||
var s5: float = Game.evaluate("pomodoro", "lena", "zoegert", false)["chance"]
|
||||
p(Game.skill_of("pomodoro") == 5, "Pomodoro auf Stufe 5 gebracht")
|
||||
p(s5 > s1, "Höherer Skill = höhere Erfolgschance (%.0f%% -> %.0f%%)" % [s1 * 100, s5 * 100])
|
||||
Game.known["pomodoro"] = 0
|
||||
|
||||
var passend: Dictionary = Game.evaluate("pomodoro", "lena", "zoegert", false)
|
||||
var unpassend: Dictionary = Game.evaluate("pomodoro", "lena", "essen", false)
|
||||
p(passend["moment"] and not unpassend["moment"], "Moment-Erkennung: zögert ja, isst nein")
|
||||
p(passend["chance"] > unpassend["chance"],
|
||||
"Passender Moment erhöht die Chance (%.0f%% > %.0f%%)" %
|
||||
[passend["chance"] * 100, unpassend["chance"] * 100])
|
||||
p(passend["tag"], "Pomodoro trifft Lenas Thema Prokrastination")
|
||||
var falsches_thema: Dictionary = Game.evaluate("pomodoro", "mira", "zoegert", false)
|
||||
p(not falsches_thema["tag"], "Bei Mira trifft Pomodoro das Thema nicht")
|
||||
p(passend["chance"] > falsches_thema["chance"], "Passendes Thema erhöht die Chance")
|
||||
|
||||
var blockiert: Dictionary = Game.evaluate("pomodoro", "lena", "schlaeft", false)
|
||||
p(blockiert["blocked"], "Schlafende Menschen sind nicht erreichbar")
|
||||
var wehrt: Dictionary = Game.evaluate("pomodoro", "lena", "zoegert", true)
|
||||
p(wehrt["chance"] < passend["chance"], "Wer abblockt, ist schwerer zu erreichen")
|
||||
var ungelernt: Dictionary = Game.evaluate("nein_sagen", "lena", "zoegert", false)
|
||||
p(ungelernt["blocked"], "Ungelernte Techniken lassen sich nicht anwenden")
|
||||
|
||||
var vorher_p: float = Game.mission_progress("lena")
|
||||
var res := Game.apply_technique("pomodoro", "lena", "zoegert", false)
|
||||
p(res["ok"], "Anwendung wird ausgeführt")
|
||||
if res["success"]:
|
||||
p(Game.mission_progress("lena") > vorher_p, "Erfolg bringt Missionsfortschritt")
|
||||
else:
|
||||
p(Game.mission_progress("lena") == vorher_p, "Misserfolg bringt keinen Fortschritt")
|
||||
p(Game.known["pomodoro"] > 0, "Jede Anwendung bringt Erfahrung")
|
||||
|
||||
# Wiederholung derselben Technik wirkt schwächer
|
||||
Game.progress["lena"] = 0.0
|
||||
Game.used.clear()
|
||||
Game.known["pomodoro"] = 28
|
||||
var ertraege: Array = []
|
||||
for i in range(6):
|
||||
var r := Game.apply_technique("pomodoro", "lena", "zoegert", false)
|
||||
if r["success"]:
|
||||
ertraege.append(r["gain"])
|
||||
p(ertraege.size() >= 2 and ertraege[ertraege.size() - 1] < ertraege[0],
|
||||
"Dieselbe Technik immer wieder wirkt schwächer")
|
||||
|
||||
|
||||
func test_mission_abschliessen() -> void:
|
||||
abschnitt("Mission abschließen")
|
||||
Game.progress["lena"] = 0.0
|
||||
Game.used.clear()
|
||||
var einsicht_vorher := Game.einsicht
|
||||
var runden := 0
|
||||
while Game.mission_progress("lena") < Game.MISSION_ZIEL and runden < 400:
|
||||
Game.apply_technique("pomodoro", "lena", "zoegert", false)
|
||||
Game.apply_technique("zerlegen", "lena", "gruebelt", false)
|
||||
Game.apply_technique("umgebung", "lena", "aufraeumen", false)
|
||||
runden += 1
|
||||
p(Game.mission_progress("lena") >= Game.MISSION_ZIEL,
|
||||
"Lenas Traum erreicht 100 %% (nach %d Runden)" % runden)
|
||||
p(Game.completed.has("lena"), "Mission gilt als abgeschlossen")
|
||||
p(Game.active_mission == "", "Aktive Mission wurde freigegeben")
|
||||
p(Game.einsicht == einsicht_vorher + 3, "Abschluss bringt 3 Einsicht")
|
||||
schritte(3)
|
||||
var lena: Human = welt().humans[0]
|
||||
p(lena.beschriftung().contains("erfüllt"), "Lena zeigt sichtbar den erfüllten Traum")
|
||||
|
||||
|
||||
func test_ui_panels() -> void:
|
||||
abschnitt("Oberfläche")
|
||||
ui().kodex_panel()
|
||||
p(ui().offen and ui()._content.get_child_count() > 0, "Kodex zeigt gelernte Techniken")
|
||||
ui().schliessen()
|
||||
ui().vertiefen_panel()
|
||||
p(ui()._content.get_child_count() > 0, "Meditationskammer listet Techniken")
|
||||
ui().schliessen()
|
||||
var e_vor := Game.einsicht
|
||||
p(Game.deepen("pomodoro"), "Vertiefen funktioniert")
|
||||
p(Game.einsicht == e_vor - 1, "Vertiefen kostet 1 Einsicht")
|
||||
ui().mission_panel("bruno")
|
||||
p(ui()._content.get_child_count() > 0, "Missions-Panel für Bruno baut auf")
|
||||
ui().schliessen()
|
||||
ui().anwenden_panel(welt().humans[1])
|
||||
p(ui()._content.get_child_count() > 0, "Anwenden-Panel für Bruno baut auf")
|
||||
ui().schliessen()
|
||||
p(main._fokus == null or true, "HUD bleibt fehlerfrei")
|
||||
|
||||
|
||||
func test_steuerung() -> void:
|
||||
abschnitt("Steuerung")
|
||||
var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "sinken",
|
||||
"interagieren", "kodex"]
|
||||
var alle := true
|
||||
for a in aktionen:
|
||||
if not InputMap.has_action(a) or InputMap.action_get_events(a).is_empty():
|
||||
alle = false
|
||||
p(false, "Aktion '%s' ist belegt" % a)
|
||||
p(alle, "Alle acht Steuerungsaktionen sind mit Tasten belegt")
|
||||
|
||||
main.ui.schliessen()
|
||||
main.player.look_enabled = true
|
||||
main.player.set_mode(Player.Mode.GEIST)
|
||||
main.player.global_position = Vector3(0, 8.0, 0)
|
||||
main.player.velocity = Vector3.ZERO
|
||||
main.player.yaw = 0.0
|
||||
var start: Vector3 = main.player.global_position
|
||||
|
||||
Input.action_press("vor")
|
||||
for i in range(40):
|
||||
await get_tree().physics_frame
|
||||
Input.action_release("vor")
|
||||
p(main.player.global_position.z < start.z - 1.0,
|
||||
"W schiebt den Spieler nach vorn (dz = %.1f)" % (main.player.global_position.z - start.z))
|
||||
|
||||
main.player.global_position = Vector3(0, 8.0, 0)
|
||||
main.player.velocity = Vector3.ZERO
|
||||
Input.action_press("rechts")
|
||||
for i in range(40):
|
||||
await get_tree().physics_frame
|
||||
Input.action_release("rechts")
|
||||
p(main.player.global_position.x > 1.0,
|
||||
"D schiebt den Spieler nach rechts (x = %.1f)" % main.player.global_position.x)
|
||||
|
||||
main.player.global_position = Vector3(0, 8.0, 0)
|
||||
main.player.velocity = Vector3.ZERO
|
||||
Input.action_press("steigen")
|
||||
for i in range(40):
|
||||
await get_tree().physics_frame
|
||||
Input.action_release("steigen")
|
||||
p(main.player.global_position.y > 9.0,
|
||||
"Leertaste lässt den Geist steigen (y = %.1f)" % main.player.global_position.y)
|
||||
|
||||
main.player.velocity = Vector3.ZERO
|
||||
var hoch: float = main.player.global_position.y
|
||||
Input.action_press("sinken")
|
||||
for i in range(40):
|
||||
await get_tree().physics_frame
|
||||
Input.action_release("sinken")
|
||||
p(main.player.global_position.y < hoch - 1.0,
|
||||
"Umschalt lässt den Geist sinken (y = %.1f)" % main.player.global_position.y)
|
||||
1
tests/verify.gd.uid
Normal file
1
tests/verify.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b3t2t2so80lnh
|
||||
6
tests/verify.tscn
Normal file
6
tests/verify.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://tests/verify.gd" id="1"]
|
||||
|
||||
[node name="Verify" type="Node"]
|
||||
script = ExtResource("1")
|
||||
Reference in New Issue
Block a user