308 lines
10 KiB
GDScript
308 lines
10 KiB
GDScript
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 FOLGE_NAH := 5.0 # näher dran braucht es keinen Anlauf
|
|
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 _anlauf: Array = [] # Weg zu dem Menschen, an den man sich gehängt hat
|
|
var _stockt := 0.0 # Sekunden ohne Vorankommen
|
|
var spruenge := 0 # wie oft die Leine nachziehen musste
|
|
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)
|
|
_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
|
|
_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:
|
|
if mode != Mode.GEIST or not will or Vector2(velocity.x, velocity.z).length() > 0.3:
|
|
_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. Steht er weit weg, wird erst der Weg zu ihm
|
|
## geplant: aus dem Haus, über die Straßen, in seine Wohnung.
|
|
func anhaengen(m: Bewohner) -> void:
|
|
folgt = m
|
|
_anlauf = []
|
|
_stockt = 0.0
|
|
if global_position.distance_to(m.global_position) > FOLGE_NAH:
|
|
var ort: String = m.place if m._path.is_empty() else ""
|
|
_anlauf = Orte.route_zu(m.places, m.netz, global_position, m.global_position,
|
|
ort, m.etage(ort) if ort != "" else 0)
|
|
|
|
|
|
## 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
|
|
if not _anlauf.is_empty():
|
|
ziel = _anlauf[0]
|
|
var bis := ziel - global_position
|
|
bis.y = 0.0
|
|
if bis.length() < 0.6 and absf(ziel.y - global_position.y) < 1.6:
|
|
_anlauf.pop_front()
|
|
if _anlauf.is_empty():
|
|
ziel = mensch.spur_punkt(FOLGE_ABSTAND)
|
|
else:
|
|
ziel = _anlauf[0]
|
|
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 d.length() > 0.6 and Vector2(velocity.x, velocity.z).length() < 0.3:
|
|
_stockt += delta
|
|
else:
|
|
_stockt = 0.0
|
|
var zu_weit: bool = _anlauf.is_empty() \
|
|
and global_position.distance_to(mensch.global_position) > LEINE
|
|
if _stockt > 2.0 or zu_weit:
|
|
global_position = ziel # hängen geblieben: die Leine zieht ihn nach
|
|
velocity = Vector3.ZERO
|
|
_stockt = 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
|