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

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