extends Node3D class_name Bewohner ## Bewohner einer Welt: läuft seinen Tagesablauf ab. Auf der Erde kann er von ## einer Technik erreicht werden -- oder eben nicht. const GEHTEMPO := 8.0 const MASSSTAB := 0.8 # Menschen sind klein, die Stadt wirkt groß var data: Dictionary var idx: int = 0 var place: String = "" var activity: String = "" var resist_left: float = 0.0 var places: Dictionary = {} var netz: Netz = null var anker: Dictionary = {} var mit_mission := false var _visual: Node3D var _ober: Node3D # Oberkörper: neigt sich, duckt sich, zappelt var _arme: Array = [] var _label: Label3D var _koerper: MeshInstance3D var _kopf: MeshInstance3D var _aura: MeshInstance3D var _strahl: MeshInstance3D var _path: Array = [] var _letzte_aktivitaet: String = "" var boden: Callable = Callable() # Höhe des Bodens (x, z) -> y; leer heißt eben var slot: int = 0 var _bob: float = 0.0 var _offset: Vector3 func setup(d: Dictionary, index: int, orte: Dictionary, wegenetz: Netz = null, wegeanker: Dictionary = {}) -> void: data = d idx = index places = orte netz = wegenetz anker = wegeanker mit_mission = bool(d.get("mission", false)) name = "Bewohner_" + str(d["id"]) var winkel := float(index) * TAU / 4.0 _offset = Vector3(cos(winkel), 0, sin(winkel)) * 2.2 _visual = Node3D.new() _visual.scale = Vector3.ONE * MASSSTAB add_child(_visual) var c: Color = d["farbe"] var leuchtet := bool(d.get("leuchtet", false)) var hoehe := 1.05 + float(index % 3) * 0.08 _ober = Node3D.new() _ober.position.y = hoehe * 0.45 _visual.add_child(_ober) var o := Vector3(0, -hoehe * 0.45, 0) _koerper = Prims.capsule(_ober, o + Vector3(0, hoehe * 0.62, 0), 0.32, hoehe, c) _kopf = Prims.sphere(_ober, o + Vector3(0, hoehe * 1.18 + 0.1, 0), 0.24, c.lightened(0.45)) Prims.box(_ober, o + Vector3(0, hoehe * 0.95, 0), Vector3(0.9, 0.14, 0.3), c.darkened(0.25), false) for sx: float in [-1.0, 1.0]: var gelenk := Node3D.new() gelenk.position = o + Vector3(sx * 0.46, hoehe * 0.92, 0) _ober.add_child(gelenk) Prims.capsule(gelenk, Vector3(0, -0.32, 0), 0.09, 0.62, c.darkened(0.1)) _arme.append(gelenk) _hut(o + Vector3(0, hoehe * 1.18 + 0.1, 0), c) Prims.box(_visual, Vector3(0, 0.06, 0), Vector3(0.5, 0.12, 0.7), c.darkened(0.45), false) if leuchtet: _koerper.material_override = Prims.mat(c, 0.9) _kopf.material_override = Prims.mat(c.lightened(0.45), 0.8) var reif := Prims.cyl(_ober, o + Vector3(0, hoehe * 0.5, 0), 0.62, 0.05, c.lightened(0.3), false, 1.6) reif.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _aura = Prims.cyl(self, Vector3(0, 0.05, 0), 0.95, 0.05, Color(1, 0.85, 0.35), false, 2.0) _aura.visible = false _strahl = Prims.cyl(self, Vector3(0, 11.0, 0), 0.14, 16.0, Color(1, 0.85, 0.35, 0.22), false, 1.2) _strahl.visible = false # gestaffelt, damit sich Schilder in einem Raum nicht übereinanderlegen _label = Prims.label(self, Vector3(0, 2.3 + float(index % 5) * 0.35, 0), d["name"], 0.36) var start := _routine_at(Game.time_h) place = start[0] activity = start[1] _letzte_aktivitaet = activity global_position = Orte.center(places, place) + _offset + Vector3(0, Orte.hoehe(etage(place)), 0) ## In welcher Etage dieser Mensch an einem Ort ist -- 0, wenn er dort nur unten ist. func etage(ort: String) -> int: return int((data.get("etage", {}) as Dictionary).get(ort, 0)) ## Wege führen durchs Treppenhaus: aus der Etage runter zur Tür, und am Ziel ## von der Tür hinauf in die Etage. func _mit_etagen(pts: Array, von: String, nach: String, war_unterwegs: bool) -> Array: if pts.is_empty(): return pts var e_von := etage(von) if e_von > 0 and not war_unterwegs: var runter: Array = [Orte.treppe(places, von, e_von)] runter.append_array(Orte.treppen_weg(places, von, e_von, 0)) runter.append(Orte.innen_tuer(places, von)) runter.append_array(pts) pts = runter var e_nach := etage(nach) if e_nach > 0: pts.pop_back() pts.append(Orte.innen_tuer(places, nach)) pts.append(Orte.treppe(places, nach, 0)) pts.append_array(Orte.treppen_weg(places, nach, 0, e_nach)) pts.append(Orte.center(places, nach) + _offset + Vector3(0, Orte.hoehe(e_nach), 0)) return pts ## Kopfbedeckung nach Tagestyp des Berufs -- so unterscheiden sich die Leute ## schon aus der Ferne. func _hut(kopf: Vector3, c: Color) -> void: var typ := str(HumanDB.BERUF_TAGESTYP.get(str(data.get("beruf", "")), "")) var dunkel := c.darkened(0.4) match typ: "frueh": # Mütze Prims.cyl(_ober, kopf + Vector3(0, 0.2, 0), 0.22, 0.14, dunkel, false) "buero": # Hut mit Krempe Prims.cyl(_ober, kopf + Vector3(0, 0.16, 0), 0.36, 0.04, dunkel, false) Prims.cyl(_ober, kopf + Vector3(0, 0.3, 0), 0.18, 0.24, dunkel, false) "studium": # Zipfel var z := Prims.cyl(_ober, kopf + Vector3(0, 0.36, 0), 0.2, 0.4, dunkel, false) (z.mesh as CylinderMesh).top_radius = 0.0 "werkstatt": # Kappe mit Schirm Prims.cyl(_ober, kopf + Vector3(0, 0.18, 0), 0.25, 0.1, dunkel, false) Prims.box(_ober, kopf + Vector3(0, 0.14, -0.3), Vector3(0.3, 0.04, 0.2), dunkel, false) _: pass ## Angst duckt, Erschöpfung beugt, Unruhe zappelt -- solange die Klemme nicht ## überwunden ist. So sieht man Probleme beim Beobachten am Körper. func _haltung(delta: float, geht: bool) -> void: var id := str(data["id"]) var offen: Dictionary = {} for k in data.get("probleme", []): if not Game.problem_ueberwunden(id, str(k)): offen[str(k)] = true var neigung := 0.38 if offen.has("erschoepfung") else 0.0 var groesse := 0.86 if offen.has("angst") else 1.0 var zappeln := 0.0 if offen.has("unruhe") and not geht: zappeln = sin(Time.get_ticks_msec() * 0.011) * 0.06 _ober.rotation.x = lerpf(_ober.rotation.x, -neigung, 1.0 - pow(0.02, delta)) _ober.rotation.z = lerpf(_ober.rotation.z, zappeln, 1.0 - pow(0.001, delta)) _ober.scale = _ober.scale.lerp(Vector3.ONE * groesse, 1.0 - pow(0.02, delta)) var schwung := sin(_bob) * 0.7 if geht else 0.0 for i in range(_arme.size()): var a: Node3D = _arme[i] a.rotation.x = lerpf(a.rotation.x, schwung * (1.0 if i == 0 else -1.0), 1.0 - pow(0.001, delta)) ## [Ort, Tätigkeit, Slotnummer] zur Stunde h. Steht in dem Slot ein System, ## ist die Tätigkeit die Ersatzhandlung. func _routine_at(h: float) -> Array: var r: Array = data["routine"] var nr := r.size() - 1 for i in range(r.size()): if h >= float(r[i][0]): nr = i var akt: String = str(r[nr][2]) if mit_mission: akt = Game.slot_aktivitaet(str(data["id"]), nr) return [str(r[nr][1]), akt, nr] func _process(roh: float) -> void: var delta := roh * Game.zeit_faktor() resist_left = maxf(0.0, resist_left - delta) var soll := _routine_at(Game.time_h) slot = int(soll[2]) if soll[0] != place: var war_unterwegs := not _path.is_empty() _path = Orte.route(places, place, soll[0], _offset, netz, anker) if war_unterwegs and _path.size() > 1: _path.remove_at(0) # steht schon draußen, alte Tür überspringen _path = _mit_etagen(_path, place, soll[0], war_unterwegs) place = soll[0] activity = "unterwegs" elif _path.is_empty() and activity != soll[1]: activity = soll[1] if not _path.is_empty(): var ziel: Vector3 = _path[0] var hier := global_position if boden.is_valid(): ziel.y = hier.y # das Gelände bestimmt die Höhe, nicht der Wegpunkt var d := ziel - hier if d.length() < 0.4: _path.remove_at(0) if _path.is_empty(): activity = soll[1] else: # nie über den Wegpunkt hinaus -- sonst pendelt er bei hohem Tempo # um ihn herum, statt anzukommen var schritt := d.normalized() * minf(GEHTEMPO * delta, d.length()) global_position += schritt _visual.rotation.y = lerp_angle(_visual.rotation.y, atan2(-d.x, -d.z), 1.0 - pow(0.002, delta)) _bob += delta * 9.0 _visual.position.y = absf(sin(_bob)) * 0.09 if _path.is_empty(): _visual.position.y = lerpf(_visual.position.y, 0.0, 1.0 - pow(0.01, delta)) _haltung(delta, not _path.is_empty()) if boden.is_valid(): global_position.y = float(boden.call(global_position.x, global_position.z)) _visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0, 1.0 - pow(0.01, delta)) if activity != _letzte_aktivitaet: _letzte_aktivitaet = activity var erledigt: bool = mit_mission and Game.completed.has(data["id"]) var aktiv: bool = mit_mission and Game.active_mission == data["id"] _aura.visible = aktiv or erledigt _strahl.visible = aktiv if _aura.visible: _aura.material_override = Prims.mat( Color(0.4, 1.0, 0.5) if erledigt else Color(1, 0.85, 0.35), 2.0) _aura.scale.x = 1.0 + sin(Time.get_ticks_msec() * 0.003) * 0.08 _aura.scale.z = _aura.scale.x _label.text = beschriftung() _label.modulate = Color(0.55, 0.85, 0.6) if erledigt else ( Color(1, 0.9, 0.6) if aktiv else Color(1, 1, 1)) func beschriftung() -> String: var s: String = data["name"] if data.has("rolle"): s += ", " + str(data["rolle"]) s += "\n" + Orte.text(activity) if mit_mission and Game.situation_bei(str(data["id"])): s += "\n» %s «" % Probleme.SITUATIONEN[Game.situation["key"]]["name"] if resist_left > 0.0: s += " (macht dicht)" if mit_mission: if Game.completed.has(data["id"]): s += "\n★ Traum erfüllt" return s ## Sichtbare Reaktion auf eine angewandte Technik. func reagiere(success: bool, text: String) -> void: var l := Prims.label(self, Vector3(0, 3.0, 0), text, 0.4, Color(0.55, 1.0, 0.6) if success else Color(1, 0.55, 0.45)) var tw := l.create_tween() tw.set_parallel(true) tw.tween_property(l, "position:y", 5.2, 2.4) tw.tween_property(l, "modulate:a", 0.0, 2.4).set_delay(0.9) tw.chain().tween_callback(l.queue_free) if not success: resist_left = Game.RESIST_SEKUNDEN * \ (1.0 + 0.3 * float(Game.verhalten(str(data["id"]), "abwehr"))) var blitz := Prims.mat( Color(0.6, 1.0, 0.7) if success else Color(1, 0.5, 0.4), 1.6) var orig := _koerper.material_override _koerper.material_override = blitz _kopf.material_override = blitz var t2 := create_tween() t2.tween_interval(0.35) t2.tween_callback(func() -> void: if is_instance_valid(_koerper): _koerper.material_override = orig _kopf.material_override = Prims.mat((data["farbe"] as Color).lightened(0.45)))