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 used: Dictionary = {} # "human:tech" -> Anzahl var cooldown: Dictionary = {} # tech_id -> Restsekunden var pipeline: Dictionary = {} # human_id -> {activity -> tech_id} 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], "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 _gewoehnung_klingt_ab() ## Abstand macht eine Technik wieder wirksam: über Nacht verfällt die Hälfte ## der Gewöhnung. Wer täglich denselben Weg geht, hält ihn brauchbar — wer ihn ## an einem Tag immer wieder auffährt, stumpft ihn ab. func _gewoehnung_klingt_ab() -> void: for key in used: used[key] = int(used[key]) / 2 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 gelernte Technik das Thema ## des Menschen trifft und in seinem Tag auch einen Moment dafür findet. ## Nur solche werden am Schwarzen Brett überhaupt ausgehängt. func machbar(human_id: String) -> bool: var h := HumanDB.by_id(human_id) if h.is_empty(): return false var tag_liste: Array = HumanDB.aktivitaeten(human_id) for id in known: var t := TechDB.by_id(id) var thema := false for tag in t["tags"]: if (h["tags"] as Array).has(tag): thema = true break if not thema: continue for a in t["momente"]: if tag_liste.has(a) and erreichbar(a): 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)) # ------------------------------------------------------------------- Regelwerk # Eine Regel ist ein Wenn-Dann: tut der Mensch etwas Bestimmtes, greift die # zugeordnete Technik von allein. Damit automatisiert der Spieler den Fortschritt. func regeln(human_id: String) -> Dictionary: return pipeline.get(human_id, {}) func regel_fuer(human_id: String, activity: String) -> String: return regeln(human_id).get(activity, "") func regel_anzahl(human_id: String) -> int: return regeln(human_id).size() ## An welchen Tätigkeiten hängt diese Technik gerade? func regel_momente(human_id: String, tech_id: String) -> Array: var out: Array = [] for activity in regeln(human_id): if regeln(human_id)[activity] == tech_id: out.append(activity) return out ## Leere tech_id löscht die Regel. Eine Technik darf an beliebig vielen ## Tätigkeiten hängen — ob das klug ist, muss der Spieler aus ihr herauslesen. func regel_setzen(human_id: String, activity: String, tech_id: String) -> void: if not pipeline.has(human_id): pipeline[human_id] = {} var r: Dictionary = pipeline[human_id] if tech_id == "": r.erase(activity) else: r[activity] = tech_id state_changed.emit() ## Greift für diese Tätigkeit eine Regel? Wird nicht nur beim Wechsel gefragt, ## sondern immer wieder — solange der Moment anhält und die Technik frei ist. func regel_ausloesen(human_id: String, activity: String, resisting: bool) -> Dictionary: if human_id != active_mission: return {} var tech_id := regel_fuer(human_id, activity) if tech_id == "" or not known.has(tech_id): return {} if not bereit(tech_id): return {} # still: die Technik greift wieder, sobald ihre Sperre fällt var res := apply_technique(tech_id, human_id, activity, resisting) if not res["ok"]: return {} res["tech"] = tech_id var t := TechDB.by_id(tech_id) if res["success"]: notified.emit("Regel greift — %s bei »%s« (+%d %%)" % [t["name"], Orte.text(activity), int(round(float(res["gain"])))], Color(0.6, 1, 0.7)) else: notified.emit("Regel läuft ins Leere — %s bei »%s« (−%d %%)" % [t["name"], Orte.text(activity), int(round(float(res["verlust"])))], Color(1, 0.7, 0.5)) return res # ----------------------------------------------------- Technik auf Mensch anwenden ## Schlafende erreicht keine Technik — weder von Hand noch über eine Regel. func erreichbar(activity: String) -> bool: return activity != "schlaeft" ## Bewertet eine Technik in genau dieser Situation, ohne sie anzuwenden. func evaluate(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary: var t := TechDB.by_id(tech_id) var h := HumanDB.by_id(human_id) var skill := skill_of(tech_id) var res := { "chance": 0.0, "tag": false, "moment": false, "blocked": false, "grund": "", "skill": skill, } if not erreichbar(activity): res["blocked"] = true res["grund"] = "Sie oder er schläft. Techniken erreichen nur wache Menschen." return res if skill == 0: res["blocked"] = true res["grund"] = "Technik nicht gelernt." return res for tag in t["tags"]: if h["tags"].has(tag): res["tag"] = true break res["moment"] = t["momente"].has(activity) # Der Moment wiegt am schwersten: eine Technik zur falschen Zeit greift kaum, # egal wie gut sie zum Menschen passt. Genau das steht in ihrem "Wann". var c := 0.05 + 0.09 * float(skill) if res["tag"]: c += 0.20 if res["moment"]: c += 0.35 if resisting: c -= 0.30 res["grund"] = "Der Mensch macht gerade dicht." res["chance"] = clampf(c, 0.05, 0.95) return res ## Wendet die Technik wirklich an: würfelt, verteilt EP und Fortschritt. func apply_technique(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary: if not bereit(tech_id): return {"ok": false, "grund": "%s ist noch nicht wieder einsatzbereit." % TechDB.by_id(tech_id)["name"]} var ev := evaluate(tech_id, human_id, activity, resisting) if ev["blocked"]: return {"ok": false, "grund": ev["grund"]} cooldown[tech_id] = TECHNIK_COOLDOWN var roll := randf() var success: bool = roll < float(ev["chance"]) var key := "%s:%s" % [human_id, tech_id] var wiederholung: int = used.get(key, 0) used[key] = wiederholung + 1 var gain := 0.0 var verlust := 0.0 if success: var base := 8.0 + 2.0 * float(ev["skill"]) if ev["tag"]: base += 5.0 if ev["moment"]: base += 5.0 gain = base / (1.0 + 0.6 * float(wiederholung)) add_xp(tech_id, 2) else: verlust = FEHLVERSUCH_VERLUST add_xp(tech_id, 1) var done := false if human_id == active_mission: var p: float = progress.get(human_id, 0.0) + gain - verlust progress[human_id] = clampf(p, 0.0, MISSION_ZIEL) if progress[human_id] >= MISSION_ZIEL and not completed.has(human_id): completed.append(human_id) active_mission = "" done = true mission_done.emit(human_id) notified.emit("%s hat es geschafft." % HumanDB.by_id(human_id)["name"], Color(1, 0.85, 0.4)) _insel_waechst() state_changed.emit() return { "ok": true, "success": success, "chance": ev["chance"], "gain": gain, "verlust": verlust, "wiederholung": wiederholung, "mission_done": done, "tag": ev["tag"], "moment": ev["moment"], }