Files
dreamer-game/scripts/game.gd
2026-09-01 13:07:02 +02:00

484 lines
15 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 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 beobachtung: Dictionary = {} # "human:slot" -> Sekunden
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
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 ERKENNEN_SEK := 3.0 # so lange muss man dabeistehen
func slots(human_id: String) -> Array:
var h := HumanDB.by_id(human_id)
return h["routine"] if h.has("routine") else []
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)
## Dabeistehen deckt auf, was dort wirklich läuft -- und damit die Ursache.
func beobachten(human_id: String, nr: int, delta: float) -> bool:
if erkannt(human_id, nr):
return false
var schluessel := "%s:%d" % [human_id, nr]
var stand: float = float(beobachtung.get(schluessel, 0.0)) + delta
beobachtung[schluessel] = stand
if stand < ERKENNEN_SEK:
return false
if not erkannt_slots.has(human_id):
erkannt_slots[human_id] = {}
erkannt_slots[human_id][nr] = true
var b := slot_grund(human_id, nr)
notified.emit("Erkannt: %s — %s" % [Ursachen.BLOCKER[b]["name"],
Ursachen.LISTE[Ursachen.ursache_von(b)]["name"]], Color(0.8, 0.95, 1))
state_changed.emit()
return true
## 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"