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 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.4 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) _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 _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 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 collision_mask = 1 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 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)) 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 ## Folgen heißt: seiner Spur nachgehen, durch dieselben Türen und Treppen. ## 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 var ziel: Vector3 = mensch.spur_punkt(FOLGE_ABSTAND) var d := ziel - global_position d.y = 0.0 var tempo := maxf(SPEED_GEIST, Bewohner.GEHTEMPO * Game.zeit_faktor() * 1.4) if d.length() > 0.3: 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() if global_position.distance_to(mensch.global_position) > LEINE: global_position = ziel # hängen geblieben: die Leine zieht ihn nach velocity = Vector3.ZERO ## 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