extends Node ## Globaler Spielzustand: Wissen, Fertigkeiten, Missionen, Uhrzeit. signal state_changed signal notified(text: String, color: Color) signal mission_done(human_id: String) const DAY_SECONDS := 360.0 # eine Erdenrunde in Echtzeit-Sekunden const XP_LEVELS := [0, 4, 10, 18, 28] const MISSION_ZIEL := 100.0 const RESIST_SEKUNDEN := 18.0 ## Drei Stufen wie in Die Sims: normal, schnell, sehr schnell. Die Stufe ist ## nicht der Faktor — Stufe 2 läuft dreifach, Stufe 3 siebenfach. const TEMPO_FAKTOR := [1.0, 3.0, 7.0] const TECHNIK_COOLDOWN := 30.0 # eine Technik braucht Luft, bevor sie wieder greift # Der Verlust ist mit Absicht so hoch, dass sich nur eine Technik lohnt, die # Thema UND Moment trifft. Wer eine Regel blind auf jede Tätigkeit hängt, # verliert unterm Strich — die Wahl muss aus der Technik gelesen werden. var known: Dictionary = {} # tech_id -> xp var active_mission: String = "" # human_id oder "" var progress: Dictionary = {} # human_id -> 0..100 var cooldown: Dictionary = {} # tech_id -> Restsekunden var systeme: Dictionary = {} # human_id -> {slot -> {tech, guete}} var erkannt_slots: Dictionary = {} # human_id -> {slot -> true} var gesehen: Dictionary = {} # human_id -> {slot -> true}, wo man dabei war var notiz_sperre: float = 0.0 # nach einem Fehlgriff kurz gesperrt var situation: Dictionary = {} # laufende Situation: mensch, key, rest var situation_pause: float = 0.0 # bis die nächste kommen darf var probleme_bekannt: Dictionary = {} # human_id -> {problem -> true} var problem_stand: Dictionary = {} # "human:problem" -> gelungene Anwendungen var ueberwunden: Dictionary = {} # human_id -> {problem -> true} var completed: Array = [] var time_h: float = 7.0 var day: int = 1 var teleport_lock: float = 0.0 var tempo: int = 1 # 1, 2 oder 3 -- vom Spieler gewählt func _ready() -> void: _setup_input() welt_erzeugen() _startweg_lernen() ## Den ersten Weg bringt das Wesen schon mit. Ohne ihn hinge am Schwarzen Brett ## nichts aus, und der erste Gang wäre eine Pflichtübung. func _startweg_lernen() -> void: for id in DreamDB.techniken_von("hoersaal"): if not known.has(str(id)): known[str(id)] = 0 func _setup_input() -> void: var binds := { "vor": [KEY_W, KEY_UP], "zurueck": [KEY_S, KEY_DOWN], "links": [KEY_A, KEY_LEFT], "rechts": [KEY_D, KEY_RIGHT], "steigen": [KEY_SPACE], "sinken": [KEY_SHIFT], "interagieren": [KEY_E], "folgen": [KEY_F], "notieren": [KEY_R], "journal": [KEY_J, KEY_TAB], "tempo_1": [KEY_1, KEY_KP_1], "tempo_2": [KEY_2, KEY_KP_2], "tempo_3": [KEY_3, KEY_KP_3], } for action in binds: if not InputMap.has_action(action): InputMap.add_action(action) for code in binds[action]: var ev := InputEventKey.new() ev.physical_keycode = code InputMap.action_add_event(action, ev) ## Schläft der Mensch, dem du gerade hilfst? Dann ist ohnehin nichts zu holen. func schlaeft_ziel() -> bool: if active_mission == "": return false var h := HumanDB.by_id(active_mission) if h.is_empty(): return false var r: Array = h["routine"] var cur: Array = r[r.size() - 1] for e in r: if time_h >= float(e[0]): cur = e return not erreichbar(str(cur[2])) ## Um wie viel schneller läuft die Welt gerade? Über der Nacht des Menschen ## läuft sie immer auf der höchsten Stufe -- warten bringt dort nichts. func zeit_faktor() -> float: return faktor_von(3 if schlaeft_ziel() else tempo) static func faktor_von(stufe: int) -> float: return float(TEMPO_FAKTOR[clampi(stufe, 1, 3) - 1]) func tempo_setzen(stufe: int) -> void: var neu := clampi(stufe, 1, 3) if neu == tempo: return tempo = neu notified.emit("Tempo %d — %d×" % [tempo, int(faktor_von(tempo))], Color(0.8, 0.9, 1)) state_changed.emit() func advance_time(delta: float) -> void: for id in cooldown.keys(): var rest: float = float(cooldown[id]) - delta if rest <= 0.0: cooldown.erase(id) else: cooldown[id] = rest time_h += delta * 24.0 / DAY_SECONDS while time_h >= 24.0: time_h -= 24.0 day += 1 func time_string() -> String: var h := int(time_h) var m := int((time_h - h) * 60.0) return "Tag %d · %02d:%02d" % [day, h, m] # ------------------------------------------------------------ Wachsende Welt # Das Traumdorf wird erzeugt. Fest sind nur Hörsaal, Dorfplatz und Tor. # Jeder erfüllte Traum bringt einen Bewohner mit seinem Haus, und jedes Haus # lehrt eine Technik, die es vorher nirgends gab. const INSEL_GRUND := 46.0 func haeuser() -> int: return completed.size() func insel_radius() -> float: return maxf(INSEL_GRUND, DreamDB.aussenradius() + 12.0) ## Baut Traumdorf und Menschheit auf den Stand der erfüllten Träume. func welt_erzeugen() -> void: DreamDB.erzeuge(haeuser()) HumanDB.erzeuge() ## Das Traumdorf beginnt leer. Mit jedem erfüllten Traum zieht ein Wesen ein. func wesen_eingezogen(index: int) -> bool: return index < haeuser() ## Steht dieser Ort schon auf der Insel? Erzeugt wird nur, was schon da ist. func ort_offen(place_key: String) -> bool: return DreamDB.PLACES.has(place_key) ## Ist die Technik überhaupt schon irgendwo zu finden? func technik_offen(tech_id: String) -> bool: var ort := DreamDB.ort_von_technik(tech_id) return ort != "" and ort_offen(ort) func offene_techniken() -> int: var n := 0 for t in TechDB.LIST: if technik_offen(t["id"]): n += 1 return n # ------------------------------------------------------------------- Cooldown # Jede Anwendung sperrt ihre Technik für eine Weile. Wer einen ganzen Tag # abdecken will, braucht deshalb mehrere Wege — einer allein kommt nicht nach. func cooldown_rest(tech_id: String) -> float: return float(cooldown.get(tech_id, 0.0)) func bereit(tech_id: String) -> bool: return cooldown_rest(tech_id) <= 0.0 # ---------------------------------------------------------------- Fertigkeiten # Gelernt wird an einem Lehrort, besser wird eine Technik nur durch Anwendung. # Es gibt keinen Weg, sie am Schreibtisch zu trainieren. func knows(tech_id: String) -> bool: return known.has(tech_id) func skill_of(tech_id: String) -> int: if not known.has(tech_id): return 0 var xp: int = known[tech_id] var lvl := 1 for i in range(XP_LEVELS.size()): if xp >= XP_LEVELS[i]: lvl = i + 1 return lvl func xp_to_next(tech_id: String) -> String: var lvl := skill_of(tech_id) if lvl >= XP_LEVELS.size(): return "Meisterschaft" var need: int = XP_LEVELS[lvl] return "%d / %d EP" % [known[tech_id], need] func learn(tech_id: String) -> bool: if known.has(tech_id): return false if not technik_offen(tech_id): notified.emit("Diesen Weg kennt im Dorf noch niemand. Erfülle einen Traum.", Color(1, 0.8, 0.5)) return false known[tech_id] = 0 var t := TechDB.by_id(tech_id) notified.emit("Technik gelernt: %s" % t["name"], Color(0.6, 1, 0.7)) state_changed.emit() return true func add_xp(tech_id: String, amount: int) -> void: if not known.has(tech_id): return var before := skill_of(tech_id) known[tech_id] = int(known[tech_id]) + amount if skill_of(tech_id) > before: var t := TechDB.by_id(tech_id) notified.emit("%s ist jetzt Stufe %d." % [t["name"], skill_of(tech_id)], Color(0.7, 0.9, 1)) state_changed.emit() # ------------------------------------------------------------------ Missionen func accept_mission(human_id: String) -> bool: if completed.has(human_id): return false if active_mission == human_id: return false active_mission = human_id if not progress.has(human_id): progress[human_id] = 0.0 var h := HumanDB.by_id(human_id) notified.emit("Mission angenommen: %s" % h["name"], Color(1, 0.9, 0.5)) state_changed.emit() return true ## Angenommen ist, wer schon einmal im Auftragsbuch stand. func angenommen(human_id: String) -> bool: return progress.has(human_id) ## Eine Mission ist machbar, wenn mindestens eine ihrer Ursachen mit einer ## gelernten Technik anzugehen ist. Die übrigen bremsen dann eben noch. func machbar(human_id: String) -> bool: for u in ursachen_von(human_id): for id in known: if str(TechDB.by_id(id)["ursache"]) == str(u): return true return false ## Die Menschen, die gerade am Brett hängen können. func offene_missionen() -> Array: var out: Array = [] for h in HumanDB.LIST: var id: String = h["id"] if completed.has(id) or not machbar(id): continue out.append(id) return out func mission_progress(human_id: String) -> float: return progress.get(human_id, 0.0) ## Ein erfüllter Traum lässt das Dorf wachsen: ein Wesen zieht mit seinem ## Haus ein, und in diesem Haus wird ein neuer Weg gelehrt. func _insel_waechst() -> void: welt_erzeugen() var i := haeuser() - 1 if i < 0 or i >= DreamDB.LIST.size(): return var w: Dictionary = DreamDB.LIST[i] notified.emit("%s, %s, ist ins Traumdorf gezogen." % [w["name"], w["rolle"]], Color(0.85, 0.75, 1)) var neu: Array = DreamDB.techniken_von(DreamDB.haus_key(i)) if neu.is_empty(): return notified.emit("In %s wird ein neuer Weg gelehrt: %s." % [DreamDB.PLACES[DreamDB.haus_key(i)]["name"], TechDB.by_id(str(neu[0]))["name"]], Color(0.75, 0.95, 1)) # --------------------------------------------------------------------- Systeme # Ein Traum scheitert an Ursachen. Die sind verdeckt und zeigen sich nur im # Tagesablauf. Wer sie gefunden hat, baut Systeme dagegen: in einen freien # Slot ein neues, oder auf einen Blocker ein besseres. Je mehr Ursachen # abgedeckt sind, desto schneller kommt der Traum voran. const BASIS_TEMPO := 45.0 # Prozent je Tag, wenn nichts mehr bremst const GUETE_ERSATZ := 1.35 # Blocker mit gleicher Belohnung ersetzt const GUETE_NEU := 1.0 # System in einen freien Slot gesetzt const NOTIZ_SPERRE := 6.0 # nach einem Fehlgriff schaut man erst wieder hin func slots(human_id: String) -> Array: var h := HumanDB.by_id(human_id) return h["routine"] if h.has("routine") else [] ## Welcher Slot deckt diese Stunde ab? Jeder Tag ist gleich, es gibt keine ## Wochentage -- der Ablauf 0-23 Uhr wiederholt sich. func slot_bei_stunde(human_id: String, stunde: int) -> int: var r := slots(human_id) if r.is_empty(): return -1 var nr := r.size() - 1 for i in range(r.size()): if float(stunde) >= float(r[i][0]): nr = i return nr ## Dabeigewesen: die Stunde ist im Ablauf nicht mehr leer. Was dort schiefläuft, ## steht damit noch nicht fest -- das muss der Spieler benennen. func gesehen_slot(human_id: String, nr: int) -> bool: return gesehen.get(human_id, {}).has(nr) func sehen(human_id: String, nr: int) -> void: if gesehen_slot(human_id, nr): return if not gesehen.has(human_id): gesehen[human_id] = {} gesehen[human_id][nr] = true state_changed.emit() ## Der Zustand einer Stunde. HUD und Panel zeichnen denselben -- zwei ## Auslegungen würden mit der Zeit auseinanderlaufen. enum Stunde { UNBEKANNT, GESEHEN, SCHLAF, UNGEKLAERT, BENANNT, GEBAUT } func slot_zustand(human_id: String, nr: int) -> int: if not gesehen_slot(human_id, nr): return Stunde.UNBEKANNT if not system_in(human_id, nr).is_empty(): return Stunde.GEBAUT var grund := slot_grund(human_id, nr) if Ursachen.ist_blocker(grund): return Stunde.BENANNT if erkannt(human_id, nr) else Stunde.UNGEKLAERT if grund == "schlaeft": return Stunde.SCHLAF return Stunde.GESEHEN func slot_grund(human_id: String, nr: int) -> String: var r := slots(human_id) return str(r[nr][2]) if nr >= 0 and nr < r.size() else "" ## Was in diesem Slot wirklich passiert: die Ersatzhandlung, wenn ein System ## steht -- sonst das, was von Haus aus dort steht. func slot_aktivitaet(human_id: String, nr: int) -> String: var sys := system_in(human_id, nr) if not sys.is_empty(): return str(TechDB.by_id(str(sys["tech"]))["ersatz"]) return slot_grund(human_id, nr) func system_in(human_id: String, nr: int) -> Dictionary: return systeme.get(human_id, {}).get(nr, {}) # ------------------------------------------------------------------- Erkennen func erkannt(human_id: String, nr: int) -> bool: if not Ursachen.ist_blocker(slot_grund(human_id, nr)): return true return erkannt_slots.get(human_id, {}).has(nr) ## Zusehen allein deckt nichts auf. Der Spieler muss den Moment benennen: ## "hier läuft etwas schief". Trifft er, steht die Ursache im Journal. ## Greift er daneben, schaut er eine Weile umsonst hin. ## Grundlage bleibt awareness training -- nur ist das Bemerken jetzt ein Akt. func notieren(human_id: String, nr: int) -> String: if notiz_sperre > 0.0: return "gesperrt" var grund := slot_grund(human_id, nr) if not Ursachen.ist_blocker(grund): notiz_sperre = NOTIZ_SPERRE notified.emit("Hier läuft nichts schief — %s ist einfach ihr Tag." % Orte.text(grund), Color(1, 0.75, 0.55)) return "daneben" if erkannt(human_id, nr): notified.emit("Das hast du schon notiert: %s." % Orte.text(grund), Color(0.8, 0.85, 1)) return "bekannt" if not erkannt_slots.has(human_id): erkannt_slots[human_id] = {} erkannt_slots[human_id][nr] = true notified.emit("Erkannt: %s — %s" % [Ursachen.BLOCKER[grund]["name"], Ursachen.LISTE[Ursachen.ursache_von(grund)]["name"]], Color(0.8, 0.95, 1)) state_changed.emit() return "treffer" ## Eine Ursache gilt als erkannt, sobald eine ihrer Tätigkeiten aufgedeckt ist. ## Nur eine Quelle -- sonst driften Slot und Ursache auseinander. func ursache_bekannt(human_id: String, ursache: String) -> bool: for nr in erkannt_slots.get(human_id, {}): if Ursachen.ursache_von(slot_grund(human_id, int(nr))) == ursache: return true return false func ursachen_von(human_id: String) -> Array: var h := HumanDB.by_id(human_id) return h["ursachen"] if h.has("ursachen") else [] # -------------------------------------------------------------------- Tempo ## Steht gegen diese Ursache ein System? Gibt die Güte zurück, sonst 0. func ursache_geloest(human_id: String, ursache: String) -> float: var beste := 0.0 for nr in systeme.get(human_id, {}): var sys: Dictionary = systeme[human_id][nr] if str(TechDB.by_id(str(sys["tech"]))["ursache"]) == ursache: beste = maxf(beste, float(sys["guete"])) return beste ## Wie schnell kommt der Traum voran? Jede ungelöste Ursache bremst. func traum_tempo(human_id: String) -> float: var t := BASIS_TEMPO for u in ursachen_von(human_id): var g := ursache_geloest(human_id, str(u)) t *= g if g > 0.0 else float(Ursachen.LISTE[u]["faktor"]) return t # ------------------------------------------------------------- System setzen ## Passt diese Technik in diesen Slot? Gibt Güte und Grund zurück. func system_pruefen(human_id: String, nr: int, tech_id: String) -> Dictionary: var res := {"geht": false, "guete": 0.0, "grund": ""} if not known.has(tech_id): res["grund"] = "Diese Technik kennst du nicht." return res if not bereit(tech_id): res["grund"] = "%s ist noch nicht wieder einsatzbereit." % TechDB.by_id(tech_id)["name"] return res var grund_akt := slot_grund(human_id, nr) if grund_akt == "schlaeft": res["grund"] = "Im Schlaf lässt sich nichts einrichten." return res if not erkannt(human_id, nr): res["grund"] = "Was hier läuft, hast du noch nicht gesehen." return res var t := TechDB.by_id(tech_id) if Ursachen.ist_blocker(grund_akt): var b: Dictionary = Ursachen.BLOCKER[grund_akt] if str(t["ursache"]) != str(b["ursache"]): res["grund"] = "Das hier hat einen anderen Grund." return res res["geht"] = true var gleich: bool = str(t["belohnung"]) == str(b["belohnung"]) res["guete"] = GUETE_ERSATZ if gleich or TechDB.entfernt_ausloeser(tech_id) else GUETE_NEU return res res["geht"] = true res["guete"] = GUETE_NEU return res ## Richtet das System ein. Gelingt nicht immer -- Übung entscheidet. func system_setzen(human_id: String, nr: int, tech_id: String, widerstand: bool) -> Dictionary: var pruef := system_pruefen(human_id, nr, tech_id) if not pruef["geht"]: return {"ok": false, "grund": pruef["grund"]} cooldown[tech_id] = TECHNIK_COOLDOWN var chance := clampf(0.20 + 0.13 * float(skill_of(tech_id)) - (0.25 if widerstand else 0.0), 0.05, 0.95) var erfolg := randf() < chance if not erfolg: add_xp(tech_id, 1) state_changed.emit() return {"ok": true, "erfolg": false, "guete": 0.0} if not systeme.has(human_id): systeme[human_id] = {} systeme[human_id][nr] = {"tech": tech_id, "guete": float(pruef["guete"])} add_xp(tech_id, 2) notified.emit("System steht: %s um %02d:00" % [TechDB.by_id(tech_id)["name"], int(slots(human_id)[nr][0])], Color(0.6, 1, 0.7)) state_changed.emit() return {"ok": true, "erfolg": true, "guete": float(pruef["guete"])} func system_entfernen(human_id: String, nr: int) -> void: if systeme.has(human_id): systeme[human_id].erase(nr) state_changed.emit() func system_anzahl(human_id: String) -> int: return systeme.get(human_id, {}).size() ## Der Traum kommt voran, solange Systeme stehen. func traum_voranbringen(stunden: float) -> void: if active_mission == "": return if situation_bei(active_mission): return # im Moment geht nichts voran var p: float = progress.get(active_mission, 0.0) \ + traum_tempo(active_mission) * stunden / 24.0 progress[active_mission] = minf(p, MISSION_ZIEL) if progress[active_mission] >= MISSION_ZIEL and not completed.has(active_mission): var wer := active_mission completed.append(wer) active_mission = "" mission_done.emit(wer) notified.emit("%s hat es geschafft." % HumanDB.by_id(wer)["name"], Color(1, 0.85, 0.4)) _insel_waechst() state_changed.emit() # ------------------------------------------------------- Probleme und Momente # Die zweite Ebene. Eine Gewohnheit sitzt im Tagesablauf und läuft ohne dich # weiter, sobald ein System steht. Ein Problem sitzt in keiner Stunde -- es # bricht in Situationen auf. Solange eine läuft, kommt der Traum nicht voran. # Du kannst sie aussitzen oder mit der passenden Technik beenden. const SITUATION_DAUER := 22.0 # so lange steht der Traum still const SITUATION_PAUSE := 40.0 # Luft zwischen zwei Situationen const PROBLEM_UEBERWINDEN := 3 # so oft muss eine Technik greifen const SITUATION_GEWINN := 6.0 # Prozent, wenn sie früh beendet wird func probleme_von(human_id: String) -> Array: var h := HumanDB.by_id(human_id) return h["probleme"] if h.has("probleme") else [] func problem_bekannt(human_id: String, problem: String) -> bool: return probleme_bekannt.get(human_id, {}).has(problem) func problem_ueberwunden(human_id: String, problem: String) -> bool: return ueberwunden.get(human_id, {}).has(problem) func problem_fortschritt(human_id: String, problem: String) -> int: return int(problem_stand.get("%s:%s" % [human_id, problem], 0)) func situation_laeuft() -> bool: return not situation.is_empty() func situation_bei(human_id: String) -> bool: return situation_laeuft() and str(situation["mensch"]) == human_id func situation_problem() -> String: return str(situation["problem"]) if situation_laeuft() else "" ## Läuft die Uhr weiter, kommt irgendwann die nächste Situation -- aber nur ## bei einem wachen Menschen, dem man gerade hilft. func situationen_ticken(delta: float, wach: bool) -> void: if situation_laeuft(): situation["rest"] = float(situation["rest"]) - delta if float(situation["rest"]) <= 0.0: var p := situation_problem() notified.emit("Der Moment ist vorbei — %s bleibt." % Probleme.name_von(p), Color(1, 0.75, 0.55)) situation = {} situation_pause = SITUATION_PAUSE state_changed.emit() return situation_pause = maxf(0.0, situation_pause - delta) if active_mission == "" or not wach or situation_pause > 0.0: return _situation_starten() func _situation_starten() -> void: var offen: Array = [] for p in probleme_von(active_mission): if not problem_ueberwunden(active_mission, str(p)): offen.append(str(p)) if offen.is_empty(): situation_pause = SITUATION_PAUSE return var problem: String = str(offen[randi() % offen.size()]) var moeglich := Probleme.situationen_von(problem) if moeglich.is_empty(): situation_pause = SITUATION_PAUSE return situation = { "mensch": active_mission, "key": str(moeglich[randi() % moeglich.size()]), "problem": problem, "rest": SITUATION_DAUER, } state_changed.emit() ## Dabeisein zeigt, welches Problem dahintersteckt. func situation_sehen() -> void: if not situation_laeuft(): return var id := str(situation["mensch"]) var p := situation_problem() if problem_bekannt(id, p): return if not probleme_bekannt.has(id): probleme_bekannt[id] = {} probleme_bekannt[id][p] = true notified.emit("Dahinter steckt: %s — %s" % [Probleme.name_von(p), Probleme.LISTE[p]["kurz"]], Color(0.8, 0.95, 1)) state_changed.emit() ## Eine Technik im Moment anwenden. Passt sie zum Problem, ist die Situation ## vorbei; oft genug, und das Problem kommt nicht wieder. func situation_anwenden(tech_id: String) -> Dictionary: var res := {"ok": false, "grund": "", "treffer": false, "ueberwunden": false} if not situation_laeuft(): res["grund"] = "Gerade ist kein Moment offen." return res if not known.has(tech_id): res["grund"] = "Diese Technik kennst du nicht." return res if not bereit(tech_id): res["grund"] = "%s braucht noch %.0f s." % [TechDB.by_id(tech_id)["name"], cooldown_rest(tech_id)] return res var id := str(situation["mensch"]) var problem := situation_problem() cooldown[tech_id] = TECHNIK_COOLDOWN res["ok"] = true if not Probleme.hilft(tech_id, problem): add_xp(tech_id, 1) notified.emit("%s greift hier nicht." % TechDB.by_id(tech_id)["name"], Color(1, 0.7, 0.5)) state_changed.emit() return res res["treffer"] = true add_xp(tech_id, 2) var schluessel := "%s:%s" % [id, problem] problem_stand[schluessel] = problem_fortschritt(id, problem) + 1 situation_sehen() situation = {} situation_pause = SITUATION_PAUSE if id == active_mission: progress[id] = minf(mission_progress(id) + SITUATION_GEWINN, MISSION_ZIEL) notified.emit("%s trägt durch den Moment. (+%d %%)" % [TechDB.by_id(tech_id)["name"], int(SITUATION_GEWINN)], Color(0.6, 1, 0.7)) if problem_fortschritt(id, problem) >= PROBLEM_UEBERWINDEN: if not ueberwunden.has(id): ueberwunden[id] = {} ueberwunden[id][problem] = true res["ueberwunden"] = true notified.emit("%s ist überwunden — dieser Moment kommt nicht wieder." % Probleme.name_von(problem), Color(1, 0.85, 0.4)) state_changed.emit() return res # --------------------------------------------------- Erreichbarkeit ## Schlafende erreicht niemand. func erreichbar(activity: String) -> bool: return activity != "schlaeft"