Files
dreamer-game/scripts/ui.gd
2026-09-15 23:17:07 +02:00

947 lines
31 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
signal panel_opened
signal panel_closed
const BREITE := 900
var offen := false
var _kind := ""
var _hud: RichTextLabel
var _hinweis: Label
var _log: VBoxContainer
var _ablauf: HBoxContainer
var _ablauf_jetzt: RichTextLabel
var _ablauf_grund: ColorRect
var _zellen: Array = []
var _ticks: Array = []
var _dim: ColorRect
var _panel: PanelContainer
var _titel: Label
var _content: VBoxContainer
var _scroll: ScrollContainer
var _apply_human: Node3D
var _journal_seite := "techniken"
var _scroll_wunsch := 0
var _scroll_frames := 0
func _ready() -> void:
layer = 10
_baue_hud()
_baue_panel()
Game.notified.connect(_notiz)
Game.state_changed.connect(_hud_aktualisieren)
_hud_aktualisieren()
func _baue_hud() -> void:
_hud = RichTextLabel.new()
_hud.bbcode_enabled = true
_hud.fit_content = true
_hud.scroll_active = false
_hud.set_anchors_preset(Control.PRESET_TOP_LEFT)
_hud.position = Vector2(18, 14)
_hud.custom_minimum_size = Vector2(420, 130)
_hud.size = Vector2(420, 130)
_hud.mouse_filter = Control.MOUSE_FILTER_IGNORE
_hud.add_theme_font_size_override("normal_font_size", 17)
_hud.add_theme_font_size_override("bold_font_size", 17)
add_child(_hud)
_baue_ablauf()
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
add_child(steuerung)
_hinweis = Label.new()
_hinweis.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
_hinweis.position = Vector2(-300, -110)
_hinweis.size = Vector2(600, 30)
_hinweis.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_hinweis.add_theme_font_size_override("font_size", 19)
_hinweis.add_theme_color_override("font_color", Color(1, 0.95, 0.75))
_hinweis.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
_hinweis.add_theme_constant_override("outline_size", 6)
_hinweis.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_hinweis)
_log = VBoxContainer.new()
_log.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
_log.position = Vector2(-520, -230)
_log.size = Vector2(500, 200)
_log.alignment = BoxContainer.ALIGNMENT_END
_log.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_log)
## Der Tagesablauf im HUD: 24 Felder, eines je Stunde. Kein Titel, keine
## Erklärung -- der Streifen erklärt sich, sobald man eine Stunde gefüllt hat.
const ABLAUF_X := 18.0
const ABLAUF_Y := 74.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.38)
_ablauf_grund.set_anchors_preset(Control.PRESET_TOP_LEFT)
_ablauf_grund.position = Vector2(ABLAUF_X - 8.0, ABLAUF_Y - 6.0)
_ablauf_grund.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_ablauf_grund)
_ablauf = HBoxContainer.new()
_ablauf.set_anchors_preset(Control.PRESET_TOP_LEFT)
_ablauf.position = Vector2(ABLAUF_X, ABLAUF_Y)
_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, 14)
zelle.mouse_filter = Control.MOUSE_FILTER_IGNORE
_ablauf.add_child(zelle)
_zellen.append(zelle)
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) - 2.0,
ABLAUF_Y + 18.0)
tick.add_theme_font_size_override("font_size", 10)
tick.add_theme_color_override("font_color", Color(1, 1, 1, 0.35))
tick.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(tick)
_ticks.append(tick)
_ablauf_jetzt = RichTextLabel.new()
_ablauf_jetzt.bbcode_enabled = true
_ablauf_jetzt.fit_content = true
_ablauf_jetzt.scroll_active = false
_ablauf_jetzt.set_anchors_preset(Control.PRESET_TOP_LEFT)
_ablauf_jetzt.position = Vector2(ABLAUF_X, ABLAUF_Y + 34.0)
_ablauf_jetzt.custom_minimum_size = Vector2(breite, 20)
_ablauf_jetzt.size = Vector2(breite, 20)
_ablauf_jetzt.mouse_filter = Control.MOUSE_FILTER_IGNORE
_ablauf_jetzt.add_theme_font_size_override("normal_font_size", 14)
_ablauf_jetzt.add_theme_font_size_override("bold_font_size", 14)
add_child(_ablauf_jetzt)
## Farbe je Stunde: unbekannt, gesehen, ungeklärt, benannt, gebaut.
func _ablauf_farbe(human_id: String, nr: int) -> Color:
match Game.slot_zustand(human_id, nr):
Game.Stunde.UNBEKANNT:
return Color(0.22, 0.23, 0.30)
Game.Stunde.GEBAUT:
return Color(0.44, 0.78, 0.50)
Game.Stunde.UNGEKLAERT:
return Color(0.85, 0.66, 0.32)
Game.Stunde.BENANNT:
return Color(0.84, 0.42, 0.35)
Game.Stunde.SCHLAF:
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_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)
for stunde in range(24):
var f := _ablauf_farbe(id, Game.slot_bei_stunde(id, stunde))
var zelle: ColorRect = _zellen[stunde]
zelle.color = f.lightened(0.55) if stunde == jetzt else f
zelle.custom_minimum_size.y = 18 if stunde == jetzt else 14
# Der offene Moment verdrängt die Stundenzeile -- er ist gleich vorbei
var zeilen: Array = []
if Game.situation_bei(id):
var s: Dictionary = Probleme.SITUATIONEN[Game.situation["key"]]
zeilen.append("[color=#ffcc66][b]» %s «[/b][/color] [b]%.0f s[/b]" %
[s["name"], float(Game.situation["rest"])])
else:
zeilen.append("[color=#cfd6ff]%02d[/color] %s" %
[jetzt, _stunden_text(id, Game.slot_bei_stunde(id, jetzt))])
var offen: Array = []
for p in Game.probleme_von(id):
var key := str(p)
if not Game.problem_bekannt(id, key):
continue
if Game.problem_ueberwunden(id, key):
offen.append("[color=#88ff99]%s ✓[/color]" % Probleme.name_von(key))
else:
offen.append("[color=#ffb0a0]%s %d/%d[/color]" % [Probleme.name_von(key),
Game.problem_fortschritt(id, key), Game.PROBLEM_UEBERWINDEN])
if not offen.is_empty():
zeilen.append(" ".join(offen))
_ablauf_jetzt.text = "\n".join(zeilen)
var hoehe := 62.0 + (20.0 if zeilen.size() > 1 else 0.0)
_ablauf_grund.size = Vector2(24.0 * (ZELLE_BREIT + ZELLE_LUECKE) + 14.0, hoehe)
func _baue_panel() -> void:
_dim = ColorRect.new()
_dim.color = Color(0, 0, 0, 0.62)
_dim.set_anchors_preset(Control.PRESET_FULL_RECT)
_dim.visible = false
add_child(_dim)
_panel = PanelContainer.new()
_panel.set_anchors_preset(Control.PRESET_CENTER)
_panel.custom_minimum_size = Vector2(BREITE, 200)
_panel.visible = false
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.10, 0.09, 0.16, 0.98)
sb.border_color = Color(0.55, 0.70, 1.0, 0.8)
sb.set_border_width_all(2)
sb.set_corner_radius_all(10)
sb.set_content_margin_all(22)
_panel.add_theme_stylebox_override("panel", sb)
add_child(_panel)
var vb := VBoxContainer.new()
vb.add_theme_constant_override("separation", 10)
_panel.add_child(vb)
_titel = Label.new()
_titel.add_theme_font_size_override("font_size", 26)
_titel.add_theme_color_override("font_color", Color(1, 0.93, 0.75))
vb.add_child(_titel)
_scroll = ScrollContainer.new()
_scroll.custom_minimum_size = Vector2(BREITE - 44, 140)
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
vb.add_child(_scroll)
var scroll := _scroll
_content = VBoxContainer.new()
_content.custom_minimum_size.x = BREITE - 70
_content.add_theme_constant_override("separation", 10)
scroll.add_child(_content)
func _process(_delta: float) -> void:
if not offen:
_hud_aktualisieren()
else:
var wunsch := _content.get_combined_minimum_size().y
var platz := get_viewport().get_visible_rect().size.y - 220.0
_scroll.custom_minimum_size.y = clampf(wunsch, 120.0, platz)
_panel.reset_size() # sonst behält der Rahmen die Höhe des Panels davor
if _scroll_frames > 0:
_scroll.scroll_vertical = _scroll_wunsch
_scroll_frames -= 1
_panel.position = (get_viewport().get_visible_rect().size - _panel.size) * 0.5
# ------------------------------------------------------------------ HUD
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)
# ------------------------------------------------------------------ Panel
func _leeren(titel: String) -> void:
for c in _content.get_children():
_content.remove_child(c)
c.queue_free()
_titel.text = titel
func _text(bb: String, groesse: int = 17) -> RichTextLabel:
var r := RichTextLabel.new()
r.bbcode_enabled = true
r.fit_content = true
r.scroll_active = false
r.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
r.custom_minimum_size.x = BREITE - 80
r.text = bb
r.add_theme_font_size_override("normal_font_size", groesse)
r.add_theme_font_size_override("bold_font_size", groesse)
_content.add_child(r)
return r
func _knopf(beschriftung: String, cb: Callable, aktiv: bool = true) -> Button:
var b := Button.new()
b.text = beschriftung
b.disabled = not aktiv
b.custom_minimum_size = Vector2(0, 32)
b.add_theme_font_size_override("font_size", 16)
_content.add_child(b)
if aktiv:
b.pressed.connect(cb)
return b
func _trenner() -> void:
var s := HSeparator.new()
_content.add_child(s)
func _zeigen(kind: String) -> void:
_kind = kind
offen = true
_dim.visible = true
_panel.visible = true
_panel.reset_size()
panel_opened.emit()
func schliessen() -> void:
if not offen:
return
offen = false
_kind = ""
_apply_human = null
_dim.visible = false
_panel.visible = false
panel_closed.emit()
func _hinweis_block(t: Dictionary) -> String:
return ("[b]Was:[/b] %s\n[b]Wann:[/b] %s\n[b]Warum:[/b] %s\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)
_knopf("%s · Stufe %d · %s" % [t["name"], Game.skill_of(id),
Ursachen.LISTE[t["ursache"]]["name"]],
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)
_trenner()
_knopf("Journal", func() -> void: journal_panel("techniken"))
_zeigen("technik")
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))
if Game.completed.has(id):
stand = "★ erfüllt"
elif Game.active_mission == id:
stand = "aktiv · %d %%" % int(Game.mission_progress(id))
_knopf("%s · %s" % [h["name"], stand],
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)
_trenner()
_knopf("Journal", func() -> void: journal_panel("missionen"))
_zeigen("systeme")
_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"
_knopf("%s %s" % [spanne, text],
func() -> void: slot_panel(human_id, nr), 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) -> 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()
_knopf("Zurück", func() -> void: systeme_panel(human_id))
_zeigen("slot")
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 := ""
if Game.active_mission == id:
marke = " · aktiv %d %%" % int(Game.mission_progress(id))
elif Game.angenommen(id):
marke = " · %d %%" % int(Game.mission_progress(id))
_knopf("%s · %s%s" % [h["name"], h["traum"], marke],
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("Aushang abnehmen", func() -> void:
Aushaenge.loeschen(human_id)
Game.welt_erzeugen()
brett_panel())
_knopf("Zurück", func() -> void: brett_panel())
_zeigen("aushang")
# -------------------------------------------------------- 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")
_zeile("Name", _eingabe("name"))
_zeile("Alter", _zahl("alter", 14, 99))
_zeile("Beruf", _auswahl("beruf", HumanDB.BERUFE))
_zeile("Traum", _eingabe("traum"))
_trenner()
var w: Array = _entwurf["woerter"]
_text("[b]Wörter[/b] [color=#8e93a8]mindestens %d · %d gewählt[/color]" %
[Woerter.MINDESTENS, w.size()], 15)
for gruppe in Woerter.GRUPPEN:
_text("[color=#9fb6ff]%s[/color]" % Woerter.GRUPPEN[gruppe], 13)
_haken_raster(Woerter.in_gruppe(gruppe))
_trenner()
_text("[b]Geschichte[/b] [color=#8e93a8]was im Weg steht[/color]", 15)
_textfeld("geschichte")
_trenner()
var reihe := HBoxContainer.new()
reihe.add_theme_constant_override("separation", 8)
_content.add_child(reihe)
_reihenknopf(reihe, "Zufall", func() -> void:
schreiben_panel(HumanDB.zufallsprofil()))
_reihenknopf(reihe, "Speichern", func() -> void: _aushang_speichern())
_reihenknopf(reihe, "Zurück", func() -> void:
_entwurf = {}
brett_panel())
_zeigen("schreiben")
_scroll_wunsch = scroll_alt
_scroll_frames = 4
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 = {}
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)
_content.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)
_content.add_child(t)
return t
func _haken_raster(keys: Array) -> void:
var g := GridContainer.new()
g.columns = 4
g.add_theme_constant_override("h_separation", 6)
_content.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))
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"):
schliessen()
get_viewport().set_input_as_handled()