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 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_BLOCKER := Color(0.84, 0.42, 0.35) const C_SYSTEM := Color(0.44, 0.78, 0.50) 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 · Esc" steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT) steuerung.position = Vector2(-260, 16) steuerung.size = Vector2(240, 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.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 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 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 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" % \ ("" if offen_std == 0 else " · %d Stunden unbekannt" % offen_std) 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() _scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED _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.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", "systeme": return _drawer "slot", "situation", "anwenden": return _karte_host return _modal func _process(_delta: float) -> void: if not offen: _hud_aktualisieren() 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) _ablauf_aktualisieren() # ------------------------------------------------------------------ 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 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", "Missionen (%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: systeme_panel(id)) # ---------------------------------------------- Diagnose und Systeme ## Der Traum scheitert an Ursachen. Die stehen nicht im Panel -- man findet sie, ## indem man dabeisteht. Gegen jede gefundene Ursache baut man ein System in ## den Tag. Je mehr abgedeckt ist, desto schneller kommt der Traum voran. func systeme_panel(human_id: String) -> void: var scroll_alt := _scroll.scroll_vertical if offen and _kind == "systeme" else 0 var h := HumanDB.by_id(human_id) _leeren("%s — Tagesablauf" % h["name"]) _text("%d %% · +%d %% je Tag · [color=#aaaaaa]%s[/color]" % [int(Game.mission_progress(human_id)), int(round(Game.traum_tempo(human_id))), h["traum"]], 16) _trenner() _text("[b]Woran es scheitert[/b]", 17) var offene := 0 for u in Game.ursachen_von(human_id): var name: String = str(u) if not Game.ursache_bekannt(human_id, name): _text("[color=#8e93a8]? — noch nicht erkannt[/color]", 15) offene += 1 continue var g := Game.ursache_geloest(human_id, name) var stand := "[color=#88ff99]gelöst[/color]" if g > 0.0 else \ "[color=#ff9977]offen · bremst auf %d %%[/color]" % \ int(float(Ursachen.LISTE[name]["faktor"]) * 100.0) _text("[b]%s[/b] %s" % [Ursachen.LISTE[name]["name"], stand], 15) if offene > 0: _trenner() _tagesablauf(human_id) _zeigen("systeme", journal_panel.bind("missionen")) _scroll_wunsch = scroll_alt _scroll_frames = 4 ## 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]%s" % ("" if offen == 0 else " [color=#8e93a8]%d Stunden leer[/color]" % 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: 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" _karte("%s %s" % [spanne, text], _ablauf_farbe(human_id, nr), func() -> void: slot_panel(human_id, nr, systeme_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. Ein neues System passt in diesen Slot.", 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("System 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 Blocker 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()