484 lines
15 KiB
GDScript
484 lines
15 KiB
GDScript
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"
|