711 lines
23 KiB
GDScript
711 lines
23 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
|
||
# 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 situation: Dictionary = {} # laufende Situation: mensch, key, rest
|
||
var situation_pause: float = 0.0 # bis die nächste kommen darf
|
||
var probleme_bekannt: Dictionary = {} # human_id -> {problem -> true}
|
||
var problem_stand: Dictionary = {} # "human:problem" -> gelungene Anwendungen
|
||
var ueberwunden: Dictionary = {} # human_id -> {problem -> true}
|
||
var gesehen_zaehler: Dictionary = {} # "human:problem" -> wie oft dabeigewesen
|
||
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],
|
||
"interagieren": [KEY_E],
|
||
"folgen": [KEY_F],
|
||
"notieren": [KEY_R],
|
||
"journal": [KEY_J, KEY_TAB],
|
||
"drehen": [KEY_Q],
|
||
"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()
|
||
|
||
|
||
## Der Zustand einer Stunde. HUD und Panel zeichnen denselben -- zwei
|
||
## Auslegungen würden mit der Zeit auseinanderlaufen.
|
||
enum Stunde { UNBEKANNT, GESEHEN, SCHLAF, UNGEKLAERT, BENANNT, GEBAUT }
|
||
|
||
|
||
func slot_zustand(human_id: String, nr: int) -> int:
|
||
if not gesehen_slot(human_id, nr):
|
||
return Stunde.UNBEKANNT
|
||
if not system_in(human_id, nr).is_empty():
|
||
return Stunde.GEBAUT
|
||
var grund := slot_grund(human_id, nr)
|
||
if Ursachen.ist_blocker(grund):
|
||
return Stunde.BENANNT if erkannt(human_id, nr) else Stunde.UNGEKLAERT
|
||
if grund == "schlaeft":
|
||
return Stunde.SCHLAF
|
||
return Stunde.GESEHEN
|
||
|
||
|
||
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("Nichts schief: %s" % Orte.text(grund), Color(1, 0.75, 0.55))
|
||
return "daneben"
|
||
if erkannt(human_id, nr):
|
||
notified.emit("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 * (1.0 + 0.15 * float(verhalten(human_id, "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 := 0.20 + 0.13 * float(skill_of(tech_id)) - (0.25 if widerstand else 0.0)
|
||
chance += 0.08 * float(verhalten(human_id, "einrichten"))
|
||
if TechDB.ist_sozial(tech_id):
|
||
chance += 0.10 * float(verhalten(human_id, "sozial"))
|
||
chance = clampf(chance, 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] = {}
|
||
var guete := float(pruef["guete"]) * (1.0 + 0.1 * float(verhalten(human_id, "guete")))
|
||
systeme[human_id][nr] = {"tech": tech_id, "guete": 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
|
||
if situation_bei(active_mission):
|
||
return # im Moment geht nichts voran
|
||
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()
|
||
|
||
|
||
# ------------------------------------------------------- Probleme und Momente
|
||
# Die zweite Ebene. Eine Gewohnheit sitzt im Tagesablauf und läuft ohne dich
|
||
# weiter, sobald ein System steht. Ein Problem sitzt in keiner Stunde -- es
|
||
# bricht in Situationen auf. Solange eine läuft, kommt der Traum nicht voran.
|
||
# Du kannst sie aussitzen oder mit der passenden Technik beenden.
|
||
|
||
const SITUATION_DAUER := 22.0 # so lange steht der Traum still
|
||
const SITUATION_PAUSE := 40.0 # Luft zwischen zwei Situationen
|
||
const PROBLEM_UEBERWINDEN := 3 # so oft muss eine Technik greifen
|
||
const SITUATION_GEWINN := 6.0 # Prozent, wenn sie früh beendet wird
|
||
|
||
|
||
## Der Verhaltenswert eines Menschen, -2 bis +2 -- aus seinen Wörtern.
|
||
func verhalten(human_id: String, key: String) -> int:
|
||
var h := HumanDB.by_id(human_id)
|
||
return int((h.get("verhalten", {}) as Dictionary).get(key, 0))
|
||
|
||
|
||
func probleme_von(human_id: String) -> Array:
|
||
var h := HumanDB.by_id(human_id)
|
||
return h["probleme"] if h.has("probleme") else []
|
||
|
||
|
||
func problem_bekannt(human_id: String, problem: String) -> bool:
|
||
return probleme_bekannt.get(human_id, {}).has(problem)
|
||
|
||
|
||
func problem_ueberwunden(human_id: String, problem: String) -> bool:
|
||
return ueberwunden.get(human_id, {}).has(problem)
|
||
|
||
|
||
func problem_fortschritt(human_id: String, problem: String) -> int:
|
||
return int(problem_stand.get("%s:%s" % [human_id, problem], 0))
|
||
|
||
|
||
func situation_laeuft() -> bool:
|
||
return not situation.is_empty()
|
||
|
||
|
||
func situation_bei(human_id: String) -> bool:
|
||
return situation_laeuft() and str(situation["mensch"]) == human_id
|
||
|
||
|
||
func situation_problem() -> String:
|
||
return str(situation["problem"]) if situation_laeuft() else ""
|
||
|
||
|
||
## Läuft die Uhr weiter, kommt irgendwann die nächste Situation -- aber nur
|
||
## bei einem wachen Menschen, dem man gerade hilft.
|
||
func situationen_ticken(delta: float, wach: bool) -> void:
|
||
if situation_laeuft():
|
||
situation["rest"] = float(situation["rest"]) - delta
|
||
if float(situation["rest"]) <= 0.0:
|
||
var p := situation_problem()
|
||
notified.emit("Moment verpasst: %s" % Probleme.name_von(p),
|
||
Color(1, 0.75, 0.55))
|
||
var wer := str(situation["mensch"])
|
||
situation = {}
|
||
situation_pause = _pause_fuer(wer)
|
||
state_changed.emit()
|
||
return
|
||
situation_pause = maxf(0.0, situation_pause - delta)
|
||
if active_mission == "" or not wach or situation_pause > 0.0:
|
||
return
|
||
_situation_starten()
|
||
|
||
|
||
## Luft bis zum nächsten Moment -- ein reizbarer Mensch hat weniger davon.
|
||
func _pause_fuer(human_id: String) -> float:
|
||
return SITUATION_PAUSE * (1.0 - 0.25 * float(verhalten(human_id, "momente")))
|
||
|
||
|
||
func _situation_starten() -> void:
|
||
var offen: Array = []
|
||
for p in probleme_von(active_mission):
|
||
if not problem_ueberwunden(active_mission, str(p)):
|
||
offen.append(str(p))
|
||
if offen.is_empty():
|
||
situation_pause = SITUATION_PAUSE
|
||
return
|
||
var problem: String = str(offen[randi() % offen.size()])
|
||
var moeglich := Probleme.situationen_von(problem)
|
||
if moeglich.is_empty():
|
||
situation_pause = SITUATION_PAUSE
|
||
return
|
||
situation = {
|
||
"mensch": active_mission,
|
||
"key": str(moeglich[randi() % moeglich.size()]),
|
||
"problem": problem,
|
||
"rest": SITUATION_DAUER * (1.0 + 0.25 * float(verhalten(active_mission, "dauer"))),
|
||
}
|
||
if verhalten(active_mission, "sichtbar") > 0:
|
||
situation_sehen() # man sieht es ihm sofort an
|
||
state_changed.emit()
|
||
|
||
|
||
## Dabeisein zeigt, welches Problem dahintersteckt.
|
||
func situation_sehen() -> void:
|
||
if not situation_laeuft():
|
||
return
|
||
var id := str(situation["mensch"])
|
||
var p := situation_problem()
|
||
if problem_bekannt(id, p):
|
||
return
|
||
if verhalten(id, "sichtbar") < 0:
|
||
var schluessel := "%s:%s" % [id, p]
|
||
gesehen_zaehler[schluessel] = int(gesehen_zaehler.get(schluessel, 0)) + 1
|
||
if int(gesehen_zaehler[schluessel]) < 2:
|
||
return # verschlossen: erst beim zweiten Mal zu durchschauen
|
||
if not probleme_bekannt.has(id):
|
||
probleme_bekannt[id] = {}
|
||
probleme_bekannt[id][p] = true
|
||
notified.emit("Erkannt: %s" % Probleme.name_von(p), Color(0.8, 0.95, 1))
|
||
state_changed.emit()
|
||
|
||
|
||
## Eine Technik im Moment anwenden. Passt sie zum Problem, ist die Situation
|
||
## vorbei; oft genug, und das Problem kommt nicht wieder.
|
||
func situation_anwenden(tech_id: String) -> Dictionary:
|
||
var res := {"ok": false, "grund": "", "treffer": false, "ueberwunden": false}
|
||
if not situation_laeuft():
|
||
res["grund"] = "Gerade ist kein Moment offen."
|
||
return res
|
||
if not known.has(tech_id):
|
||
res["grund"] = "Diese Technik kennst du nicht."
|
||
return res
|
||
if not bereit(tech_id):
|
||
res["grund"] = "%s braucht noch %.0f s." % [TechDB.by_id(tech_id)["name"],
|
||
cooldown_rest(tech_id)]
|
||
return res
|
||
var id := str(situation["mensch"])
|
||
var problem := situation_problem()
|
||
cooldown[tech_id] = TECHNIK_COOLDOWN
|
||
res["ok"] = true
|
||
if not Probleme.hilft(tech_id, problem):
|
||
add_xp(tech_id, 1)
|
||
notified.emit("%s greift hier nicht." % TechDB.by_id(tech_id)["name"],
|
||
Color(1, 0.7, 0.5))
|
||
state_changed.emit()
|
||
return res
|
||
|
||
res["treffer"] = true
|
||
add_xp(tech_id, 2)
|
||
var schluessel := "%s:%s" % [id, problem]
|
||
problem_stand[schluessel] = problem_fortschritt(id, problem) + 1
|
||
situation_sehen()
|
||
situation = {}
|
||
situation_pause = _pause_fuer(id)
|
||
if id == active_mission:
|
||
progress[id] = minf(mission_progress(id) + SITUATION_GEWINN, MISSION_ZIEL)
|
||
notified.emit("%s trägt durch +%d %%" %
|
||
[TechDB.by_id(tech_id)["name"], int(SITUATION_GEWINN)], Color(0.6, 1, 0.7))
|
||
if problem_fortschritt(id, problem) >= PROBLEM_UEBERWINDEN:
|
||
if not ueberwunden.has(id):
|
||
ueberwunden[id] = {}
|
||
ueberwunden[id][problem] = true
|
||
res["ueberwunden"] = true
|
||
notified.emit("%s überwunden" % Probleme.name_von(problem), Color(1, 0.85, 0.4))
|
||
state_changed.emit()
|
||
return res
|
||
|
||
|
||
# --------------------------------------------------- Erreichbarkeit
|
||
|
||
## Schlafende erreicht niemand.
|
||
func erreichbar(activity: String) -> bool:
|
||
return activity != "schlaeft"
|