Files
dreamer-game/scripts/game.gd
2026-08-31 11:07:01 +02:00

478 lines
14 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 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"],
}