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