extends CharacterBody3D class_name Player ## Ein Wesen aus der Traumwelt. In der Traumwelt läuft es, auf der Erde schwebt ## es als unsichtbarer Geist durch Wände. enum Mode { TRAUM, GEIST } const SPEED_TRAUM := 8.0 const SPEED_GEIST := 11.0 const GRAVITY := 24.0 const JUMP := 9.0 const FOLGE_ABSTAND := 2.5 # so weit bleibt der Geist auf der Spur zurück const LEINE := 14.0 # weiter weg reißt die Leine: er wird nachgezogen const SCHWEBE := 0.25 # eine Handbreit über dem Boden const PITCH := -0.96 # 55° von oben, fest wie in Diablo const CAM_YAW := PI * 0.25 # Blick diagonal über den Bezirk const ZOOM := [26.0, 40.0] # zwei Stufen, Mausrad const FOV := 35.0 const DREHSCHRITT := PI * 0.5 var mode: int = Mode.TRAUM var yaw := CAM_YAW # Kamera- und damit Steuerrichtung var look_enabled := true # Steuerung an (aus, solange ein Panel offen ist) var folgt: Node3D = null # Mensch, an den sich der Geist gehängt hat var pivot: Node3D var arm: Node3D var cam: Camera3D var _dreh_rest := 0.0 # noch zu drehender Winkel (Q) var _zoom_stufe := 0 var _leine_pfad: Array = [] # Pfad zum Verfolgten, alle 0,4 s neu var _leine_uhr := 0.0 var _verloren := 0.0 # Sekunden zu weit weg vom Verfolgten var _stockt := 0.0 # Sekunden ohne Vorankommen (eingeklemmt) var _vorher := Vector3.ZERO # Position im letzten Physikbild var spruenge := 0 # wie oft Leine oder Stadt nachziehen mussten var body: Node3D var _schleier: MeshInstance3D var _kern: MeshInstance3D var _kopf: MeshInstance3D var _augen: Array = [] var _funken: Array = [] # kreisende Lichter, eins je gelernter Technik var _schleppe: Array = [] # Lichtschweif hinter dem Geist var _schleppe_pos: Array = [] var _farbe := Color(0.55, 0.85, 1.0) var _schleier_mat: StandardMaterial3D var _kern_mat: StandardMaterial3D var _kopf_mat: StandardMaterial3D var _kreis := 0.0 const FARBE_TRAUM := Color(0.55, 0.85, 1.0) const FARBE_GEIST := Color(0.50, 0.92, 0.90) # kühl: beobachten const FARBE_FOLGE := Color(1.0, 0.72, 0.38) # warm: verfolgen const FUNKEN_MAX := 6 func _ready() -> void: var cs := CollisionShape3D.new() var shape := CapsuleShape3D.new() shape.radius = 0.35 # passt durch die 0,8 m zwischen zwei Häusern shape.height = 1.6 cs.shape = shape cs.position.y = 0.8 add_child(cs) body = Node3D.new() add_child(body) # Schleier: oben schmal, unten weit, halb durchsichtig _schleier = Prims.cyl(body, Vector3(0, 1.05, 0), 0.6, 1.7, _farbe, false) (_schleier.mesh as CylinderMesh).top_radius = 0.24 _schleier_mat = Prims.mat(Color(_farbe, 0.55), 0.35).duplicate() _schleier.material_override = _schleier_mat _schleier.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF # Lichtkern im Schleier _kern = Prims.sphere(body, Vector3(0, 1.15, 0), 0.26, _farbe, 2.2) _kern_mat = (_kern.material_override as StandardMaterial3D).duplicate() _kern.material_override = _kern_mat _kern.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _kopf = Prims.sphere(body, Vector3(0, 2.0, 0), 0.3, _farbe.lightened(0.4), 0.6) _kopf.name = "Kopf" _kopf_mat = (_kopf.material_override as StandardMaterial3D).duplicate() _kopf.material_override = _kopf_mat for sx: float in [-1.0, 1.0]: var auge := Prims.box(body, Vector3(sx * 0.11, 2.04, -0.27), Vector3(0.07, 0.11, 0.05), Color(1, 1, 1), false, 3.0) auge.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _augen.append(auge) for i in range(FUNKEN_MAX): var f := Prims.sphere(body, Vector3.ZERO, 0.08, _farbe.lightened(0.3), 2.6) f.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF f.visible = false _funken.append(f) for i in range(3): var k := Prims.sphere(self, Vector3.ZERO, 0.16 - i * 0.04, _farbe, 1.8) k.top_level = true k.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF k.visible = false _schleppe.append(k) _schleppe_pos.append(global_position) pivot = Node3D.new() pivot.position.y = 1.0 add_child(pivot) arm = Node3D.new() arm.rotation.x = PITCH pivot.add_child(arm) cam = Camera3D.new() cam.position.z = ZOOM[0] cam.fov = FOV cam.near = 0.5 arm.add_child(cam) set_mode(Mode.TRAUM) func set_mode(m: int) -> void: mode = m folgt = null collision_layer = 1 # die Maske stellt main je Welt: jede Welt hat ihre Ebene if m == Mode.GEIST: body.scale = Vector3.ONE * 0.6 _schleier_mat.albedo_color.a = 0.3 else: body.scale = Vector3.ONE * 0.8 _schleier_mat.albedo_color.a = 0.55 velocity = Vector3.ZERO func _unhandled_input(event: InputEvent) -> void: if not look_enabled: return if event.is_action_pressed("drehen"): _dreh_rest += DREHSCHRITT elif event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_WHEEL_UP: _zoom_stufe = 0 elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: _zoom_stufe = 1 func _physics_process(delta: float) -> void: var dreh := minf(_dreh_rest, delta * 4.0) yaw += dreh _dreh_rest -= dreh pivot.rotation.y = yaw cam.position.z = lerpf(cam.position.z, float(ZOOM[_zoom_stufe]), 1.0 - pow(0.01, delta)) _beleben(delta) var input := Vector2.ZERO if look_enabled: input.x = Input.get_axis("links", "rechts") input.y = Input.get_axis("vor", "zurueck") var dir := Vector3.ZERO if input.length() > 0.01: var basis_yaw := Basis(Vector3.UP, yaw) dir = (basis_yaw * Vector3(input.x, 0, input.y)).normalized() if mode == Mode.GEIST and folgt != null: if not is_instance_valid(folgt): folgt = null elif dir.length() > 0.01: folgt = null # eigene Bewegung löst die Verfolgung else: _an_der_leine(delta) return var tempo := SPEED_GEIST if mode == Mode.GEIST else SPEED_TRAUM velocity.x = lerpf(velocity.x, dir.x * tempo, 1.0 - pow(0.0005, delta)) velocity.z = lerpf(velocity.z, dir.z * tempo, 1.0 - pow(0.0005, delta)) _fallen(delta) move_and_slide() if global_position.y < -400.0: global_position.y = 2.0 _eingeklemmt(delta, dir.length() > 0.01) if dir.length() > 0.01: var ziel := atan2(-dir.x, -dir.z) body.rotation.y = lerp_angle(body.rotation.y, ziel, 1.0 - pow(0.0005, delta)) ## Wer mit gedrückter Taste zwei Sekunden nicht vorankommt, steckt fest -- ## dann setzt ihn die Stadt auf die nächste Straße. func _eingeklemmt(delta: float, will: bool) -> void: # gemessen am Weg, nicht an der Geschwindigkeit: in einer Ecke schiebt man mit # voller Geschwindigkeit gegen die Wand und kommt doch nicht vom Fleck var weg := Vector2(global_position.x - _vorher.x, global_position.z - _vorher.z).length() _vorher = global_position if mode != Mode.GEIST or not will or weg > 0.3 * delta: _stockt = 0.0 return _stockt += delta if _stockt < 2.0: return _stockt = 0.0 var netz: Netz = HumanDB.NETZ if netz == null or netz.knoten.is_empty(): return var nah: Vector3 = netz.knoten[int(netz.andocken(global_position)["knoten"])] global_position = Vector3(nah.x, 0.3, nah.z) velocity = Vector3.ZERO spruenge += 1 Game.notified.emit("Eingeklemmt -- du stehst wieder auf der Straße.", Color(0.8, 0.85, 1)) func blickrichtung() -> Vector3: return -Vector3(sin(yaw), 0, cos(yaw)) ## Schwerkraft in beiden Welten. Springen kann nur der Läufer im Traum. func _fallen(delta: float) -> void: if is_on_floor(): if mode == Mode.TRAUM and look_enabled and Input.is_action_pressed("steigen"): velocity.y = JUMP else: velocity.y = -1.0 else: velocity.y -= GRAVITY * delta ## An einen Menschen hängen. Den Weg zu ihm findet das Navigationsnetz. func anhaengen(m: Bewohner) -> void: folgt = m _leine_pfad = [] _leine_uhr = 0.0 _verloren = 0.0 ## Folgen heißt: ihm nachgehen, durch dieselben Türen und Treppen. Der Pfad ## kommt vom Navigationsnetz und wird laufend neu geplant. Er läuft so schnell ## wie nötig, um dranzubleiben -- auch bei hohem Tempo. func _an_der_leine(delta: float) -> void: var mensch := folgt as Bewohner _leine_uhr -= delta if _leine_uhr <= 0.0 or _leine_pfad.is_empty(): _leine_uhr = 0.4 _leine_pfad = [] if mensch.nav_map.is_valid(): var pts := NavigationServer3D.map_get_path(mensch.nav_map, global_position, mensch.global_position, true) for i in range(1, pts.size()): _leine_pfad.append(pts[i]) if pts.size() < 2: print(" [kein Weg] Geist von %s zu %s bei %s" % [str(global_position), mensch.data["name"], str(mensch.global_position)]) var abstand := global_position.distance_to(mensch.global_position) var ziel: Vector3 = _leine_pfad[0] if not _leine_pfad.is_empty() else mensch.global_position var d := ziel - global_position d.y = 0.0 if not _leine_pfad.is_empty() and d.length() < 0.5 and absf(ziel.y - global_position.y) < 1.6: _leine_pfad.pop_front() var tempo := maxf(SPEED_GEIST, Bewohner.GEHTEMPO * Game.zeit_faktor() * 1.4) if abstand > FOLGE_ABSTAND and d.length() > 0.05: var v := d.normalized() * minf(tempo, d.length() / delta) velocity.x = v.x velocity.z = v.z body.rotation.y = lerp_angle(body.rotation.y, atan2(-d.x, -d.z), 1.0 - pow(0.001, delta)) else: velocity.x = 0.0 velocity.z = 0.0 _fallen(delta) move_and_slide() # Verloren? Nur wenn das Netz keinen Weg weiß und er zu weit weg ist, # wird er nachgezogen -- ein Notnagel, kein Spielzug. _verloren = _verloren + delta if abstand > LEINE and _leine_pfad.is_empty() else 0.0 if _verloren > 3.0: global_position = mensch.global_position + Vector3(0, 0.3, 0) velocity = Vector3.ZERO _verloren = 0.0 spruenge += 1 ## Schweben, Funken, Schweif und Modusfarbe -- der Zustand ist am Geist ablesbar. func _beleben(delta: float) -> void: var t := Time.get_ticks_msec() * 0.001 var ziel := FARBE_TRAUM if mode == Mode.GEIST: ziel = FARBE_FOLGE if folgt != null else FARBE_GEIST if not Game.situation.is_empty() and fmod(t, 0.5) < 0.12: ziel = ziel.lightened(0.6) _farbe = _farbe.lerp(ziel, 1.0 - pow(0.02, delta)) _schleier_mat.albedo_color = Color(_farbe, _schleier_mat.albedo_color.a) _schleier_mat.emission = _farbe _kern_mat.albedo_color = _farbe _kern_mat.emission = _farbe _kopf_mat.albedo_color = _farbe.lightened(0.4) _kopf_mat.emission = _farbe.lightened(0.4) body.position.y = (SCHWEBE if mode == Mode.GEIST else 0.0) + sin(t * 1.6) * 0.08 _kern.scale = Vector3.ONE * (1.0 + sin(t * 3.1) * 0.12) var tempo_flach := Vector2(velocity.x, velocity.z).length() var neigung := -0.16 * clampf(tempo_flach / SPEED_TRAUM, 0.0, 1.0) body.rotation.x = lerpf(body.rotation.x, neigung, 1.0 - pow(0.01, delta)) var anzahl := clampi(Game.known.size(), 1, FUNKEN_MAX) _kreis += delta * (0.9 + 0.7 * float(Game.tempo - 1)) for i in range(FUNKEN_MAX): var f: MeshInstance3D = _funken[i] f.visible = i < anzahl if not f.visible: continue var w := _kreis + float(i) * TAU / float(anzahl) f.position = Vector3(cos(w) * 0.85, 1.2 + sin(w * 2.0 + float(i)) * 0.3, sin(w) * 0.85) f.scale = Vector3.ONE * (0.8 + 0.3 * sin(t * 4.0 + float(i))) var kopf_welt := body.to_global(Vector3(0, 1.1, 0)) var sichtbar := tempo_flach > 1.0 for i in range(_schleppe.size()): var k: MeshInstance3D = _schleppe[i] var vorher: Vector3 = kopf_welt if i == 0 else _schleppe_pos[i - 1] _schleppe_pos[i] = (_schleppe_pos[i] as Vector3).lerp(vorher, 1.0 - pow(0.0002 * (i + 1), delta)) k.global_position = _schleppe_pos[i] k.visible = sichtbar k.material_override = _kern_mat