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 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 nav_map: RID # auf der Erde: Pfade vom Navigationsnetz var nav_fehl := 0 # wie oft das Netz keinen Pfad wusste (Rückfall aufs Wegenetz) var nav_fehl_info := "" var _bob: float = 0.0 var _takt := 0.0 # Schild und Haltungsziele alle 0,3 s, nicht je Bild var _neigung_ziel := 0.0 var _groesse_ziel := 1.0 var _zappelt := false var _offset: Vector3 func setup(d: Dictionary, index: int, orte: Dictionary) -> void: data = d idx = index places = orte 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 ## Pfad über das Navigationsnetz zu einem Ort -- leer, wenn es keins gibt. func _nav_route(nach: String) -> Array: if not nav_map.is_valid(): return [] var ziel := Orte.center(places, nach) + _offset var pts := NavigationServer3D.map_get_path(nav_map, global_position, ziel, true) if pts.size() < 2: nav_fehl += 1 nav_fehl_info = "%s: %s -> %s von %s nach %s (%d Punkte)" % [data["name"], place, nach, str(global_position), str(ziel), pts.size()] print(" [kein Weg] " + nav_fehl_info) return [] var out: Array = [] for i in range(1, pts.size()): out.append(pts[i]) return out ## 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: if _takt <= 0.0: _haltungsziele() var zappeln := sin(Time.get_ticks_msec() * 0.011) * 0.06 if _zappelt and not geht else 0.0 _ober.rotation.x = lerpf(_ober.rotation.x, -_neigung_ziel, 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_ziel, 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)) func _haltungsziele() -> 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 _neigung_ziel = 0.38 if offen.has("erschoepfung") else 0.0 _groesse_ziel = 0.86 if offen.has("angst") else 1.0 _zappelt = offen.has("unruhe") ## [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: _path = _nav_route(soll[0]) if _path.is_empty(): # kein Netz oder kein Weg: Luftlinie, damit er ankommt _path = [Orte.center(places, soll[0]) + _offset] 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 # Auf der Erde tragen nur Fokus, Verfolgte, Mission und wer unter der Maus # steht ein Schild -- sonst überdecken sich acht Namen im Café. _label.visible = (not mit_mission and not bool(data.get("stumm", false))) \ or Game.markiert.has(str(data["id"])) if _takt <= 0.0: _takt = 0.3 if _label.visible: _label.text = beschriftung() _takt -= delta _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)))