This commit is contained in:
Team3
2026-09-04 00:06:51 +02:00
parent 8c7da42e64
commit 8b60164592
8 changed files with 690 additions and 73 deletions

View File

@@ -11,7 +11,13 @@ sie allein nicht bewegt bekommen. Du wirst zu ihnen geschickt.
godot --path .
Steuerung: **WASD** bewegen · **Maus** umsehen · **E** interagieren ·
**J** Journal · **1/2/3** Tempo · **Esc** Maus freigeben.
**F** folgen · **R** notieren · **J** Journal · **1/2/3** Tempo ·
**Esc** Maus freigeben.
Der Tagesablauf steht im **HUD**: 24 Felder, eines je Stunde, die laufende
hervorgehoben. Grau heißt unbekannt, gelb „hier stimmt etwas nicht, aber du
hast es noch nicht benannt", rot ein benannter Blocker, grün ein gebautes
System. Darunter steht, was gerade läuft.
In der Traumwelt springt **Leertaste**; auf der Erde steigt sie und **Umschalt**
lässt sinken.
@@ -24,7 +30,9 @@ Das Journal führt Buch — es lehrt und vergibt nichts.
* **Techniken** — was du gelernt hast, mit Stufe und Erfahrung. Besser werden
sie nur draußen.
* **Missionen** — die Menschen, für die du dich entschieden hast, jeweils mit
ihrem **Tagesablauf**.
ihrem **Tagesablauf**: der Tag von 0 bis 23 Uhr, in Abschnitte geteilt. Es
gibt keine Wochentage; jeder Tag ist derselbe. Zu Beginn steht in jedem
Abschnitt nur ein Strich — der Tag ist leer, bis du dabei warst.
**Diagnose und Systeme** — der Kern des Spiels
@@ -32,6 +40,10 @@ Ein Traum scheitert an Ursachen: *kein Anfang*, *keine Zeit*, *keine Kraft*,
*kein Halt*, *kein Grund*. Jeder Mensch hat zwei bis drei davon, nie alle. Die
stehen nirgends — du findest sie nur, indem du dabeistehst.
Dabeisein füllt den Abschnitt im Ablauf: du siehst, dass sie um zehn aufräumt.
Ob das ihren Traum aufhält, entscheidest du — mit **R**. Bis dahin steht dort
ein Fragezeichen.
Sichtbar wird eine Ursache an den Tätigkeiten, die sie hervorbringt. Wer nie in
Gang kommt, räumt auf statt anzufangen, recherchiert oder schreibt Später-Listen.
Wer keine Kraft hat, schaut abends eine Serie und bleibt danach wach. Jede
@@ -116,6 +128,14 @@ Abwehr. Am Ausgang ändert sich dadurch nichts, nur an der Wartezeit.
ihren Tag: schlafen, arbeiten, grübeln, essen, spazieren. Wer wo wohnt,
arbeitet in der Nähe; das Café liegt in der Mitte.
* Du bist ein unsichtbarer Geist, schwebst und gehst durch Wände.
* **F** hängt dich an einen Menschen: du treibst hinter ihm her, durch Türen
und über den ganzen Tag, ohne ihm nachlaufen zu müssen. **F** noch einmal —
oder ein Schritt mit **WASD** — lässt ihn wieder los.
* **R notieren.** Was den Traum aufhält, steht nirgends geschrieben. Du siehst
nur, was der Mensch tut, und musst selbst entscheiden: läuft hier etwas
schief? Triffst du den Moment, steht die Ursache im Journal und der Slot
wird bebaubar. Greifst du daneben, schaust du sechs Sekunden umsonst hin.
Zusehen allein deckt nichts auf — das Bemerken ist der Zug.
* Nah herangehen, **E** drücken, Technik wählen. Das Panel nennt für jede
Technik nur, ob Thema und Moment passen — keine Zahl.
* Die Säule im Park bringt dich zurück in die Traumwelt.

View File

@@ -3,14 +3,15 @@ Kern
um Menschen zu helfen, ihre Träume zu erfüllen.
Loops
- Techniken lernen -> Systeme anpassen -> Probleme lösen
- Träume erfüllen -> Welt vergrößeren -> Techniken entdecken
- Techniken entdecken -> Techniken lernen -> Missionen freischalten
- Menschen beobachten -> Probleme finden -> Techniken anwenden
Funktionen
- Bewegung: Bewegen, Springen, Geist
- Bewegung: Bewegen, Springen, Geist, Anhängen
- Journal: Gelernte Techniken, Akzetierte Missionen
- Techniken: Lernen, Verbessern
- Missionen: Pipeline
- Missionen: Pipeline, Beobachten
- Techniken: Cooldown, Fehlschlag
- Menschen: Tagesablauf, Tempo
- Menschen: Tagesablauf, Tempo, Verfolgung
- Prozedurale Generierung: Dorf, Stadt

View File

@@ -111,7 +111,9 @@ func _process(roh: float) -> void:
if _path.is_empty():
activity = soll[1]
else:
var schritt := d.normalized() * GEHTEMPO * delta
# nie über den Wegpunkt hinaus -- sonst pendelt er bei hohem Tempo
# um ihn herum, statt anzukommen
var schritt := d.normalized() * minf(GEHTEMPO * delta, d.length())
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

View File

@@ -24,7 +24,8 @@ var progress: Dictionary = {} # human_id -> 0..100
var cooldown: Dictionary = {} # tech_id -> Restsekunden
var systeme: Dictionary = {} # human_id -> {slot -> {tech, guete}}
var erkannt_slots: Dictionary = {} # human_id -> {slot -> true}
var beobachtung: Dictionary = {} # "human:slot" -> Sekunden
var gesehen: Dictionary = {} # human_id -> {slot -> true}, wo man dabei war
var notiz_sperre: float = 0.0 # nach einem Fehlgriff kurz gesperrt
var completed: Array = []
var time_h: float = 7.0
var day: int = 1
@@ -55,6 +56,8 @@ func _setup_input() -> void:
"steigen": [KEY_SPACE],
"sinken": [KEY_SHIFT],
"interagieren": [KEY_E],
"folgen": [KEY_F],
"notieren": [KEY_R],
"journal": [KEY_J, KEY_TAB],
"tempo_1": [KEY_1, KEY_KP_1],
"tempo_2": [KEY_2, KEY_KP_2],
@@ -305,7 +308,7 @@ func _insel_waechst() -> void:
const BASIS_TEMPO := 45.0 # Prozent je Tag, wenn nichts mehr bremst
const GUETE_ERSATZ := 1.35 # Blocker mit gleicher Belohnung ersetzt
const GUETE_NEU := 1.0 # System in einen freien Slot gesetzt
const ERKENNEN_SEK := 3.0 # so lange muss man dabeistehen
const NOTIZ_SPERRE := 6.0 # nach einem Fehlgriff schaut man erst wieder hin
func slots(human_id: String) -> Array:
@@ -313,6 +316,34 @@ func slots(human_id: String) -> Array:
return h["routine"] if h.has("routine") else []
## Welcher Slot deckt diese Stunde ab? Jeder Tag ist gleich, es gibt keine
## Wochentage -- der Ablauf 0-23 Uhr wiederholt sich.
func slot_bei_stunde(human_id: String, stunde: int) -> int:
var r := slots(human_id)
if r.is_empty():
return -1
var nr := r.size() - 1
for i in range(r.size()):
if float(stunde) >= float(r[i][0]):
nr = i
return nr
## Dabeigewesen: die Stunde ist im Ablauf nicht mehr leer. Was dort schiefläuft,
## steht damit noch nicht fest -- das muss der Spieler benennen.
func gesehen_slot(human_id: String, nr: int) -> bool:
return gesehen.get(human_id, {}).has(nr)
func sehen(human_id: String, nr: int) -> void:
if gesehen_slot(human_id, nr):
return
if not gesehen.has(human_id):
gesehen[human_id] = {}
gesehen[human_id][nr] = true
state_changed.emit()
func slot_grund(human_id: String, nr: int) -> String:
var r := slots(human_id)
return str(r[nr][2]) if nr >= 0 and nr < r.size() else ""
@@ -339,23 +370,30 @@ func erkannt(human_id: String, nr: int) -> bool:
return erkannt_slots.get(human_id, {}).has(nr)
## Dabeistehen deckt auf, was dort wirklich läuft -- und damit die Ursache.
func beobachten(human_id: String, nr: int, delta: float) -> bool:
## Zusehen allein deckt nichts auf. Der Spieler muss den Moment benennen:
## "hier läuft etwas schief". Trifft er, steht die Ursache im Journal.
## Greift er daneben, schaut er eine Weile umsonst hin.
## Grundlage bleibt awareness training -- nur ist das Bemerken jetzt ein Akt.
func notieren(human_id: String, nr: int) -> String:
if notiz_sperre > 0.0:
return "gesperrt"
var grund := slot_grund(human_id, nr)
if not Ursachen.ist_blocker(grund):
notiz_sperre = NOTIZ_SPERRE
notified.emit("Hier läuft nichts schief — %s ist einfach ihr Tag." %
Orte.text(grund), Color(1, 0.75, 0.55))
return "daneben"
if erkannt(human_id, nr):
return false
var schluessel := "%s:%d" % [human_id, nr]
var stand: float = float(beobachtung.get(schluessel, 0.0)) + delta
beobachtung[schluessel] = stand
if stand < ERKENNEN_SEK:
return false
notified.emit("Das hast du schon notiert: %s." % Orte.text(grund),
Color(0.8, 0.85, 1))
return "bekannt"
if not erkannt_slots.has(human_id):
erkannt_slots[human_id] = {}
erkannt_slots[human_id][nr] = true
var b := slot_grund(human_id, nr)
notified.emit("Erkannt: %s — %s" % [Ursachen.BLOCKER[b]["name"],
Ursachen.LISTE[Ursachen.ursache_von(b)]["name"]], Color(0.8, 0.95, 1))
notified.emit("Erkannt: %s — %s" % [Ursachen.BLOCKER[grund]["name"],
Ursachen.LISTE[Ursachen.ursache_von(grund)]["name"]], Color(0.8, 0.95, 1))
state_changed.emit()
return true
return "treffer"
## Eine Ursache gilt als erkannt, sobald eine ihrer Tätigkeiten aufgedeckt ist.

View File

@@ -72,21 +72,12 @@ func _process(delta: float) -> void:
if ui.offen:
ui.setze_hinweis("")
return
Game.notiz_sperre = maxf(0.0, Game.notiz_sperre - delta)
_fokus_suchen()
_beobachten(dt)
_dabeisein()
_hinweis_setzen()
## Dabeistehen deckt auf, was der Mensch gerade wirklich tut.
func _beobachten(dt: float) -> void:
if _fokus_mensch == null or not auf_erde:
return
var id: String = str(_fokus_mensch.data["id"])
if id != Game.active_mission:
return
Game.beobachten(id, int(_fokus_mensch.slot), dt)
func _fokus_suchen() -> void:
_fokus = null
_fokus_mensch = null
@@ -111,14 +102,12 @@ func _fokus_suchen() -> void:
func _hinweis_setzen() -> void:
if _fokus_mensch != null:
var mid: String = str(_fokus_mensch.data["name"])
var mnr: int = int(_fokus_mensch.slot)
var mkey: String = str(_fokus_mensch.data["id"])
if mkey == Game.active_mission and not Game.erkannt(mkey, mnr):
ui.setze_hinweis("%s — schau zu, was sie hier tut …" % mid)
else:
ui.setze_hinweis("[E] %s — %s" % [mid, Orte.text(_fokus_mensch.activity)])
if player.folgt != null and is_instance_valid(player.folgt):
ui.setze_hinweis("[F] loslassen · %s — %s" %
[player.folgt.data["name"], _notiz_hinweis(player.folgt)])
elif _fokus_mensch != null:
ui.setze_hinweis("[F] folgen · %s — %s" %
[_fokus_mensch.data["name"], _notiz_hinweis(_fokus_mensch)])
elif _fokus != null:
var kind: String = _fokus.get_meta("kind")
var titel: String = _fokus.get_meta("titel")
@@ -137,6 +126,18 @@ func _hinweis_setzen() -> void:
ui.setze_hinweis("")
## Was am Menschen gerade dransteht: notieren, ansprechen oder nur zusehen.
func _notiz_hinweis(mensch: Bewohner) -> String:
var tat := Orte.text(mensch.activity)
if str(mensch.data["id"]) != Game.active_mission:
return tat
if Game.notiz_sperre > 0.0:
return "%s (%.0f s)" % [tat, Game.notiz_sperre]
if Game.erkannt(str(mensch.data["id"]), int(mensch.slot)):
return "%s · [E] ansprechen" % tat
return "%s · [R] notieren, wenn hier etwas schiefläuft" % tat
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 \
@@ -162,6 +163,66 @@ func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interagieren"):
_interagieren()
get_viewport().set_input_as_handled()
return
if event.is_action_pressed("folgen"):
_verfolgen()
get_viewport().set_input_as_handled()
return
if event.is_action_pressed("notieren"):
_notieren()
get_viewport().set_input_as_handled()
## Wer dabeisteht, füllt die Stunde im Ablauf -- mehr nicht. Was dort
## schiefläuft, muss er selbst benennen.
func _dabeisein() -> void:
if _fokus_mensch == null or not auf_erde:
return
var id: String = str(_fokus_mensch.data["id"])
if id == Game.active_mission:
Game.sehen(id, int(_fokus_mensch.slot))
## Den Moment benennen: hier läuft etwas schief. Das ist der Kern des
## Beobachtens -- der Spieler entscheidet, wann er hinschaut.
func _notieren() -> void:
if not auf_erde:
return
var ziel: Bewohner = _fokus_mensch
if ziel == null and player.folgt != null and is_instance_valid(player.folgt):
ziel = player.folgt
if ziel == null:
Game.notified.emit("Geh näher an den Menschen heran, dem du hilfst.",
Color(1, 0.85, 0.5))
return
var id: String = str(ziel.data["id"])
if id != Game.active_mission:
Game.notified.emit("Für %s hast du keine Mission angenommen." % ziel.data["name"],
Color(1, 0.85, 0.5))
return
if Game.notiz_sperre > 0.0:
Game.notified.emit("Du bist noch beim letzten Fehlgriff (%.0f s)." %
Game.notiz_sperre, Color(1, 0.75, 0.55))
return
Game.notieren(id, int(ziel.slot))
## Als Geist an einen Menschen hängen und ihn durch seinen Tag begleiten.
func _verfolgen() -> void:
if not auf_erde:
return
if player.folgt != null:
var name: String = player.folgt.data["name"]
player.folgt = null
Game.notified.emit("Du lässt %s wieder los." % name, Color(0.8, 0.85, 1))
return
if _fokus_mensch == null:
Game.notified.emit("Geh näher an einen Menschen heran, um ihm zu folgen.",
Color(1, 0.85, 0.5))
return
player.folgt = _fokus_mensch
Game.notified.emit("Du hängst dich an %s." % _fokus_mensch.data["name"],
Color(0.7, 0.9, 1))
func _interagieren() -> void:

View File

@@ -10,11 +10,14 @@ const SPEED_GEIST := 11.0
const GRAVITY := 24.0
const JUMP := 9.0
const MAUS := 0.0026
const FOLGE_ABSTAND := 3.0
const FOLGE_SPIEL := 1.5
var mode: int = Mode.TRAUM
var yaw := 0.0
var pitch := -0.25
var look_enabled := true
var folgt: Node3D = null # Mensch, an den sich der Geist gehängt hat
var pivot: Node3D
var arm: SpringArm3D
@@ -67,6 +70,7 @@ func _ready() -> void:
func set_mode(m: int) -> void:
mode = m
folgt = null
if m == Mode.GEIST:
collision_layer = 0
collision_mask = 0
@@ -118,6 +122,23 @@ func _physics_process(delta: float) -> void:
var basis_yaw := Basis(Vector3.UP, yaw)
dir = (basis_yaw * Vector3(input.x, 0, input.y)).normalized()
if mode == Mode.GEIST and folgt != null:
if not is_instance_valid(folgt):
folgt = null
elif dir.length() > 0.01:
folgt = null # eigene Bewegung löst die Verfolgung
else:
var schulter := folgt.global_position + Vector3(0, 1.6, 0) \
- blickrichtung() * FOLGE_ABSTAND
global_position = global_position.lerp(schulter, 1.0 - pow(0.004, delta))
# weich nachziehen, aber an der Leine: sonst reißt der Faden beim Gehen
var ab := global_position - schulter
if ab.length() > FOLGE_SPIEL:
global_position = schulter + ab.normalized() * FOLGE_SPIEL
velocity = Vector3.ZERO
body.rotation.y = lerp_angle(body.rotation.y, yaw, 1.0 - pow(0.001, delta))
return
if mode == Mode.GEIST:
var v := dir * SPEED_GEIST
if look_enabled:

View File

@@ -12,6 +12,12 @@ var _kind := ""
var _hud: RichTextLabel
var _hinweis: Label
var _log: VBoxContainer
var _ablauf: HBoxContainer
var _ablauf_titel: Label
var _ablauf_jetzt: Label
var _ablauf_grund: ColorRect
var _zellen: Array = []
var _ticks: Array = []
var _dim: ColorRect
var _panel: PanelContainer
var _titel: Label
@@ -47,11 +53,14 @@ func _baue_hud() -> void:
_hud.add_theme_font_size_override("bold_font_size", 17)
add_child(_hud)
_baue_ablauf()
var steuerung := Label.new()
steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · J Journal · 1/2/3 Tempo · Esc Maus frei"
steuerung.text = "WASD Bewegen · Maus Umsehen · E Interagieren · F Folgen · " + \
"R Notieren · J Journal · 1/2/3 Tempo · Esc Maus frei"
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
steuerung.position = Vector2(-640, 16)
steuerung.size = Vector2(620, 24)
steuerung.position = Vector2(-720, 16)
steuerung.size = Vector2(700, 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))
@@ -79,6 +88,111 @@ func _baue_hud() -> void:
add_child(_log)
## Der Tagesablauf im HUD: 24 Felder, eines je Stunde, immer sichtbar.
const ABLAUF_X := 18.0
const ABLAUF_Y := 126.0
const ZELLE_BREIT := 14.0
const ZELLE_LUECKE := 2.0
func _baue_ablauf() -> void:
var breite := 24.0 * (ZELLE_BREIT + ZELLE_LUECKE) - ZELLE_LUECKE
_ablauf_grund = ColorRect.new()
_ablauf_grund.color = Color(0, 0, 0, 0.42)
_ablauf_grund.set_anchors_preset(Control.PRESET_TOP_LEFT)
_ablauf_grund.position = Vector2(ABLAUF_X - 8.0, ABLAUF_Y - 6.0)
_ablauf_grund.custom_minimum_size = Vector2(breite + 16.0, 88.0)
_ablauf_grund.size = _ablauf_grund.custom_minimum_size
_ablauf_grund.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_ablauf_grund)
_ablauf_titel = Label.new()
_ablauf_titel.set_anchors_preset(Control.PRESET_TOP_LEFT)
_ablauf_titel.position = Vector2(ABLAUF_X, ABLAUF_Y)
_ablauf_titel.add_theme_font_size_override("font_size", 14)
_ablauf_titel.add_theme_color_override("font_color", Color(1, 1, 1, 0.7))
_ablauf_titel.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_ablauf_titel)
_ablauf = HBoxContainer.new()
_ablauf.set_anchors_preset(Control.PRESET_TOP_LEFT)
_ablauf.position = Vector2(ABLAUF_X, ABLAUF_Y + 22.0)
_ablauf.add_theme_constant_override("separation", int(ZELLE_LUECKE))
_ablauf.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_ablauf)
for stunde in range(24):
var zelle := ColorRect.new()
zelle.custom_minimum_size = Vector2(ZELLE_BREIT, 16)
zelle.mouse_filter = Control.MOUSE_FILTER_IGNORE
_ablauf.add_child(zelle)
_zellen.append(zelle)
# Stundenmarken unter dem Streifen
for stunde in [0, 6, 12, 18]:
var tick := Label.new()
tick.text = "%d" % stunde
tick.set_anchors_preset(Control.PRESET_TOP_LEFT)
tick.position = Vector2(ABLAUF_X + float(stunde) * (ZELLE_BREIT + ZELLE_LUECKE),
ABLAUF_Y + 42.0)
tick.add_theme_font_size_override("font_size", 11)
tick.add_theme_color_override("font_color", Color(1, 1, 1, 0.45))
tick.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(tick)
_ticks.append(tick)
_ablauf_jetzt = Label.new()
_ablauf_jetzt.set_anchors_preset(Control.PRESET_TOP_LEFT)
_ablauf_jetzt.position = Vector2(ABLAUF_X, ABLAUF_Y + 60.0)
_ablauf_jetzt.add_theme_font_size_override("font_size", 15)
_ablauf_jetzt.add_theme_color_override("font_color", Color(1, 0.95, 0.8))
_ablauf_jetzt.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_ablauf_jetzt)
## Farbe je Stunde: unbekannt, gesehen, ungeklärt, benannt, gebaut.
func _ablauf_farbe(human_id: String, nr: int) -> Color:
if not Game.gesehen_slot(human_id, nr):
return Color(0.22, 0.23, 0.30)
if not Game.system_in(human_id, nr).is_empty():
return Color(0.44, 0.78, 0.50)
var grund := Game.slot_grund(human_id, nr)
if Ursachen.ist_blocker(grund):
return Color(0.85, 0.66, 0.32) if not Game.erkannt(human_id, nr) \
else Color(0.84, 0.42, 0.35)
if grund == "schlaeft":
return Color(0.30, 0.33, 0.44)
return Color(0.46, 0.50, 0.62)
func _ablauf_aktualisieren() -> void:
var id := Game.active_mission
var an := id != ""
_ablauf.visible = an
_ablauf_titel.visible = an
_ablauf_jetzt.visible = an
_ablauf_grund.visible = an
for t in _ticks:
(t as Label).visible = an
if not an:
return
var jetzt := int(Game.time_h)
var offen := 0
for stunde in range(24):
var nr := Game.slot_bei_stunde(id, stunde)
var f := _ablauf_farbe(id, nr)
if not Game.gesehen_slot(id, nr):
offen += 1
if stunde == jetzt:
f = f.lightened(0.55)
var zelle: ColorRect = _zellen[stunde]
zelle.color = f
zelle.custom_minimum_size.y = 20 if stunde == jetzt else 16
_ablauf_titel.text = "Ihr Tag 0 – 23 Uhr%s" % \
("" if offen == 0 else " · %d Stunden unbekannt" % offen)
var nr_jetzt := Game.slot_bei_stunde(id, jetzt)
_ablauf_jetzt.text = "%02d Uhr %s" % [jetzt, _stunden_text(id, nr_jetzt)]
func _baue_panel() -> void:
_dim = ColorRect.new()
_dim.color = Color(0, 0, 0, 0.62)
@@ -163,6 +277,7 @@ func _hud_aktualisieren() -> void:
else:
zeilen.append("[color=#aaaaaa]Keine Mission. Schwarzes Brett am Dorfplatz.[/color]")
_hud.text = "\n".join(zeilen)
_ablauf_aktualisieren()
func setze_hinweis(text: String) -> void:
@@ -381,13 +496,11 @@ func systeme_panel(human_id: String) -> void:
_text("[b]%s[/b] — %s %s" % [Ursachen.LISTE[name]["name"],
Ursachen.LISTE[name]["kurz"], stand], 15)
if offene > 0:
_text("[color=#aaaaaa]Steh dabei, wenn sie etwas tut, das du noch nicht " +
"kennst. Dann zeigt sich, warum.[/color]", 14)
_text("[color=#aaaaaa]Steh dabei, dann füllt sich ihr Tag. Und drücke [b]R[/b], " +
"wenn du meinst, dass eine Stunde den Traum aufhält.[/color]", 14)
_trenner()
_text("[b]Ihr Tag[/b]", 17)
for nr in range(Game.slots(human_id).size()):
_slot_zeile(human_id, nr)
_tagesablauf(human_id)
_trenner()
_knopf("Zurück zum Journal", func() -> void: journal_panel("missionen"))
_zeigen("systeme")
@@ -395,27 +508,50 @@ func systeme_panel(human_id: String) -> void:
_scroll_frames = 4
func _slot_zeile(human_id: String, nr: int) -> void:
var slot: Array = Game.slots(human_id)[nr]
var stunde := "%02d:00" % int(slot[0])
var grund := Game.slot_grund(human_id, nr)
if grund == "schlaeft":
_knopf("%s schläft" % stunde, func() -> void: pass, false)
return
## Der Ablauf von 0 bis 23 Uhr. Jeder Tag ist gleich -- es gibt keine
## Wochentage. Zu Beginn ist jede Stunde leer; wer dabeisteht, füllt sie.
func _tagesablauf(human_id: String) -> void:
var offen := 0
for stunde in range(24):
if not Game.gesehen_slot(human_id, Game.slot_bei_stunde(human_id, stunde)):
offen += 1
_text("[b]Ihr Tag[/b] [color=#8e93a8]0 – 23 Uhr, jeden Tag derselbe%s[/color]" %
("" if offen == 0 else " · %d Stunden noch leer" % offen), 17)
# eine Zeile je Abschnitt des Tages -- auch wenn noch keiner bekannt ist,
# steht der Tag vollständig da
var von := 0
while von < 24:
var nr := Game.slot_bei_stunde(human_id, von)
var bis := von
while bis + 1 < 24 and Game.slot_bei_stunde(human_id, bis + 1) == nr:
bis += 1
_stunden_zeile(human_id, nr, von, bis, _stunden_text(human_id, nr))
von = bis + 1
func _stunden_text(human_id: String, nr: int) -> String:
if not Game.gesehen_slot(human_id, nr):
return "—"
var sys := Game.system_in(human_id, nr)
if not sys.is_empty():
_knopf("%s %s ← %s" % [stunde, Game.slot_aktivitaet(human_id, nr),
TechDB.by_id(str(sys["tech"]))["name"]],
func() -> void: slot_panel(human_id, nr))
return
return "%s ← %s" % [Game.slot_aktivitaet(human_id, nr),
TechDB.by_id(str(sys["tech"]))["name"]]
var grund := Game.slot_grund(human_id, nr)
if not Game.erkannt(human_id, nr):
_knopf("%s ?" % stunde, func() -> void: pass, false)
return
var text := Orte.text(grund)
return "?"
if Ursachen.ist_blocker(grund):
text = "%s (%s)" % [Ursachen.BLOCKER[grund]["name"],
return "%s (%s)" % [Ursachen.BLOCKER[grund]["name"],
Ursachen.LISTE[Ursachen.BLOCKER[grund]["ursache"]]["name"]]
_knopf("%s %s" % [stunde, text], func() -> void: slot_panel(human_id, nr))
return Orte.text(grund)
func _stunden_zeile(human_id: String, nr: int, von: int, bis: int, text: String) -> void:
var spanne := "%02d – %02d" % [von, bis + 1 if bis < 23 else 24]
var offen: bool = Game.gesehen_slot(human_id, nr) and Game.erkannt(human_id, nr) \
and Game.slot_grund(human_id, nr) != "schlaeft"
_knopf("%s %s" % [spanne, text],
func() -> void: slot_panel(human_id, nr), offen)
## Was soll in diesem Slot stehen?
@@ -580,8 +716,9 @@ func anwenden_panel(mensch: Node3D) -> void:
return
var grund := Game.slot_grund(id, nr)
if not Game.erkannt(id, nr):
_text("[color=#ffcc66]Was hier läuft, hast du noch nicht gesehen. " +
"Bleib stehen und schau zu.[/color]", 16)
_text("[color=#ffcc66]Was hier läuft, hast du noch nicht notiert. " +
"Sieh ihr zu und drücke [b]R[/b], wenn du meinst, hier läuft etwas " +
"schief.[/color]", 16)
_zeigen("anwenden")
return
if Ursachen.ist_blocker(grund):

View File

@@ -34,7 +34,12 @@ func _ready() -> void:
await test_tagesablauf()
await test_wege_machbar()
await test_geist()
await test_verfolgung()
await test_gehen_bei_tempo()
await test_diagnose()
await test_notieren_im_spiel()
await test_tagesablauf_anzeige()
await test_ablauf_im_hud()
await test_systeme()
await test_tempo_des_traums()
await test_cooldown()
@@ -691,6 +696,110 @@ func test_geist() -> void:
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
## Bei hohem Tempo wird der Schritt je Bild größer als die Ankunftsschwelle.
## Wer dann über den Wegpunkt hinausläuft, pendelt um ihn herum statt anzukommen.
func test_gehen_bei_tempo() -> void:
abschnitt("Gehen bleibt auch im Schnelllauf ruhig")
ui().schliessen()
var mensch: Bewohner = welt().humans[0]
var stufe_kehrt: Dictionary = {}
for stufe in [1, 3]:
Game.tempo_setzen(stufe)
Game.time_h = 12.9
for i in range(400):
schritte(1, 1.0 / 60.0)
if not mensch._path.is_empty():
break
var kehrt := 0
var bewegt := 0
var vorher: Vector3 = mensch.global_position
var richtung := Vector3.ZERO
for i in range(600):
schritte(1, 1.0 / 60.0)
if mensch._path.is_empty():
continue
var d: Vector3 = mensch.global_position - vorher
d.y = 0.0
if d.length() > 0.001:
bewegt += 1
if richtung != Vector3.ZERO and d.normalized().dot(richtung) < -0.5:
kehrt += 1
richtung = d.normalized()
vorher = mensch.global_position
stufe_kehrt[stufe] = kehrt
var weite := Bewohner.GEHTEMPO * (1.0 / 60.0) * Game.faktor_von(stufe)
p(kehrt <= 4, "Tempo %d (%d×, %.2f je Bild): %d Kehrtwenden in %d Bildern" %
[stufe, int(Game.faktor_von(stufe)), weite, kehrt, bewegt])
p(int(stufe_kehrt[3]) <= int(stufe_kehrt[1]) + 4,
"Schnelllauf zappelt nicht mehr als Normaltempo (%d gegen %d)" %
[stufe_kehrt[3], stufe_kehrt[1]])
Game.tempo_setzen(1)
## Sich an einen Menschen hängen: der Geist begleitet ihn durch seinen Tag,
## statt ihm hinterherzulaufen. Damit bleibt er die ganze Zeit beobachtbar.
func test_verfolgung() -> void:
abschnitt("Als Geist einen Menschen begleiten")
p(InputMap.has_action("folgen") and not InputMap.action_get_events("folgen").is_empty(),
"Eigene Taste fürs Folgen")
ui().schliessen()
main.player.set_mode(Player.Mode.GEIST)
main.player.look_enabled = true
main.player.yaw = 0.0
var mensch: Bewohner = welt().humans[0]
Game.active_mission = str(mensch.data["id"])
main.player.global_position = mensch.global_position + Vector3(0, 1.5, 2.0)
main.player.velocity = Vector3.ZERO
main._process(0.05)
p(main._fokus_mensch == mensch, "Der Mensch ist im Fokus")
main._verfolgen()
p(main.player.folgt == mensch, "F hängt den Geist an ihn")
# Er lebt seinen Tag weiter, der Geist bleibt dran
var weiteste := 0.0
var gelaufen := false
var im_fokus := 0
for i in range(900):
schritte(1)
await get_tree().physics_frame
if not mensch._path.is_empty():
gelaufen = true
if main._fokus_mensch == mensch:
im_fokus += 1
weiteste = maxf(weiteste,
main.player.global_position.distance_to(mensch.global_position))
p(gelaufen, "Er wechselt in dieser Zeit den Ort")
p(weiteste < 6.0, "Der Geist bleibt die ganze Zeit dicht dabei (höchstens %.1f)" % weiteste)
p(im_fokus > 850, "Und behält ihn im Fokus (%d von 900 Schritten)" % im_fokus)
p(main._fokus_mensch == mensch or main.player.folgt == mensch,
"Unterwegs bleibt er notierbar")
# Eigene Bewegung löst die Verfolgung
Input.action_press("vor")
for i in range(10):
await get_tree().physics_frame
Input.action_release("vor")
p(main.player.folgt == null, "Wer selbst losgeht, lässt ihn los")
# Die Taste löst ebenfalls
main.player.global_position = mensch.global_position + Vector3(0, 1.5, 2.0)
main.player.velocity = Vector3.ZERO
main._process(0.05)
main._verfolgen()
p(main.player.folgt == mensch, "Wieder angehängt")
main._verfolgen()
p(main.player.folgt == null, "F lässt wieder los")
# In der Traumwelt gibt es niemanden zu verfolgen
main._traumwelt()
await get_tree().process_frame
main._verfolgen()
p(main.player.folgt == null, "In der Traumwelt hängt sich der Läufer an niemanden")
main._erde()
await get_tree().process_frame
## Woran der Traum scheitert, steht nirgends. Man findet es, indem man
## dabeisteht -- awareness training, bevor irgendetwas ersetzt wird.
func test_diagnose() -> void:
@@ -699,7 +808,7 @@ func test_diagnose() -> void:
Game.active_mission = id
Game.systeme.clear()
Game.erkannt_slots.clear()
Game.beobachtung.clear()
Game.notiz_sperre = 0.0
var urs: Array = Game.ursachen_von(id)
p(urs.size() >= 2 and urs.size() <= 3,
@@ -739,11 +848,19 @@ func test_diagnose() -> void:
p(not Game.ursache_bekannt(id, ursache), "Und die Ursache ist noch unbekannt")
# Solange man nicht lange genug dabeisteht, passiert nichts
Game.beobachten(id, blocker_nr, Game.ERKENNEN_SEK * 0.4)
p(not Game.erkannt(id, blocker_nr), "Kurz hinschauen reicht nicht")
Game.beobachten(id, blocker_nr, Game.ERKENNEN_SEK)
p(Game.erkannt(id, blocker_nr), "Nach %.0f Sekunden dabeistehen zeigt es sich" %
Game.ERKENNEN_SEK)
# Zusehen allein deckt nichts auf -- der Spieler muss den Moment benennen
for i in range(60):
schritte(1)
p(not Game.erkannt(id, blocker_nr), "Bloßes Dabeistehen deckt nichts auf")
p(Game.notieren(id, neutral_nr) == "daneben",
"Im freien Slot geht die Notiz daneben")
p(Game.notiz_sperre > 0.0, "Ein Fehlgriff sperrt kurz (%.0f s)" % Game.notiz_sperre)
p(Game.notieren(id, blocker_nr) == "gesperrt", "Solange nimmt er keine Notiz an")
Game.notiz_sperre = 0.0
p(Game.notieren(id, blocker_nr) == "treffer", "Im Blocker-Slot sitzt sie")
p(Game.erkannt(id, blocker_nr), "Und deckt auf, was dort läuft")
p(is_equal_approx(Game.notiz_sperre, 0.0), "Ein Treffer sperrt nicht")
p(Game.notieren(id, blocker_nr) == "bekannt", "Zweimal notieren bringt nichts Neues")
p(Game.ursache_bekannt(id, ursache),
"Und damit die Ursache: %s" % Ursachen.LISTE[ursache]["name"])
@@ -761,6 +878,226 @@ func test_diagnose() -> void:
## Gegen eine Ursache baut man ein System. Passt die Belohnung, hält es besser.
## Der Ablauf steht auch im HUD, ohne dass man ein Panel öffnen muss.
func test_ablauf_im_hud() -> void:
abschnitt("Der Ablauf steht im HUD")
var id := "mensch_0"
Game.gesehen.clear()
Game.erkannt_slots.clear()
Game.systeme.clear()
Game.active_mission = ""
ui().schliessen()
ui()._hud_aktualisieren()
p(not ui()._ablauf.visible, "Ohne Mission zeigt das HUD keinen Ablauf")
Game.active_mission = id
Game.time_h = 9.5
ui()._hud_aktualisieren()
p(ui()._ablauf.visible, "Mit Mission schon")
p(ui()._zellen.size() == 24, "Ein Feld je Stunde (%d)" % ui()._zellen.size())
p(ui()._ablauf_titel.text.contains("0 – 23"), "Beschriftet von 0 bis 23 Uhr")
p(ui()._ablauf_titel.text.contains("24 Stunden unbekannt"),
"Am Anfang ist alles unbekannt: %s" % ui()._ablauf_titel.text)
var unbekannt: Color = ui()._ablauf_farbe(id, Game.slot_bei_stunde(id, 9))
var nr := Game.slot_bei_stunde(id, 9)
Game.sehen(id, nr)
ui()._hud_aktualisieren()
p(ui()._ablauf_farbe(id, nr) != unbekannt, "Eine gesehene Stunde färbt sich um")
p(not ui()._ablauf_titel.text.contains("24 Stunden"),
"Und der Zähler geht runter: %s" % ui()._ablauf_titel.text)
p(ui()._ablauf_jetzt.text.begins_with("09 Uhr"),
"Darunter steht die laufende Stunde: %s" % ui()._ablauf_jetzt.text)
# Die laufende Stunde ist hervorgehoben
var jetzt: ColorRect = ui()._zellen[9]
var andere: ColorRect = ui()._zellen[8]
p(jetzt.custom_minimum_size.y > andere.custom_minimum_size.y,
"Das Feld der laufenden Stunde steht heraus")
# Ein benannter Blocker sieht anders aus als eine harmlose Stunde
var blocker_nr := -1
var frei_nr := -1
for n in range(Game.slots(id).size()):
var g := Game.slot_grund(id, n)
if Ursachen.ist_blocker(g) and blocker_nr < 0:
blocker_nr = n
elif g != "schlaeft" and not Ursachen.ist_blocker(g) and frei_nr < 0:
frei_nr = n
Game.sehen(id, blocker_nr)
Game.sehen(id, frei_nr)
var vor_notiz: Color = ui()._ablauf_farbe(id, blocker_nr)
Game.notieren(id, blocker_nr)
p(ui()._ablauf_farbe(id, blocker_nr) != vor_notiz,
"Notiert wechselt der Blocker noch einmal die Farbe")
p(ui()._ablauf_farbe(id, blocker_nr) != ui()._ablauf_farbe(id, frei_nr),
"Und hebt sich von einer harmlosen Stunde ab")
Game.notiz_sperre = 0.0
## Der Ablauf 0-23 Uhr: am Anfang leer, füllt sich durchs Dabeisein.
func test_tagesablauf_anzeige() -> void:
abschnitt("Der Tag steht als Ablauf von 0 bis 23 Uhr")
var id := "mensch_0"
Game.active_mission = id
Game.gesehen.clear()
Game.erkannt_slots.clear()
Game.systeme.clear()
Game.notiz_sperre = 0.0
# Jede Stunde des Tages hat einen Slot, und jeder Tag ist derselbe
var luecken := 0
for stunde in range(24):
if Game.slot_bei_stunde(id, stunde) < 0:
luecken += 1
p(luecken == 0, "Alle 24 Stunden sind belegt")
Game.day = 1
var tag_eins: Array = []
for stunde in range(24):
tag_eins.append(Game.slot_bei_stunde(id, stunde))
Game.day = 9
var tag_neun: Array = []
for stunde in range(24):
tag_neun.append(Game.slot_bei_stunde(id, stunde))
p(tag_eins == tag_neun, "Tag 9 sieht aus wie Tag 1 — es gibt keine Wochentage")
Game.day = 1
ui().systeme_panel(id)
var zeilen := _knopf_texte()
p(zeilen.contains("00 – "), "Der Ablauf beginnt um 00 Uhr")
p(zeilen.contains(" – 24"), "Und reicht bis 24 Uhr")
var leer := 0
for stunde in range(24):
if not Game.gesehen_slot(id, Game.slot_bei_stunde(id, stunde)):
leer += 1
p(leer == 24, "Am Anfang ist jede der 24 Stunden unbekannt (%d)" % leer)
p(zeilen.contains("—") and not zeilen.contains("schläft"),
"Und im Ablauf steht nur ein Strich")
ui().schliessen()
# Dabeistehen füllt genau die Stunde, in der man steht
var mensch: Bewohner = null
for m in welt().humans:
if m.data["id"] == id:
mensch = m
var frei_nr := -1
for nr in range(Game.slots(id).size()):
var g := Game.slot_grund(id, nr)
if g != "schlaeft" and not Ursachen.ist_blocker(g):
frei_nr = nr
break
await _hin_zu(mensch, float(Game.slots(id)[frei_nr][0]) + 0.5)
main._process(0.05)
p(Game.gesehen_slot(id, frei_nr), "Wer dabeisteht, füllt die Stunde")
ui().systeme_panel(id)
p(_knopf_texte().contains(Orte.text(Game.slot_grund(id, frei_nr))),
"Sie steht jetzt im Ablauf: %s" % Orte.text(Game.slot_grund(id, frei_nr)))
ui().schliessen()
# Ein Blocker bleibt ein Fragezeichen, bis er benannt ist
var blocker_nr := -1
for nr in range(Game.slots(id).size()):
if Ursachen.ist_blocker(Game.slot_grund(id, nr)):
blocker_nr = nr
break
Game.sehen(id, blocker_nr)
ui().systeme_panel(id)
p(_knopf_texte().contains("?"), "Was schiefläuft, steht erst als Fragezeichen da")
ui().schliessen()
Game.notieren(id, blocker_nr)
ui().systeme_panel(id)
var b: Dictionary = Ursachen.BLOCKER[Game.slot_grund(id, blocker_nr)]
p(_knopf_texte().contains(str(b["name"])),
"Nach dem Notieren steht dort, was sie tut")
p(_knopf_texte().contains(str(Ursachen.LISTE[b["ursache"]]["name"])),
"Und warum: %s" % Ursachen.LISTE[b["ursache"]]["name"])
ui().schliessen()
## Der ganze Weg: dabeistehen, den Moment einschätzen, R drücken.
func test_notieren_im_spiel() -> void:
abschnitt("Notieren ist eine Entscheidung, kein Timer")
var id := "mensch_0"
Game.active_mission = id
Game.erkannt_slots.clear()
Game.notiz_sperre = 0.0
ui().schliessen()
main.player.set_mode(Player.Mode.GEIST)
main.player.look_enabled = true
var blocker_nr := -1
var frei_nr := -1
for nr in range(Game.slots(id).size()):
var g := Game.slot_grund(id, nr)
if Ursachen.ist_blocker(g) and blocker_nr < 0:
blocker_nr = nr
elif g != "schlaeft" and not Ursachen.ist_blocker(g) and frei_nr < 0:
frei_nr = nr
p(blocker_nr >= 0 and frei_nr >= 0, "Ihr Tag hat einen Blocker und einen freien Slot")
var mensch: Bewohner = null
for m in welt().humans:
if m.data["id"] == id:
mensch = m
# Im freien Slot danebengegriffen
await _hin_zu(mensch, float(Game.slots(id)[frei_nr][0]) + 0.5)
p(mensch.slot == frei_nr, "Sie steht im freien Slot (%d)" % mensch.slot)
main._notieren()
p(not Game.erkannt(id, blocker_nr), "Hier gibt es nichts zu notieren")
p(Game.notiz_sperre > 0.0, "Der Fehlgriff kostet eine Sperre")
# Sperre läuft in Echtzeit ab
for i in range(200):
main._process(0.05)
p(is_equal_approx(Game.notiz_sperre, 0.0), "Sie läuft von allein ab")
# Im Blocker-Slot getroffen
await _hin_zu(mensch, float(Game.slots(id)[blocker_nr][0]) + 0.5)
p(mensch.slot == blocker_nr, "Jetzt steht sie im Blocker-Slot (%d)" % mensch.slot)
p(main._fokus_mensch == mensch, "Und ist in Reichweite")
main._notieren()
p(Game.erkannt(id, blocker_nr), "R notiert, was hier schiefläuft: %s" %
Orte.text(Game.slot_grund(id, blocker_nr)))
# Ein fremder Mensch geht niemanden etwas an
Game.erkannt_slots.clear()
Game.notiz_sperre = 0.0
var fremd: Bewohner = null
for m in welt().humans:
if m.data["id"] != id:
fremd = m
main.player.folgt = null
main.player.global_position = fremd.global_position + Vector3(0, 1.5, 1.5)
main._process(0.05)
main._notieren()
p(Game.erkannt_slots.is_empty(),
"Ohne angenommene Mission notiert er bei %s nichts" % fremd.data["name"])
p(is_equal_approx(Game.notiz_sperre, 0.0), "Und das kostet auch keine Sperre")
# Aus der Verfolgung heraus geht es ebenfalls
Game.erkannt_slots.clear()
await _hin_zu(mensch, float(Game.slots(id)[blocker_nr][0]) + 0.5)
main._verfolgen()
p(main.player.folgt == mensch, "An sie gehängt")
main._notieren()
p(Game.erkannt(id, blocker_nr), "Auch im Vorbeischweben lässt sich notieren")
main.player.folgt = null
## Stellt den Geist zur genannten Stunde neben den Menschen.
func _hin_zu(mensch: Bewohner, stunde: float) -> void:
for i in range(600):
schritte(1, 0.05, stunde)
if mensch._path.is_empty():
break
main.player.folgt = null
main.player.global_position = mensch.global_position + Vector3(0, 1.5, 1.5)
main.player.velocity = Vector3.ZERO
await get_tree().process_frame
main._process(0.05)
func test_systeme() -> void:
abschnitt("Systeme — gegen die Ursache, nicht gegen die Tätigkeit")
var id := "mensch_0"
@@ -1612,7 +1949,7 @@ func _erster_traum_tage(saat: int) -> int:
Game.known.clear()
Game.systeme.clear()
Game.erkannt_slots.clear()
Game.beobachtung.clear()
Game.notiz_sperre = 0.0
Game.progress.clear()
Game.completed.clear()
Game.active_mission = ""