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 const FEHLVERSUCH_VERLUST := 14.0 # ein Fehlschlag wirft den Traum zurück # 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 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() 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 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() # --------------------------------------------------- Erreichbarkeit ## Schlafende erreicht niemand. func erreichbar(activity: String) -> bool: return activity != "schlaeft"