Files
dreamer-game/scripts/ui.gd
2026-09-16 20:06:38 +02:00

1364 lines
46 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends CanvasLayer
class_name UI
## Die Oberfläche. Ein Baukasten (_text, _karte, _knopf) füllt immer denselben
## Inhalt (_content); drei Wirte zeigen ihn: die Seitenleiste rechts für das
## Journal, die Karte über der Zeitleiste für alles am Menschen, das Fenster in
## der Mitte für Orte. Die Zeitleiste unten ist der Tag des Menschen -- alles
## im Spiel hängt daran.
signal hinfahren(id: String)
signal panel_opened
signal panel_closed
const BREITE := 900
const BREITE_FORMULAR := 1080
const DRAWER_BREITE := 470.0
const KARTE_BREITE := 780.0
const LEISTE_H := 116.0
const C_PANEL := Color(0.09, 0.09, 0.16, 0.97)
const C_ROW := Color(0.14, 0.15, 0.24)
const C_ROW_HOVER := Color(0.20, 0.22, 0.34)
const C_AKZENT := Color(0.50, 0.84, 1.0)
const C_TEXT := Color(0.92, 0.93, 0.97)
const C_MUTED := Color(0.56, 0.58, 0.68)
const C_UNBEKANNT := Color(0.22, 0.23, 0.30)
const C_GESEHEN := Color(0.46, 0.50, 0.62)
const C_SCHLAF := Color(0.30, 0.33, 0.44)
const C_WARN := Color(0.85, 0.66, 0.32)
const C_VERDAECHTIG := Color(0.58, 0.46, 0.26) # Spur gelesen, Stunde noch nicht gesehen
const C_BLOCKER := Color(0.84, 0.42, 0.35)
const C_SYSTEM := Color(0.44, 0.78, 0.50)
const C_PAPIER := Color(0.95, 0.92, 0.85) # die Akte: Papier, dunkle Tinte
const C_PAPIER_ZEILE := Color(0.90, 0.86, 0.78)
const TINTE := "#2b2620"
const TINTE_HELL := "#6b6257"
const TINTE_ROT := "#a83a2e"
const TINTE_GRUEN := "#3d7a45"
var offen := false
var _kind := ""
var _root: Control
var _hud: RichTextLabel
var _hinweis: Label
var _log: VBoxContainer
# Zeitleiste
var _ablauf: PanelContainer
var _ablauf_titel: Label
var _ursachen: RichTextLabel
var _probleme: RichTextLabel
var _zellen: Array = []
var _ticks: Array = []
var _ablauf_jetzt: RichTextLabel
# Wirte und Inhalt
var _drawer: PanelContainer
var _karte_host: PanelContainer
var _modal: PanelContainer
var _dim: ColorRect
var _box: VBoxContainer
var _titel: Label
var _scroll: ScrollContainer
var _content: VBoxContainer
var _ziel: Container = null # wohin die Helfer gerade bauen
var _verlauf: Array = [] # wohin Esc zurückführt
var _apply_human: Node3D
var _journal_seite := "techniken"
var _scroll_wunsch := 0
var _scroll_frames := 0
func _ready() -> void:
layer = 10
_root = Control.new()
_root.set_anchors_preset(Control.PRESET_FULL_RECT)
_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.theme = _theme()
add_child(_root)
_baue_hud()
_baue_leiste()
_baue_wirte()
Game.notified.connect(_notiz)
Game.state_changed.connect(_hud_aktualisieren)
_hud_aktualisieren()
# ------------------------------------------------------------------ Theme
func _sb(bg: Color, radius: int = 6, rand: Color = Color(0, 0, 0, 0), rand_breite: int = 0,
links: int = 0) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = bg
s.set_corner_radius_all(radius)
s.border_color = rand
s.set_border_width_all(rand_breite)
if links > 0:
s.border_width_left = links
s.content_margin_left = 12
s.content_margin_right = 12
s.content_margin_top = 6
s.content_margin_bottom = 6
return s
## Dunkel, flach, eine Akzentfarbe. Einmal gesetzt, gilt es überall darunter.
func _theme() -> Theme:
var th := Theme.new()
th.set_stylebox("normal", "Button", _sb(C_ROW))
th.set_stylebox("hover", "Button", _sb(C_ROW_HOVER))
th.set_stylebox("pressed", "Button", _sb(C_ROW_HOVER.lightened(0.1)))
th.set_stylebox("disabled", "Button", _sb(C_ROW.darkened(0.25)))
th.set_stylebox("focus", "Button", StyleBoxEmpty.new())
th.set_color("font_color", "Button", C_TEXT)
th.set_color("font_hover_color", "Button", Color.WHITE)
th.set_color("font_disabled_color", "Button", C_MUTED)
for typ in ["LineEdit", "TextEdit", "SpinBox", "OptionButton"]:
th.set_stylebox("normal", typ, _sb(Color(0.06, 0.06, 0.11), 5, C_MUTED, 1))
th.set_stylebox("focus", typ, _sb(Color(0.06, 0.06, 0.11), 5, C_AKZENT, 1))
th.set_stylebox("hover", "OptionButton", _sb(C_ROW_HOVER, 5, C_MUTED, 1))
th.set_color("font_color", "LineEdit", C_TEXT)
th.set_color("font_color", "TextEdit", C_TEXT)
th.set_color("font_color", "CheckBox", C_TEXT)
th.set_color("font_color", "Label", C_TEXT)
th.set_color("default_color", "RichTextLabel", C_TEXT)
th.set_stylebox("panel", "PanelContainer", _sb(C_PANEL, 10, C_AKZENT * Color(1, 1, 1, 0.45), 1))
th.set_stylebox("panel", "ScrollContainer", StyleBoxEmpty.new())
return th
# ------------------------------------------------------------------ HUD
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, 50)
_hud.size = Vector2(420, 50)
_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)
_root.add_child(_hud)
var steuerung := Label.new()
steuerung.text = "J Journal · 1-3 Tempo · Q Kamera · Mausrad Zoom · Esc"
# rechts verankert und nach links wachsend: sonst ragt der Text aus dem Fenster
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
steuerung.grow_horizontal = Control.GROW_DIRECTION_BEGIN
steuerung.offset_right = -16
steuerung.offset_left = -520
steuerung.offset_top = 16
steuerung.offset_bottom = 40
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.4))
steuerung.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(steuerung)
_hinweis = Label.new()
_hinweis.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
_hinweis.position = Vector2(-320, -(LEISTE_H + 44))
_hinweis.size = Vector2(640, 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
_root.add_child(_hinweis)
_log = VBoxContainer.new()
_log.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
_log.position = Vector2(-520, -(LEISTE_H + 110))
_log.size = Vector2(500, 90)
_log.alignment = BoxContainer.ALIGNMENT_END
_log.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(_log)
func _hud_aktualisieren() -> void:
var kopf := "[b]%s[/b]" % Game.time_string()
if Game.schlaeft_ziel():
kopf += " [color=#9fb6ff]Tempo 3 (%d×) Nacht[/color]" % int(Game.zeit_faktor())
elif Game.tempo != 1:
kopf += " [color=#9fb6ff]Tempo %d (%d×)[/color]" % [Game.tempo, int(Game.zeit_faktor())]
if Game.active_mission != "":
var h := HumanDB.by_id(Game.active_mission)
kopf += "\n[color=#ffdd77]%s[/color] %d %% [color=#8e93a8]+%d %%/Tag[/color]" % \
[h["name"], int(Game.mission_progress(Game.active_mission)),
int(round(Game.traum_tempo(Game.active_mission)))]
elif Game.completed.size() >= HumanDB.LIST.size():
kopf += "\n[color=#88ff99]Alle Träume erfüllt[/color]"
else:
kopf += "\n[color=#8e93a8]Keine Mission — Schwarzes Brett[/color]"
_hud.text = kopf
_ablauf_aktualisieren()
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", 14)
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() > 3:
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)
# ------------------------------------------------------------ Zeitleiste
# Der Tag des Menschen, unten über die ganze Breite. 24 Felder, die laufende
# Stunde steht heraus, links die Ursachen, rechts die Momente. Ein Klick auf
# eine Stunde öffnet ihre Karte darüber.
func _baue_leiste() -> void:
_ablauf = PanelContainer.new()
_ablauf.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
_ablauf.offset_left = 12
_ablauf.offset_right = -12
_ablauf.offset_top = -LEISTE_H
_ablauf.offset_bottom = -10
_ablauf.add_theme_stylebox_override("panel", _sb(Color(0.06, 0.06, 0.11, 0.92), 10,
C_AKZENT * Color(1, 1, 1, 0.3), 1))
_root.add_child(_ablauf)
var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 4)
_ablauf.add_child(vb)
var kopf := HBoxContainer.new()
kopf.add_theme_constant_override("separation", 16)
vb.add_child(kopf)
_ablauf_titel = Label.new()
_ablauf_titel.add_theme_font_size_override("font_size", 13)
_ablauf_titel.add_theme_color_override("font_color", C_MUTED)
kopf.add_child(_ablauf_titel)
_ursachen = _hud_zeile(14)
_ursachen.size_flags_horizontal = Control.SIZE_EXPAND_FILL
kopf.add_child(_ursachen)
_probleme = _hud_zeile(14)
_probleme.size_flags_horizontal = Control.SIZE_EXPAND_FILL
kopf.add_child(_probleme)
var reihe := HBoxContainer.new()
reihe.add_theme_constant_override("separation", 3)
vb.add_child(reihe)
for stunde in range(24):
var zelle := ColorRect.new()
zelle.custom_minimum_size = Vector2(10, 16)
zelle.size_flags_horizontal = Control.SIZE_EXPAND_FILL
zelle.size_flags_vertical = Control.SIZE_SHRINK_END
zelle.mouse_filter = Control.MOUSE_FILTER_STOP
zelle.gui_input.connect(func(ev: InputEvent) -> void:
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
_stunde_klick(stunde))
reihe.add_child(zelle)
_zellen.append(zelle)
reihe.custom_minimum_size.y = 24
var ticks := HBoxContainer.new()
ticks.add_theme_constant_override("separation", 3)
vb.add_child(ticks)
for stunde in range(24):
var t := Label.new()
t.text = "%d" % stunde if stunde % 3 == 0 else ""
t.size_flags_horizontal = Control.SIZE_EXPAND_FILL
t.add_theme_font_size_override("font_size", 10)
t.add_theme_color_override("font_color", Color(1, 1, 1, 0.35))
ticks.add_child(t)
_ticks.append(t)
_ablauf_jetzt = _hud_zeile(15)
vb.add_child(_ablauf_jetzt)
func _hud_zeile(groesse: int) -> RichTextLabel:
var r := RichTextLabel.new()
r.bbcode_enabled = true
r.fit_content = true
r.scroll_active = false
r.mouse_filter = Control.MOUSE_FILTER_IGNORE
r.add_theme_font_size_override("normal_font_size", groesse)
r.add_theme_font_size_override("bold_font_size", groesse)
return r
func _ablauf_farbe(human_id: String, nr: int) -> Color:
match Game.slot_zustand(human_id, nr):
Game.Stunde.UNBEKANNT:
return C_UNBEKANNT
Game.Stunde.GEBAUT:
return C_SYSTEM
Game.Stunde.UNGEKLAERT:
return C_WARN
Game.Stunde.BENANNT:
return C_BLOCKER
Game.Stunde.SCHLAF:
return C_SCHLAF
Game.Stunde.VERDAECHTIG:
return C_VERDAECHTIG
return C_GESEHEN
func _ablauf_aktualisieren() -> void:
var id := Game.active_mission
var an := id != ""
_ablauf.visible = an
if not an:
return
var jetzt := int(Game.time_h)
var offen_std := 0
var verdaechtig := 0
var puls := 0.5 + 0.5 * sin(Time.get_ticks_msec() * 0.008)
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_std += 1
if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG:
verdaechtig += 1
var zelle: ColorRect = _zellen[stunde]
if stunde == jetzt:
f = f.lightened(0.55 if not Game.situation_bei(id) else 0.3 + 0.4 * puls)
zelle.color = f
zelle.custom_minimum_size.y = 24 if stunde == jetzt else 16
zelle.tooltip_text = "%02d Uhr — %s" % [stunde, _stunden_text(id, nr)]
_ablauf_titel.text = "Ihr Tag 0 – 23 Uhr%s%s" % [
"" if offen_std == 0 else " · %d Stunden unbekannt" % offen_std,
"" if verdaechtig == 0 else " · %d verdächtig" % verdaechtig]
var urs: Array = []
for u in Game.ursachen_von(id):
var name := str(Ursachen.LISTE[u]["name"])
if Game.ursache_geloest(id, str(u)) > 0.0:
urs.append("[color=#7bea8f]%s[/color]" % name)
elif Game.ursache_bekannt(id, str(u)):
urs.append("[color=#ff9a8a]%s[/color]" % name)
else:
urs.append("[color=#55596b]?[/color]")
_ursachen.text = "[center]" + " · ".join(urs) + "[/center]"
var zeilen: Array = []
if Game.situation_bei(id):
var s: Dictionary = Probleme.SITUATIONEN[Game.situation["key"]]
zeilen.append("[right][color=#ffcc66][b]Jetzt: %s[/b][/color] %s [b]%.0f s[/b][/right]" %
[s["name"], s["was"], float(Game.situation["rest"])])
else:
var chips: Array = []
for pr in Game.probleme_von(id):
var key := str(pr)
if not Game.problem_bekannt(id, key):
chips.append("[color=#55596b]?[/color]")
elif Game.problem_ueberwunden(id, key):
chips.append("[color=#7bea8f]%s ✓[/color]" % Probleme.name_von(key))
else:
chips.append("[color=#ffb0a0]%s %d/%d[/color]" % [Probleme.name_von(key),
Game.problem_fortschritt(id, key), Game.PROBLEM_UEBERWINDEN])
zeilen.append("[right]" + " ".join(chips) + "[/right]")
_probleme.text = "\n".join(zeilen)
_ablauf_jetzt.text = "%02d Uhr [color=#cfd6ff]%s[/color]" % \
[jetzt, _stunden_text(id, Game.slot_bei_stunde(id, jetzt))]
## Klick auf eine Stunde: ihre Karte über der Leiste.
func _stunde_klick(stunde: int) -> void:
var id := Game.active_mission
if id == "":
return
slot_panel(id, Game.slot_bei_stunde(id, stunde))
# ------------------------------------------------------------------ Wirte
func _baue_wirte() -> void:
_dim = ColorRect.new()
_dim.color = Color(0, 0, 0, 0.55)
_dim.set_anchors_preset(Control.PRESET_FULL_RECT)
_dim.visible = false
_dim.mouse_filter = Control.MOUSE_FILTER_IGNORE
_root.add_child(_dim)
_box = VBoxContainer.new()
_box.add_theme_constant_override("separation", 10)
_titel = Label.new()
_titel.add_theme_font_size_override("font_size", 24)
_titel.add_theme_color_override("font_color", Color(1, 0.93, 0.75))
_box.add_child(_titel)
_scroll = ScrollContainer.new()
# nie querscrollen, aber auch nie die Leiste verbreitern: zu breiter Inhalt
# wird abgeschnitten statt aus dem Fenster zu ragen
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_SHOW_NEVER
_scroll.clip_contents = true
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
_box.add_child(_scroll)
_content = VBoxContainer.new()
_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_content.add_theme_constant_override("separation", 8)
_scroll.add_child(_content)
_drawer = PanelContainer.new()
_drawer.set_anchors_preset(Control.PRESET_RIGHT_WIDE)
_drawer.offset_left = -DRAWER_BREITE - 12
_drawer.offset_right = -12
_drawer.offset_top = 56
_drawer.offset_bottom = -(LEISTE_H + 22)
_drawer.clip_contents = true
_drawer.visible = false
_root.add_child(_drawer)
_karte_host = PanelContainer.new()
_karte_host.set_anchors_preset(Control.PRESET_TOP_LEFT)
_karte_host.custom_minimum_size.x = KARTE_BREITE
_karte_host.visible = false
_root.add_child(_karte_host)
_modal = PanelContainer.new()
_modal.set_anchors_preset(Control.PRESET_CENTER)
_modal.visible = false
_root.add_child(_modal)
func _wirt_fuer(kind: String) -> PanelContainer:
match kind:
"journal", "technik", "akte":
return _drawer
"slot", "situation", "anwenden":
return _karte_host
return _modal
var _takt := 0.0 # HUD- und Leistentexte alle 0,1 s; der Puls läuft je Bild
func _process(delta: float) -> void:
_takt -= delta
var dran := _takt <= 0.0
if dran:
_takt = 0.1
if not offen:
if dran:
_hud_aktualisieren()
_ablauf_aktualisieren()
else:
_ablauf_pulsen()
return
var wirt := _wirt_fuer(_kind)
var wunsch := _content.get_combined_minimum_size().y
var vp := get_viewport().get_visible_rect().size
if wirt == _drawer:
_scroll.custom_minimum_size.y = maxf(120.0, _drawer.size.y - 90.0)
else:
var platz: float = vp.y - LEISTE_H - 120.0 if wirt == _karte_host else vp.y - 200.0
_scroll.custom_minimum_size.y = clampf(wunsch, 60.0, platz)
if _scroll_frames > 0:
_scroll.scroll_vertical = _scroll_wunsch
_scroll_frames -= 1
if wirt == _modal:
var breite: float = BREITE_FORMULAR if _kind == "schreiben" else BREITE
_modal.custom_minimum_size.x = breite
_scroll.custom_minimum_size.x = breite - 48
_modal.reset_size()
_modal.position = (vp - _modal.size) * 0.5
elif wirt == _karte_host:
_scroll.custom_minimum_size.x = KARTE_BREITE - 48
_karte_host.reset_size()
_karte_host.position = Vector2((vp.x - _karte_host.size.x) * 0.5,
vp.y - LEISTE_H - 22.0 - _karte_host.size.y)
if dran:
_ablauf_aktualisieren()
else:
_ablauf_pulsen()
## Nur die laufende Stunde pulsiert -- das darf jedes Bild passieren.
func _ablauf_pulsen() -> void:
var id := Game.active_mission
if id == "" or not _ablauf.visible or not Game.situation_bei(id):
return
var jetzt := int(Game.time_h)
var puls := 0.5 + 0.5 * sin(Time.get_ticks_msec() * 0.008)
var zelle: ColorRect = _zellen[jetzt]
zelle.color = _ablauf_farbe(id, Game.slot_bei_stunde(id, jetzt)).lightened(0.3 + 0.4 * puls)
# ------------------------------------------------------------------ Baukasten
func _wo() -> Container:
return _ziel if _ziel != null else _content
func _leeren(titel: String) -> void:
_ziel = null
for c in _content.get_children():
_content.remove_child(c)
c.queue_free()
_titel.text = titel
func _text(bb: String, groesse: int = 17, in_: Container = null) -> 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.size_flags_horizontal = Control.SIZE_EXPAND_FILL
r.text = bb
r.add_theme_font_size_override("normal_font_size", groesse)
r.add_theme_font_size_override("bold_font_size", groesse)
(in_ if in_ != null else _wo()).add_child(r)
return r
## Eine Aktion: farbig, mittig. Nur für Dinge, die etwas tun.
func _knopf(beschriftung: String, cb: Callable, aktiv: bool = true, in_: Container = null) -> Button:
var b := Button.new()
b.text = beschriftung
b.disabled = not aktiv
b.custom_minimum_size = Vector2(0, 34)
b.add_theme_font_size_override("font_size", 16)
b.add_theme_stylebox_override("normal", _sb(C_AKZENT.darkened(0.62), 6, C_AKZENT, 1))
b.add_theme_stylebox_override("hover", _sb(C_AKZENT.darkened(0.48), 6, C_AKZENT, 1))
b.add_theme_stylebox_override("pressed", _sb(C_AKZENT.darkened(0.35), 6, C_AKZENT, 1))
(in_ if in_ != null else _wo()).add_child(b)
if aktiv:
b.pressed.connect(cb)
return b
## Eine Datenzeile: linksbündig, Farbkante für den Zustand. Klick öffnet Details.
func _karte(beschriftung: String, farbe: Color, cb: Callable, aktiv: bool = true,
in_: Container = null) -> Button:
var b := Button.new()
b.text = beschriftung
b.alignment = HORIZONTAL_ALIGNMENT_LEFT
b.disabled = not aktiv
b.custom_minimum_size = Vector2(0, 34)
b.add_theme_font_size_override("font_size", 15)
b.add_theme_stylebox_override("normal", _sb(C_ROW, 6, farbe, 0, 4))
b.add_theme_stylebox_override("hover", _sb(C_ROW_HOVER, 6, farbe, 0, 4))
b.add_theme_stylebox_override("pressed", _sb(C_ROW_HOVER.lightened(0.1), 6, farbe, 0, 4))
b.add_theme_stylebox_override("disabled", _sb(C_ROW.darkened(0.25), 6, farbe.darkened(0.45), 0, 4))
(in_ if in_ != null else _wo()).add_child(b)
if aktiv:
b.pressed.connect(cb)
return b
## Leise Aktion: zurück, abbrechen.
func _knopf_leise(beschriftung: String, cb: Callable, in_: Container = null) -> Button:
var b := Button.new()
b.text = beschriftung
b.custom_minimum_size = Vector2(0, 30)
b.add_theme_font_size_override("font_size", 14)
b.add_theme_color_override("font_color", C_MUTED)
(in_ if in_ != null else _wo()).add_child(b)
b.pressed.connect(cb)
return b
func _trenner() -> void:
var s := HSeparator.new()
_wo().add_child(s)
## Zeigt den gebauten Inhalt im passenden Wirt. `zurueck` ist der Weg, den Esc
## nimmt -- nur setzen, wenn das Panel aus einem anderen heraus geöffnet wurde.
func _zeigen(kind: String, zurueck: Callable = Callable()) -> void:
_kind = kind
_ziel = null
if kind != "akte":
_drawer.remove_theme_stylebox_override("panel")
_titel.remove_theme_color_override("font_color")
var wirt := _wirt_fuer(kind)
if _box.get_parent() != wirt:
if _box.get_parent() != null:
_box.get_parent().remove_child(_box)
wirt.add_child(_box)
_drawer.visible = wirt == _drawer
_karte_host.visible = wirt == _karte_host
_modal.visible = wirt == _modal
_dim.visible = wirt == _modal
if zurueck.is_valid():
_verlauf.append(zurueck)
if wirt == _modal:
_modal.reset_size()
if not offen:
offen = true
panel_opened.emit()
## Esc: eine Ebene zurück, sonst zu.
func _zurueck() -> void:
if _verlauf.is_empty():
schliessen()
return
var cb: Callable = _verlauf.pop_back()
cb.call()
func schliessen() -> void:
if not offen:
return
offen = false
_kind = ""
_ziel = null
_verlauf.clear()
_apply_human = null
_drawer.visible = false
_karte_host.visible = false
_modal.visible = false
_dim.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\n" +
"[color=#9fb6ff][b]Zu sehen an:[/b] %s[/color]") % \
[t["was"], t["wann"], t["warum"], t["woran"]]
# -------------------------------------------------------- Journal
## Das Journal ist immer dabei und führt Buch: welche Techniken du gelernt hast
## und welche Missionen du angenommen hast. Gelehrt wird im Hörsaal,
## vergeben werden die Missionen am Schwarzen Brett.
func journal_panel(seite: String = "") -> void:
if seite != "":
_journal_seite = seite
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "journal" else 0
_leeren("Journal")
_reiter()
if _journal_seite == "missionen":
_journal_missionen()
else:
_journal_techniken()
_zeigen("journal")
_scroll_wunsch = scroll_alt
_scroll_frames = 4
func _reiter() -> void:
var hb := HBoxContainer.new()
hb.add_theme_constant_override("separation", 8)
_content.add_child(hb)
var seiten := [
["techniken", "Techniken (%d)" % Game.known.size()],
["missionen", "Akten (%d)" % Game.progress.size()],
]
for eintrag in seiten:
var b := Button.new()
b.text = eintrag[1]
b.custom_minimum_size = Vector2(0, 36)
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
b.add_theme_font_size_override("font_size", 16)
b.disabled = _journal_seite == eintrag[0]
if b.disabled:
b.add_theme_color_override("font_disabled_color", Color(1, 0.88, 0.55))
hb.add_child(b)
if not b.disabled:
b.pressed.connect(func() -> void: journal_panel(eintrag[0]))
_trenner()
func _journal_techniken() -> void:
if Game.known.is_empty():
_text("[color=#ffcc66]Noch nichts gelernt — Hörsaal der Wege.[/color]", 16)
return
for id in TechDB.ids():
if not Game.knows(id):
continue
var t := TechDB.by_id(id)
_karte("%s · Stufe %d · %s" % [t["name"], Game.skill_of(id),
Ursachen.LISTE[t["ursache"]]["name"]], t["farbe"],
func() -> void: technik_panel(id))
## Alles zu einer Technik -- nur, wenn man sie aufschlägt.
func technik_panel(tech_id: String) -> void:
var t := TechDB.by_id(tech_id)
_leeren(str(t["name"]))
_text("[color=#aaaaaa]Stufe %d (%s) · gegen [b]%s[/b][/color]" %
[Game.skill_of(tech_id), Game.xp_to_next(tech_id),
Ursachen.LISTE[t["ursache"]]["name"]], 15)
_trenner()
_text(_hinweis_block(t), 15)
_text("[color=#9fb6ff]Zu sehen an:[/color] %s" % t["woran"], 15)
var gegen: Array = []
for tag in t["tags"]:
if Probleme.LISTE.has(str(tag)):
gegen.append(Probleme.name_von(str(tag)))
if not gegen.is_empty():
_text("[color=#9fb6ff]Im Moment gegen:[/color] %s" % ", ".join(gegen), 15)
_zeigen("technik", journal_panel.bind("techniken"))
func _journal_missionen() -> void:
if Game.progress.is_empty():
_text("[color=#ffcc66]Keine Mission — Schwarzes Brett am Dorfplatz.[/color]", 16)
return
for h in HumanDB.LIST:
var id: String = h["id"]
if not Game.angenommen(id):
continue
var stand := "%d %%" % int(Game.mission_progress(id))
var farbe := C_GESEHEN
if Game.completed.has(id):
stand = "★ erfüllt"
farbe = C_SYSTEM
elif Game.active_mission == id:
stand = "aktiv · %d %%" % int(Game.mission_progress(id))
farbe = C_AKZENT
_karte("%s · %s" % [h["name"], stand], farbe,
func() -> void: akte_panel(id))
# ---------------------------------------------- Die Akte
## Alles zu einem Menschen auf einem Blatt: Person und Traum, was im Weg steht,
## der Befund aus Spuren und Beobachtungen, der Tag, die Routinen, die Notizen.
## Keine Patientenakte -- die Leute sind nicht krank, sie haben Gewohnheiten.
var _blatt: VBoxContainer = null
func systeme_panel(human_id: String) -> void:
akte_panel(human_id)
func akte_panel(human_id: String) -> void:
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "akte" else 0
var h := HumanDB.by_id(human_id)
_leeren("Akte")
_papier()
var erfuellt := Game.completed.has(human_id)
_akte_text("[b]%s[/b] [color=%s]%d · %s[/color]" % [h["name"], TINTE_HELL,
int(h.get("alter", 0)), h.get("beruf", "")], 20)
_akte_text("[color=%s]Traum[/color] „%s“" % [TINTE_HELL, h["traum"]], 16)
if erfuellt:
_stempel("TRAUM ERFÜLLT")
else:
_akte_text("[color=%s]Stand[/color] %d %% · +%d %% je Tag" % [TINTE_HELL,
int(Game.mission_progress(human_id)), int(round(Game.traum_tempo(human_id)))], 16)
_knopf("Hinfahren — vor die Tür", func() -> void:
schliessen()
hinfahren.emit(human_id), true, _blatt)
_akte_abschnitt("Was im Weg steht")
_akte_text(str(h["problem"]), 15)
_akte_abschnitt("Befund")
var offen_urs := 0
for u in Game.ursachen_von(human_id):
var name: String = str(u)
var stand: String
if Game.ursache_geloest(human_id, name) > 0.0:
stand = "[color=%s]Routine steht[/color]" % TINTE_GRUEN
elif Game.ursache_bekannt(human_id, name):
var woher := "Spur gelesen" if Game.spur_bekannt(human_id, name) else "beobachtet"
stand = "[color=%s]%s · bremst auf %d %%[/color]" % [TINTE_ROT, woher,
int(float(Ursachen.LISTE[name]["faktor"]) * 100.0)]
else:
offen_urs += 1
continue
_akte_text("[b]%s[/b] %s" % [Ursachen.LISTE[name]["name"], stand], 15)
if offen_urs > 0:
_akte_text("[color=%s]? %s noch nichts gefunden — Spuren in der Wohnung, Momente im Tag[/color]" %
[TINTE_HELL, "eine Ursache," if offen_urs == 1 else "%d Ursachen," % offen_urs], 15)
var probleme: Array = []
for pr in Game.probleme_von(human_id):
var key := str(pr)
if not Game.problem_bekannt(human_id, key):
probleme.append("[color=%s]?[/color]" % TINTE_HELL)
elif Game.problem_ueberwunden(human_id, key):
probleme.append("[color=%s]%s ✓[/color]" % [TINTE_GRUEN, Probleme.name_von(key)])
else:
probleme.append("%s %d/%d" % [Probleme.name_von(key),
Game.problem_fortschritt(human_id, key), Game.PROBLEM_UEBERWINDEN])
if not probleme.is_empty():
_akte_text("[color=%s]In Momenten[/color] %s" % [TINTE_HELL, " · ".join(probleme)], 15)
_akte_abschnitt("Tag")
_tagesablauf(human_id)
_akte_abschnitt("Notizen")
var zeilen: Array = Game.notizen.get(human_id, [])
if zeilen.is_empty():
_akte_text("[color=%s]noch keine[/color]" % TINTE_HELL, 14)
for i in range(maxi(0, zeilen.size() - 8), zeilen.size()):
_akte_text("[color=%s]%s[/color]" % [TINTE, zeilen[i]], 14)
_zeigen("akte", journal_panel.bind("missionen"))
_scroll_wunsch = scroll_alt
_scroll_frames = 4
## Die Leiste selbst wird zum Blatt Papier. Ein Container im Container bricht
## den Text nicht um -- er richtet sich an der längsten Zeile aus und ragt hinaus.
func _papier() -> void:
var s := _sb(C_PAPIER, 4, Color(0.78, 0.72, 0.6), 1)
s.content_margin_left = 18
s.content_margin_right = 18
s.content_margin_top = 10
s.content_margin_bottom = 14
_drawer.add_theme_stylebox_override("panel", s)
_titel.add_theme_color_override("font_color", Color(0.17, 0.15, 0.13))
_blatt = _content
func _akte_text(bb: String, groesse: int = 15) -> RichTextLabel:
var r := _text("[color=%s]%s[/color]" % [TINTE, bb], groesse, _blatt)
return r
func _akte_abschnitt(titel: String) -> void:
var r := _text("[color=%s]%s[/color]" % [TINTE_HELL, titel.to_upper()], 12, _blatt)
r.add_theme_constant_override("line_separation", 2)
var linie := HSeparator.new()
linie.add_theme_stylebox_override("separator", _sb(Color(0.78, 0.72, 0.6, 0.6), 0))
linie.custom_minimum_size.y = 2
_blatt.add_child(linie)
func _stempel(text: String) -> void:
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", 18)
l.add_theme_color_override("font_color", Color(0.66, 0.2, 0.15))
l.add_theme_stylebox_override("normal", _sb(Color(0, 0, 0, 0), 2, Color(0.66, 0.2, 0.15), 2))
l.rotation_degrees = -4.0
_blatt.add_child(l)
## Eine Zeile der Akte, die sich aufschlagen lässt (Stunden des Tages).
func _akte_karte(beschriftung: String, farbe: Color, cb: Callable, aktiv: bool = true) -> Button:
var b := Button.new()
b.text = beschriftung
b.alignment = HORIZONTAL_ALIGNMENT_LEFT
b.disabled = not aktiv
b.custom_minimum_size = Vector2(0, 30)
b.add_theme_font_size_override("font_size", 14)
b.add_theme_color_override("font_color", Color(0.17, 0.15, 0.13))
b.add_theme_color_override("font_hover_color", Color(0.1, 0.08, 0.06))
b.add_theme_color_override("font_disabled_color", Color(0.3, 0.27, 0.24))
b.add_theme_stylebox_override("normal", _sb(C_PAPIER_ZEILE, 3, farbe, 0, 4))
b.add_theme_stylebox_override("hover", _sb(C_PAPIER_ZEILE.darkened(0.06), 3, farbe, 0, 4))
b.add_theme_stylebox_override("pressed", _sb(C_PAPIER_ZEILE.darkened(0.1), 3, farbe, 0, 4))
b.add_theme_stylebox_override("disabled", _sb(C_PAPIER_ZEILE, 3, farbe.darkened(0.2), 0, 4))
_blatt.add_child(b)
if aktiv:
b.pressed.connect(cb)
return b
## 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
if offen > 0:
_akte_text("[color=%s]%d Stunden leer[/color]" % [TINTE_HELL, offen], 14)
# 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:
var grund := Game.slot_grund(human_id, nr)
match Game.slot_zustand(human_id, nr):
Game.Stunde.UNBEKANNT:
return "—"
Game.Stunde.GEBAUT:
var sys := Game.system_in(human_id, nr)
return "%s ← %s" % [Game.slot_aktivitaet(human_id, nr),
TechDB.by_id(str(sys["tech"]))["name"]]
Game.Stunde.UNGEKLAERT:
return "?"
Game.Stunde.BENANNT:
return "%s (%s)" % [Ursachen.BLOCKER[grund]["name"],
Ursachen.LISTE[Ursachen.BLOCKER[grund]["ursache"]]["name"]]
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"
_akte_karte("%s %s" % [spanne, text], _ablauf_farbe(human_id, nr),
func() -> void: slot_panel(human_id, nr, akte_panel.bind(human_id)), offen)
## Der offene Moment: hier zählt, welche Technik zu diesem Problem passt.
func situation_panel(mensch: Node3D) -> void:
_apply_human = mensch
var id: String = str(mensch.data["id"])
if not Game.situation_bei(id):
anwenden_panel(mensch)
return
var s: Dictionary = Probleme.SITUATIONEN[Game.situation["key"]]
var problem := Game.situation_problem()
_leeren("%s — %s" % [mensch.data["name"], s["name"]])
var kopf := "[i]%s[/i]" % s["was"]
if Game.problem_bekannt(id, problem):
kopf += " [color=#ffb0a0]%s %d/%d[/color]" % [Probleme.name_von(problem),
Game.problem_fortschritt(id, problem), Game.PROBLEM_UEBERWINDEN]
else:
kopf += " [color=#ffcc66]?[/color]"
kopf += " [b]%.0f s[/b]" % float(Game.situation["rest"])
_text(kopf, 16)
_trenner()
if Game.known.is_empty():
_text("[color=#ffcc66]Du kennst keine Technik.[/color]", 16)
_zeigen("situation")
return
for tech_id in TechDB.ids():
if not Game.knows(tech_id):
continue
var t := TechDB.by_id(tech_id)
var frei := Game.bereit(tech_id)
var text := "%s · Stufe %d" % [t["name"], Game.skill_of(tech_id)]
if not frei:
text += " · %.0f s" % Game.cooldown_rest(tech_id)
_knopf(text, func() -> void:
Game.situation_anwenden(tech_id)
schliessen(), frei)
_zeigen("situation")
## Was soll in diesem Slot stehen?
func slot_panel(human_id: String, nr: int, zurueck: Callable = Callable()) -> void:
var h := HumanDB.by_id(human_id)
var grund := Game.slot_grund(human_id, nr)
var stunde := "%02d:00" % int(Game.slots(human_id)[nr][0])
_leeren("%s um %s" % [h["name"], stunde])
if Ursachen.ist_blocker(grund):
var b: Dictionary = Ursachen.BLOCKER[grund]
_text("Sie %s.\n[b]Auslöser:[/b] %s\n[b]Belohnung:[/b] %s" %
[b["name"], b["ausloeser"], Ursachen.belohnung_text(str(b["belohnung"]))], 16)
else:
_text("Hier ist Platz für eine neue Routine.", 16)
_trenner()
var sys := Game.system_in(human_id, nr)
if not sys.is_empty():
_text("[color=#88ff99]Hier steht: %s[/color]" %
TechDB.by_id(str(sys["tech"]))["name"], 16)
_knopf("Routine wieder herausnehmen", func() -> void:
Game.system_entfernen(human_id, nr)
systeme_panel(human_id))
_trenner()
for id in TechDB.ids():
if not Game.knows(id):
continue
var pruef := Game.system_pruefen(human_id, nr, id)
var t := TechDB.by_id(id)
var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(id)]
if not pruef["geht"]:
beschriftung += " — " + str(pruef["grund"])
elif float(pruef["guete"]) > Game.GUETE_NEU:
beschriftung += " — passt genau"
_knopf(beschriftung, func() -> void: _system_setzen(human_id, nr, id),
bool(pruef["geht"]))
_trenner()
_zeigen("slot", zurueck)
func _system_setzen(human_id: String, nr: int, tech_id: String) -> void:
var res := Game.system_setzen(human_id, nr, tech_id, false)
if not res["ok"]:
Game.notified.emit(str(res["grund"]), Color(1, 0.6, 0.4))
return
if not res["erfolg"]:
Game.notified.emit("%s greift diesmal nicht. Später noch einmal." %
TechDB.by_id(tech_id)["name"], Color(1, 0.6, 0.5))
systeme_panel(human_id)
# -------------------------------------------------------- Lehrorte
## Jeder Lehrort unterrichtet genau einen Weg: der Hörsaal den ersten, jedes
## Haus den, den sein Bewohner mitgebracht hat.
func lehre_panel(ort: String = "hoersaal") -> void:
if not DreamDB.PLACES.has(ort):
return
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "lehre" else 0
var pl: Dictionary = DreamDB.PLACES[ort]
var ids: Array = DreamDB.techniken_von(ort)
_leeren(str(pl["name"]))
_text("[color=#aaaaaa]Gelernt: [b]%d[/b] von %d entdeckten Wegen · %d im Spiel[/color]" %
[Game.known.size(), Game.offene_techniken(), TechDB.LIST.size()], 15)
_trenner()
if ids.is_empty():
_text("[color=#ffcc66]Hier wird nichts gelehrt.[/color]", 16)
for id in ids:
var t := TechDB.by_id(id)
if Game.knows(id):
_text("[b][color=#ffe3a0]%s[/color][/b] · Stufe %d (%s)" %
[t["name"], Game.skill_of(id), Game.xp_to_next(id)], 18)
else:
_text("[b][color=#9aa0b8]%s[/color][/b] · noch nicht gelernt" % t["name"], 18)
_text(_hinweis_block(t), 15)
if not Game.knows(id):
_knopf("Diesen Weg lernen", func() -> void:
if Game.learn(id):
lehre_panel(ort))
_trenner()
_naechster_weg()
_zeigen("lehre")
_scroll_wunsch = scroll_alt
_scroll_frames = 4
## Was als Nächstes ins Dorf käme.
func _naechster_weg() -> void:
var offen_zahl := Game.offene_techniken()
if offen_zahl >= TechDB.LIST.size():
_text("[color=#88ff99]Alle zwölf Wege sind entdeckt. Das Dorf wächst weiter.[/color]", 15)
return
var naechste: Dictionary = TechDB.LIST[offen_zahl]
_text("[color=#8e93a8]Noch unentdeckt: %s +%d[/color]" %
[naechste["name"], TechDB.LIST.size() - offen_zahl - 1], 14)
# -------------------------------------------------------- Schwarzes Brett
## Hier hängen die Menschen aus, deren Traum feststeckt.
func brett_panel() -> void:
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "brett" else 0
_leeren("Schwarzes Brett")
_knopf("Aushang schreiben", func() -> void: schreiben_panel())
_trenner()
var offen_ids := Game.offene_missionen()
var gezeigt := 0
var verdeckt := 0
for h in HumanDB.LIST:
var id: String = h["id"]
if Game.completed.has(id):
continue
if not offen_ids.has(id):
verdeckt += 1
continue
gezeigt += 1
var marke := ""
var farbe := C_GESEHEN
if Game.active_mission == id:
marke = " · aktiv %d %%" % int(Game.mission_progress(id))
farbe = C_AKZENT
elif Game.angenommen(id):
marke = " · %d %%" % int(Game.mission_progress(id))
if bool(h.get("eigen", false)):
farbe = C_WARN
_karte("%s · %s%s" % [h["name"], h["traum"], marke], farbe,
func() -> void: aushang_panel(id))
if gezeigt == 0:
_text("[color=#ffcc66]Nichts passt — im Hörsaal einen Weg lernen.[/color]", 16)
if verdeckt > 0:
_text("[color=#aaaaaa]%d verdeckt[/color]" % verdeckt, 14)
_zeigen("brett")
_scroll_wunsch = scroll_alt
_scroll_frames = 4
## Ein Aushang im Ganzen -- erst hier steht, was im Weg steht.
func aushang_panel(human_id: String) -> void:
var h := HumanDB.by_id(human_id)
_leeren(str(h["name"]))
_text("[color=#aaaaaa]%d — %s[/color]" % [h["alter"], h["beruf"]], 15)
_text("[b]Traum:[/b] %s" % h["traum"], 16)
_text("[b]Was im Weg steht:[/b] %s" % h["problem"], 15)
_trenner()
if Game.active_mission == human_id:
_text("[color=#ffdd77]Aktive Mission — %d %%[/color]" %
int(Game.mission_progress(human_id)), 15)
else:
if Game.angenommen(human_id):
_text("[color=#aaaaaa]Zurückgestellt — %d %%[/color]" %
int(Game.mission_progress(human_id)), 15)
_knopf("Mission annehmen", func() -> void:
Game.accept_mission(human_id)
brett_panel())
if bool(h.get("eigen", false)):
_knopf_leise("Aushang abnehmen", func() -> void:
Aushaenge.loeschen(human_id)
Game.welt_erzeugen()
brett_panel())
_zeigen("aushang", brett_panel)
# -------------------------------------------------------- Aushang schreiben
## Ein eigener Mensch: Name, Alter, Beruf, Traum, Wörter, Geschichte.
## Die Wörter sind das Einzige, was die Mechanik liest.
var _entwurf: Dictionary = {}
func schreiben_panel(entwurf: Dictionary = {}) -> void:
if not entwurf.is_empty():
_entwurf = entwurf
elif _entwurf.is_empty():
_entwurf = {"name": "", "alter": 30, "beruf": str(HumanDB.BERUFE[0]),
"traum": "", "geschichte": "", "woerter": []}
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "schreiben" else 0
_leeren("Aushang schreiben")
var spalten := HBoxContainer.new()
spalten.add_theme_constant_override("separation", 22)
_content.add_child(spalten)
var links := VBoxContainer.new()
links.custom_minimum_size.x = 400
links.add_theme_constant_override("separation", 8)
spalten.add_child(links)
var rechts := VBoxContainer.new()
rechts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
rechts.add_theme_constant_override("separation", 6)
spalten.add_child(rechts)
_ziel = links
_zeile("Name", _eingabe("name"))
_zeile("Alter", _zahl("alter", 14, 99))
_zeile("Beruf", _auswahl("beruf", HumanDB.BERUFE))
_zeile("Traum", _eingabe("traum"))
_text("[b]Geschichte[/b] [color=#8e93a8]was im Weg steht[/color]", 14)
_textfeld("geschichte")
_trenner()
_vorschau = _text("", 14)
_trenner()
var reihe := HBoxContainer.new()
reihe.add_theme_constant_override("separation", 8)
links.add_child(reihe)
_reihenknopf(reihe, "Zufall", func() -> void:
schreiben_panel(HumanDB.zufallsprofil()))
_reihenknopf(reihe, "Speichern", func() -> void: _aushang_speichern())
_ziel = rechts
var w: Array = _entwurf["woerter"]
_woerter_kopf = _text("", 14)
for gruppe in Woerter.GRUPPEN:
_text("[color=#9fb6ff]%s[/color]" % Woerter.GRUPPEN[gruppe], 13)
_haken_raster(Woerter.in_gruppe(gruppe))
_ziel = null
_vorschau_aktualisieren()
_zeigen("schreiben", brett_panel)
_scroll_wunsch = scroll_alt
_scroll_frames = 4
var _vorschau: RichTextLabel
var _woerter_kopf: RichTextLabel
## Was aus den Wörtern wird -- live, während man ankreuzt.
func _vorschau_aktualisieren() -> void:
if _vorschau == null or not is_instance_valid(_vorschau):
return
var w: Array = _entwurf["woerter"]
_woerter_kopf.text = "[b]Wörter[/b] [color=#8e93a8]mindestens %d · %d gewählt[/color]" % \
[Woerter.MINDESTENS, w.size()]
if w.is_empty():
_vorschau.text = "[color=#8e93a8]Kreuze an, was auf sie oder ihn zutrifft — daraus entsteht der Mensch.[/color]"
return
var bild := Woerter.profil(w)
var urs: Array = []
for u in bild["ursachen"]:
urs.append(str(Ursachen.LISTE[u]["name"]))
var prob: Array = []
for pr in bild["probleme"]:
prob.append(Probleme.name_von(str(pr)))
var zuege: Array = []
var namen := {"einrichten": "offen", "guete": "beständig", "abwehr": "stur",
"momente": "reizbar", "dauer": "lang", "tempo": "treibt", "sozial": "gesellig",
"sichtbar": "durchschaubar"}
for v in Woerter.VERHALTEN:
var wert := int(bild["verhalten"][v])
if wert != 0:
zuege.append("%s %s" % [namen[v], "+" if wert > 0 else "−"])
_vorschau.text = "[color=#ff9a8a]%s[/color]\n[color=#ffcc66]%s[/color] · %d Gewohnheiten im Tag\n[color=#8e93a8]%s[/color]" % \
[" · ".join(urs), " · ".join(prob), (bild["blocker"] as Array).size(),
" ".join(zuege) if not zuege.is_empty() else "unauffällig"]
func _aushang_speichern() -> void:
var fehler := Aushaenge.pruefen(_entwurf)
if not fehler.is_empty():
Game.notified.emit("Aushang unvollständig: %s" % ", ".join(fehler), Color(1, 0.7, 0.5))
return
var id := Aushaenge.speichern(_entwurf)
Game.welt_erzeugen()
Game.notified.emit("Aushang hängt: %s" % _entwurf["name"], Color(0.6, 1, 0.7))
_entwurf = {}
_verlauf.clear()
aushang_panel(id)
# ---- Eingabehelfer
func _zeile(titel: String, widget: Control) -> void:
var hb := HBoxContainer.new()
hb.add_theme_constant_override("separation", 10)
var l := Label.new()
l.text = titel
l.custom_minimum_size.x = 90
l.add_theme_font_size_override("font_size", 15)
hb.add_child(l)
widget.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hb.add_child(widget)
_wo().add_child(hb)
func _eingabe(feld: String) -> LineEdit:
var e := LineEdit.new()
e.text = str(_entwurf.get(feld, ""))
e.add_theme_font_size_override("font_size", 15)
e.text_changed.connect(func(t: String) -> void: _entwurf[feld] = t)
return e
func _zahl(feld: String, von: int, bis: int) -> SpinBox:
var s := SpinBox.new()
s.min_value = von
s.max_value = bis
s.value = int(_entwurf.get(feld, von))
s.value_changed.connect(func(v: float) -> void: _entwurf[feld] = int(v))
return s
func _auswahl(feld: String, optionen: Array) -> OptionButton:
var o := OptionButton.new()
for i in range(optionen.size()):
o.add_item(str(optionen[i]), i)
if str(optionen[i]) == str(_entwurf.get(feld, "")):
o.select(i)
o.item_selected.connect(func(i: int) -> void: _entwurf[feld] = str(optionen[i]))
return o
func _textfeld(feld: String) -> TextEdit:
var t := TextEdit.new()
t.text = str(_entwurf.get(feld, ""))
t.custom_minimum_size = Vector2(0, 80)
t.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
t.add_theme_font_size_override("font_size", 15)
t.text_changed.connect(func() -> void: _entwurf[feld] = t.text)
_wo().add_child(t)
return t
func _haken_raster(keys: Array) -> void:
var g := GridContainer.new()
g.columns = 3
g.add_theme_constant_override("h_separation", 4)
_wo().add_child(g)
var gewaehlt: Array = _entwurf["woerter"]
for key in keys:
var c := CheckBox.new()
c.text = Woerter.name_von(str(key))
c.button_pressed = gewaehlt.has(key)
c.add_theme_font_size_override("font_size", 14)
c.toggled.connect(func(an: bool) -> void:
var liste: Array = _entwurf["woerter"]
if an and not liste.has(key):
liste.append(key)
elif not an:
liste.erase(key)
_vorschau_aktualisieren())
g.add_child(c)
func _reihenknopf(reihe: HBoxContainer, text: String, cb: Callable) -> void:
var b := Button.new()
b.text = text
b.custom_minimum_size = Vector2(0, 32)
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
b.add_theme_font_size_override("font_size", 16)
b.pressed.connect(cb)
reihe.add_child(b)
# -------------------------------------------------------- Vor Ort
## Draußen am Menschen: hier greift man in den Slot ein, der gerade läuft.
func anwenden_panel(mensch: Node3D) -> void:
_apply_human = mensch
var h: Dictionary = mensch.data
var id: String = str(h["id"])
var nr: int = int(mensch.slot)
_leeren("%s — %s" % [h["name"], Orte.text(mensch.activity)])
_text("[b]Traum:[/b] %s" % h["traum"], 16)
if Game.active_mission != id:
_text("[color=#ffcc66]Keine aktive Mission für diesen Menschen.[/color]", 15)
_zeigen("anwenden")
return
var grund := Game.slot_grund(id, nr)
if not Game.erkannt(id, nr):
_text("[color=#ffcc66]Noch nicht notiert — [b]R[/b] im Moment.[/color]", 16)
_zeigen("anwenden")
return
if Ursachen.ist_blocker(grund):
var b: Dictionary = Ursachen.BLOCKER[grund]
_text("Sie %s. [b]Auslöser:[/b] %s · [b]Belohnung:[/b] %s" %
[b["name"], b["ausloeser"], Ursachen.belohnung_text(str(b["belohnung"]))], 15)
_trenner()
if Game.known.is_empty():
_text("[color=#ffcc66]Du kennst keine Technik.[/color]")
_zeigen("anwenden")
return
for tid in TechDB.ids():
if not Game.knows(tid):
continue
var pruef := Game.system_pruefen(id, nr, tid)
var t := TechDB.by_id(tid)
var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(tid)]
if not pruef["geht"]:
beschriftung += " — " + str(pruef["grund"])
elif mensch.resist_left > 0.0:
beschriftung += " — macht gerade dicht"
_knopf(beschriftung, func() -> void: _vor_ort_setzen(tid), bool(pruef["geht"]))
_zeigen("anwenden")
func _vor_ort_setzen(tech_id: String) -> void:
if _apply_human == null or not is_instance_valid(_apply_human):
schliessen()
return
var mensch := _apply_human
var id: String = str(mensch.data["id"])
var res := Game.system_setzen(id, int(mensch.slot), tech_id, mensch.resist_left > 0.0)
schliessen()
if not res["ok"]:
Game.notified.emit(str(res["grund"]), Color(1, 0.6, 0.4))
return
var t := TechDB.by_id(tech_id)
if res["erfolg"]:
mensch.reagiere(true, "%s\nsteht" % t["name"])
else:
mensch.reagiere(false, "%s greift nicht" % t["name"])
func _unhandled_input(event: InputEvent) -> void:
if offen and event.is_action_pressed("ui_cancel"):
_zurueck()
get_viewport().set_input_as_handled()