454 lines
No EOL
17 KiB
GDScript
454 lines
No EOL
17 KiB
GDScript
extends Camera3D
|
||
## Orbit-Kamera + Freelook-Flug für den Editor-Viewport.
|
||
## Steuerung:
|
||
## Mittlere Maustaste ziehen = Drehen (Orbit)
|
||
## Mittlere Maustaste + Shift = Verschieben (Pan, parallel zum Boden)
|
||
## Mausrad = Zoom — AUSSER ein Objekt/eine Platzier-Vorschau
|
||
## ist aktiv: dann dreht das Mausrad das Objekt
|
||
## (interaction.gd meldet das per wheel_filter).
|
||
## Rechte Maustaste GEDACHT halten = Freelook-Flug (Karte t_c46d31c2): Umsehen +
|
||
## W vor / S zurück / A links / D rechts,
|
||
## Leertaste hoch / Shift runter (Shooter-
|
||
## Konvention, Sly-Wunsch 17.10.),
|
||
## Strg = schneller, Alt = langsamer.
|
||
## Mausrad ändert im Flug die
|
||
## GRUNDgeschwindigkeit dauerhaft.
|
||
## Umschalt+F (main.gd) = Freelook dauerhaft an/aus (Flug bleibt auch
|
||
## ohne gedrückte rechte Maustaste aktiv).
|
||
## F (main.gd) = Auswahl ins Bild zentrieren → focus_on().
|
||
## Orbit bleibt der Default-Zustand: Beim Verlassen des Flugs werden Orbit-
|
||
## Ziel/Winkel/Abstand aus der Flugposition/Blickrichtung zurückgerechnet.
|
||
## Die Mathe lebt in core/editor/fly_math.gd (pure, headless testbar).
|
||
## Ziel-Punkt, Winkel und Abstand bleiben intern; Position wird jede Frame
|
||
## aus ihnen berechnet (Orbit) bzw. im Flug direkt bewegt.
|
||
## Karte K1 (t_950fddaa): Beim Dreh-Start liegt der Drehpunkt UNTER DEM
|
||
## MAUSZEIGER (Unity/Blender-Verhalten) — Objekt/Bauteil-Treffer zuerst, sonst
|
||
## Terrain, sonst alter Ziel-Punkt. Kameraposition und Blickrichtung bleiben
|
||
## exakt unverändert (kein Sprung), nur Ziel/Winkel/Abstand werden neu
|
||
## gerechnet. fit_to_bounds() ersetzt frame_map(): ganze Map ins Bild.
|
||
|
||
const FlyMath := preload("res://core/editor/fly_math.gd")
|
||
const TerrainField := preload("res://core/svmap/terrain_field.gd")
|
||
const SvmapIO := preload("res://core/svmap/svmap_io.gd")
|
||
|
||
const ORBIT_DEG_PER_PX := 0.3 # Karte K1: Drehgeschwindigkeit gekapselt — kein Nachlaufen
|
||
const DISTANCE_MIN := 5.0
|
||
const FIT_FLOOR_MIN := 30.0 # fit_to_bounds: nie näher als 30 m an die Map ran
|
||
## Karte K1d (t_6c1027c1): Startabstand nach Laden/Neu — Slys Wahl („Nah dran:
|
||
## startet ~80 m über der Kartenmitte, Objekte sind sofort groß und greifbar").
|
||
const START_DISTANCE_M := 80.0
|
||
const START_YAW_DEG := 45.0
|
||
const START_PITCH_DEG := 35.0
|
||
|
||
var target := Vector3.ZERO
|
||
var yaw_deg := 45.0
|
||
var pitch_deg := 35.0
|
||
var distance := 300.0
|
||
|
||
## Von main.gd gesetzt: Callable → true, wenn das Mausrad gerade ein Objekt
|
||
## drehen soll (Platzier-Modus oder Auswahl) statt zu zoomen.
|
||
var wheel_filter := Callable()
|
||
## Von main.gd gesetzt: Callable für Klartext-Status-Meldungen.
|
||
var status_cb := Callable()
|
||
|
||
## Grundgeschwindigkeit des Flugs (Mausrad ändert sie dauerhaft, Karte 1/3).
|
||
var base_speed_mps := FlyMath.BASE_SPEED_MPS
|
||
|
||
var _dragging := false
|
||
var _panning := false
|
||
var _fly_held := false # Rechte Maustaste gehalten → Flug temporär aktiv
|
||
var _fly_locked := false # Umschalt+F: Flug dauerhaft (auch ohne RMT)
|
||
var _flying := false # Ergebnis-Flag: gerade im Flugmodus?
|
||
var _fly_yaw := 0.0
|
||
var _fly_pitch := 0.0
|
||
var _fit_bounds_mn := Vector3.ZERO # zuletzt gesetzte Map-Bounds (fit_to_bounds)
|
||
var _fit_bounds_mx := Vector3.ZERO
|
||
var _fit_bounds_valid := false
|
||
|
||
|
||
func _ready() -> void:
|
||
_apply()
|
||
|
||
|
||
func _process(delta: float) -> void:
|
||
if not _flying or not InputMap.has_action("editor_fly_forward"):
|
||
return
|
||
var f := Input.get_action_strength("editor_fly_forward") - Input.get_action_strength("editor_fly_back")
|
||
var s := Input.get_action_strength("editor_fly_right") - Input.get_action_strength("editor_fly_left")
|
||
var v := Input.get_action_strength("editor_fly_up") - Input.get_action_strength("editor_fly_down")
|
||
if f == 0.0 and s == 0.0 and v == 0.0:
|
||
return
|
||
var speed := FlyMath.speed_for(base_speed_mps,
|
||
Input.is_action_pressed("editor_fly_fast"),
|
||
Input.is_action_pressed("editor_fly_slow"))
|
||
var forward := FlyMath.forward_from_euler(_fly_yaw, _fly_pitch)
|
||
var right := FlyMath.forward_from_euler(_fly_yaw, 0.0).cross(Vector3.UP).normalized()
|
||
position += FlyMath.move_delta(forward, right, f, s, v, speed, delta)
|
||
_apply_fly_orientation()
|
||
|
||
|
||
func _unhandled_input(event: InputEvent) -> void:
|
||
if event is InputEventKey:
|
||
# Karte K1: Pos1/Home = ganze Karte ins Bild (fit_to_bounds).
|
||
_handle_home_key(event as InputEventKey)
|
||
if event is InputEventMouseButton:
|
||
var mb := event as InputEventMouseButton
|
||
if mb.button_index == MOUSE_BUTTON_RIGHT:
|
||
# Karte t_2470de89: RMB = Flug mit ABSOLUTEM Vorrang — kein Filter,
|
||
# keine Heuristik. Werkzeuge behandeln RMB nicht mehr, also startet
|
||
# der Flug in JEDEM Werkzeugzustand (auch Terrain/Platzieren/Bauen).
|
||
if mb.pressed:
|
||
_enter_fly()
|
||
_fly_held = true
|
||
elif _fly_held:
|
||
_fly_held = false
|
||
if not _fly_locked:
|
||
_exit_fly()
|
||
elif mb.button_index == MOUSE_BUTTON_MIDDLE:
|
||
if _flying:
|
||
return # Im Flug: MMT-Ziehen bleibt unangetastet (kein Orbit-Sprung)
|
||
if mb.pressed:
|
||
# Karte K1: Drehpunkt unter den Mauszeiger legen — VOR dem
|
||
# ersten Ziehen, damit kein Sprung passiert.
|
||
_pivot_to_cursor(mb.position)
|
||
_dragging = mb.pressed and not mb.shift_pressed
|
||
_panning = mb.pressed and mb.shift_pressed
|
||
elif mb.button_index == MOUSE_BUTTON_WHEEL_UP and mb.pressed:
|
||
if _flying:
|
||
base_speed_mps = FlyMath.wheel_speed(base_speed_mps, true)
|
||
_status("Flug-Tempo: %.0f m/s (Grundgeschwindigkeit, bleibt dauerhaft)." % base_speed_mps)
|
||
return
|
||
if _wheel_blocked():
|
||
return
|
||
distance = maxf(DISTANCE_MIN, distance * 0.9)
|
||
_apply()
|
||
elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN and mb.pressed:
|
||
if _flying:
|
||
base_speed_mps = FlyMath.wheel_speed(base_speed_mps, false)
|
||
_status("Flug-Tempo: %.0f m/s (Grundgeschwindigkeit, bleibt dauerhaft)." % base_speed_mps)
|
||
return
|
||
if _wheel_blocked():
|
||
return
|
||
distance = minf(distance * 1.1, distance_max())
|
||
_apply()
|
||
elif event is InputEventMouseMotion:
|
||
var mm := event as InputEventMouseMotion
|
||
if _flying:
|
||
if _fly_held:
|
||
# Umsehen: volle 360° Yaw, Pitch −85…+85 (auch von unten hoch).
|
||
_fly_yaw = wrapf(_fly_yaw - mm.relative.x * 0.25, -180.0, 180.0)
|
||
_fly_pitch = clampf(_fly_pitch + mm.relative.y * 0.25,
|
||
-FlyMath.PITCH_MAX_DEG, FlyMath.PITCH_MAX_DEG)
|
||
_apply_fly_orientation()
|
||
return
|
||
if _dragging:
|
||
yaw_deg = wrapf(yaw_deg - mm.relative.x * ORBIT_DEG_PER_PX, -180.0, 180.0)
|
||
pitch_deg = clampf(pitch_deg + mm.relative.y * ORBIT_DEG_PER_PX, 5.0, 85.0)
|
||
_apply()
|
||
elif _panning:
|
||
var right := transform.basis.x
|
||
var fwd := -transform.basis.z
|
||
fwd.y = 0.0
|
||
fwd = fwd.normalized()
|
||
var scale := distance * 0.0015
|
||
target -= right * mm.relative.x * scale
|
||
target += fwd * mm.relative.y * scale
|
||
_apply()
|
||
|
||
|
||
func _wheel_blocked() -> bool:
|
||
return wheel_filter.is_valid() and bool(wheel_filter.call())
|
||
|
||
|
||
func distance_max() -> float:
|
||
return 6000.0
|
||
|
||
|
||
## Freelook dauerhaft an/aus (Umschalt+F, main.gd ruft an).
|
||
func toggle_fly_lock() -> void:
|
||
if _flying:
|
||
_fly_locked = false
|
||
if not _fly_held:
|
||
_exit_fly()
|
||
_status("Freelook-Flug aus — zurück zum Orbit (Mittlere Maustaste ziehen = drehen).")
|
||
else:
|
||
_fly_locked = true
|
||
_enter_fly()
|
||
_status("Freelook-Flug DAUERHAFT an: W/S/A/D fliegen, Leertaste hoch / Shift runter, Strg schneller, Alt langsamer — Umschalt+F zurück zum Orbit.")
|
||
|
||
|
||
## Im Flugmodus? (interaction.gd blockt z. B. Mausrad-Objektdrehung dann ab.)
|
||
func fly_active() -> bool:
|
||
return _flying
|
||
|
||
|
||
## Aktueller Gierwinkel der BLICKRICHTUNG (Karte t_55257792): im Flug der
|
||
## Flug-Yaw, im Orbit der Orbit-Yaw. Für die Pfeiltasten-Richtung (MoveMath
|
||
## rastet sie auf die nächste Weltachse).
|
||
func view_yaw_deg() -> float:
|
||
return _fly_yaw if _flying else yaw_deg
|
||
|
||
|
||
## Auswahl ins Bild zentrieren (F): Orbit → Ziel-Punkt auf den Auswahl-Mittelpunkt;
|
||
## Flug → Blickrichtung drehen, Position bleibt.
|
||
func focus_on(center: Vector3) -> void:
|
||
if _flying:
|
||
var to := center - position
|
||
if to.length_squared() < 0.01:
|
||
return
|
||
var ep := FlyMath.euler_from_forward(to.normalized())
|
||
_fly_yaw = ep.x
|
||
_fly_pitch = clampf(ep.y, -FlyMath.PITCH_MAX_DEG, FlyMath.PITCH_MAX_DEG)
|
||
_apply_fly_orientation()
|
||
else:
|
||
target = center
|
||
_apply()
|
||
|
||
|
||
func _enter_fly() -> void:
|
||
if _flying:
|
||
return
|
||
_fly_yaw = yaw_deg
|
||
_fly_pitch = pitch_deg
|
||
_flying = true
|
||
# Konflikt-Check (Sly-Wunsch 17.10.): Leertaste ist jetzt „hoch fliegen" —
|
||
# ein fokussierter Button würde Space/Enter als Klick interpretieren
|
||
# (Bibliothek-Button = Platzierung starten). Im Flug gehört die Leertaste
|
||
# der Kamera, also GUI-Fokus freigeben.
|
||
var vp := get_viewport()
|
||
if vp != null and vp.gui_get_focus_owner() != null:
|
||
vp.gui_release_focus()
|
||
_status("Freelook-Flug an: W/S/A/D fliegen, Leertaste hoch / Shift runter, Strg = schnell, Alt = langsam, Mausrad = Grund-Tempo. Rechte Maustaste loslassen = zurück zum Orbit.")
|
||
|
||
|
||
func _exit_fly() -> void:
|
||
if not _flying:
|
||
return
|
||
_flying = false
|
||
# Orbit-Zustand aus Flugposition/Blickrichtung zurückrechnen — kein Sprung.
|
||
var fwd := -transform.basis.z
|
||
if fwd.length_squared() < 0.0000001:
|
||
fwd = FlyMath.forward_from_euler(yaw_deg, pitch_deg)
|
||
var ep := FlyMath.euler_from_forward(fwd)
|
||
yaw_deg = ep.x
|
||
pitch_deg = clampf(ep.y, 5.0, 85.0)
|
||
distance = clampf(distance, 30.0, distance_max())
|
||
target = position + fwd.normalized() * distance
|
||
_apply()
|
||
|
||
|
||
func _apply_fly_orientation() -> void:
|
||
var fwd := FlyMath.forward_from_euler(_fly_yaw, _fly_pitch)
|
||
look_at_from_position(position, position + fwd, Vector3.UP)
|
||
|
||
|
||
## Karte K1 (t_950fddaa): Drehpunkt unter den Mauszeiger — aus der unver-
|
||
## änderten Kameraposition werden Ziel/Winkel/Abstand neu gerechnet.
|
||
func set_pivot(hit: Vector3) -> void:
|
||
var pos := position
|
||
var fwd := -transform.basis.z
|
||
if fwd.length_squared() < 0.0000001:
|
||
fwd = FlyMath.forward_from_euler(yaw_deg, pitch_deg)
|
||
var ep := FlyMath.euler_from_forward(fwd.normalized())
|
||
target = hit
|
||
distance = pos.distance_to(hit)
|
||
yaw_deg = ep.x
|
||
pitch_deg = ep.y
|
||
# Bewusst KEIN _apply(): Position und Basis bleiben exakt wie vorher —
|
||
# kein Sprung. Der nächste Dreh-/Zoom-Schritt rechnet aus dem neuen
|
||
# Zustand dieselbe Position zurück (Offset-Konvention konsistent).
|
||
|
||
|
||
## MMT-Druck: Punkt unter dem Cursor bestimmen — Objekt/Bauteil zuerst, sonst
|
||
## Terrain/Boden, sonst aktuellen Ziel-Punkt behalten (kein Sprung).
|
||
func _pivot_to_cursor(viewport_pos: Vector2) -> void:
|
||
var inter := _find_sibling("_pick_object")
|
||
if inter != null:
|
||
var idx := int(inter.call("_pick_object", viewport_pos))
|
||
if idx >= 0:
|
||
var pt: Variant = _object_pivot_point(idx)
|
||
if pt != null:
|
||
set_pivot(pt)
|
||
return
|
||
var g: Variant = inter.call("_ground_point", viewport_pos)
|
||
if g != null:
|
||
set_pivot(g)
|
||
return
|
||
# Kein Treffer: Ziel-Punkt unverändert lassen — Ansicht darf nicht springen.
|
||
|
||
|
||
## Mittelpunkt des Objekt-Markers (Fußpunkt + halbe Höhe — gleiche Konvention
|
||
## wie interaction.gd beim F-Tasten-Zentrieren).
|
||
func _object_pivot_point(idx: int) -> Variant:
|
||
var builder := _find_builder()
|
||
if builder == null:
|
||
return null
|
||
var markers: Array = builder.get("object_markers")
|
||
if markers == null or idx < 0 or idx >= (markers as Array).size():
|
||
return null
|
||
var marker = (markers as Array)[idx]
|
||
if marker == null or not is_instance_valid(marker):
|
||
return null
|
||
var half_h := float(marker.get_meta("half_height", 0.0))
|
||
return (marker as Node3D).global_position + Vector3.UP * half_h
|
||
|
||
|
||
func _find_sibling(has_method_name: String) -> Node:
|
||
var p := get_parent()
|
||
if p == null:
|
||
return null
|
||
for child in p.get_children():
|
||
var c := child as Node
|
||
if c != self and c.has_method(has_method_name):
|
||
return c
|
||
return null
|
||
|
||
|
||
func _find_builder() -> Node:
|
||
var p := get_parent()
|
||
if p == null:
|
||
return null
|
||
for child in p.get_children():
|
||
var c := child as Node
|
||
if c != self and c.get("object_markers") != null:
|
||
return c
|
||
return null
|
||
|
||
|
||
## Fit: target = Mitte der bounds; Abstand so, dass die GANZE Map im Frustum
|
||
## liegt; Pos1 ruft das mit den zuletzt gesetzten Bounds erneut auf. Ersetzt
|
||
## das tote frame_map().
|
||
func fit_to_bounds(mn: Vector3, mx: Vector3) -> void:
|
||
_fit_bounds_mn = Vector3(minf(mn.x, mx.x), minf(mn.y, mx.y), minf(mn.z, mx.z))
|
||
_fit_bounds_mx = Vector3(maxf(mn.x, mx.x), maxf(mn.y, mx.y), maxf(mn.z, mx.z))
|
||
_fit_bounds_valid = true
|
||
_fly_held = false
|
||
_fly_locked = false
|
||
_flying = false
|
||
var span := _fit_bounds_mx - _fit_bounds_mn
|
||
target = _fit_bounds_mn + span * 0.5
|
||
# Karte K1: Abstand über Umkugel-Formel — ALLE 8 Ecken sicher im Bild.
|
||
# (Reiner Faktor der Kantenlänge reicht bei flachen Maps nicht: die nahe
|
||
# Oberecke liegt dann fast seitlich zur Blickachse.)
|
||
distance = clampf(_fit_distance(span.length() * 0.5),
|
||
FIT_FLOOR_MIN, distance_max())
|
||
yaw_deg = 45.0
|
||
pitch_deg = 35.0
|
||
_apply()
|
||
|
||
|
||
## Karte K1d (t_6c1027c1, Neuauflage von K1c): Startansicht NAH am Karteninhalt.
|
||
## Laden/Neu setzt die Kamera auf die KARTENMITTE (XZ-Mitte der Map-Bounds,
|
||
## Höhe = Gelände dort) mit festem Abstand START_DISTANCE_M (80 m), yaw 45°,
|
||
## pitch 35° — Objekte sind
|
||
## sofort groß und greifbar (Slys Wahl), statt die 1-km-Fläche von weitem zu
|
||
## zeigen. Die Regel ist bewusst UNABHÄNGIG von Größe/Lage des Inhalts: immer
|
||
## 80 m. Pos1 (Home) bleibt die GANZE Karte — dafür werden hier die MAP-Bounds
|
||
## gemerkt (NICHT die Content-Bounds), sodass fit_to_bounds danach die Karte
|
||
## einpasst; Startansicht und Pos1 sind also zwei verschiedene Ansichten.
|
||
func fit_to_document(doc: Dictionary, map_mn: Vector3, map_mx: Vector3) -> void:
|
||
var mn := Vector3(minf(map_mn.x, map_mx.x), minf(map_mn.y, map_mx.y),
|
||
minf(map_mn.z, map_mx.z))
|
||
var mx := Vector3(maxf(map_mn.x, map_mx.x), maxf(map_mn.y, map_mx.y),
|
||
maxf(map_mn.z, map_mx.z))
|
||
_fly_held = false
|
||
_fly_locked = false
|
||
_flying = false
|
||
# Pos1 = ganze Karte: MAP-Bounds merken (NICHT die Content-Bounds).
|
||
_fit_bounds_mn = mn
|
||
_fit_bounds_mx = mx
|
||
_fit_bounds_valid = true
|
||
# Kartenmitte in XZ — in y aber die GELÄNDE-Oberfläche, nicht die Mitte der
|
||
# Bounds: die Bounds reichen bei neuen Maps von −10 bis 80 m, ihre Mitte
|
||
# (35 m) liegt 35 m über dem Boden. Damit hätte die Ansicht 35 m über den
|
||
# Boden gezielt und die Objekte stünden am unteren Bildrand.
|
||
var mitte := mn + (mx - mn) * 0.5
|
||
target = Vector3(mitte.x, SvmapDocument.terrain_height_at(doc, mitte.x, mitte.z), mitte.z)
|
||
distance = clampf(START_DISTANCE_M, DISTANCE_MIN, distance_max())
|
||
yaw_deg = START_YAW_DEG
|
||
pitch_deg = START_PITCH_DEG
|
||
_apply()
|
||
var count := int(content_bounds(doc)["count"])
|
||
if count == 0:
|
||
_status("Ansicht: Kartenmitte, Abstand %d m (kein Inhalt — Pos1 = ganze Karte)." %
|
||
int(START_DISTANCE_M))
|
||
else:
|
||
_status("Ansicht: Kartenmitte, Abstand %d m (Pos1 = ganze Karte)." %
|
||
int(START_DISTANCE_M))
|
||
|
||
|
||
## Bounding-Box des Dokument-INHALTS: Objekte (inkl. Bauteile) und
|
||
## Spawnpunkte — Terrain und Map-Bounds zählen NICHT (Karte K1b).
|
||
## Rückgabe: {mn: Vector3, mx: Vector3, count: int}; count 0 = leeres Dokument.
|
||
static func content_bounds(doc: Dictionary) -> Dictionary:
|
||
var mn := Vector3(INF, INF, INF)
|
||
var mx := Vector3(-INF, -INF, -INF)
|
||
var count := 0
|
||
for p: Vector3 in _content_points(doc.get("objects", []) as Array):
|
||
mn = mn.min(p)
|
||
mx = mx.max(p)
|
||
count += 1
|
||
var zones: Dictionary = doc.get("spawn_zones", {}) as Dictionary
|
||
for team_key in zones:
|
||
var zone: Dictionary = zones[team_key] if zones[team_key] is Dictionary else {}
|
||
for p: Vector3 in _content_points(zone.get("points", []) as Array):
|
||
mn = mn.min(p)
|
||
mx = mx.max(p)
|
||
count += 1
|
||
if count == 0:
|
||
return {"mn": Vector3.ZERO, "mx": Vector3.ZERO, "count": 0}
|
||
return {"mn": mn, "mx": mx, "count": count}
|
||
|
||
|
||
## pos-Felder [x, y, z] aus einer Dokument-Liste lesen; defekte Einträge
|
||
## überspringen (stillschweigend, nie ein harter Fehler beim Laden).
|
||
static func _content_points(items: Array) -> Array:
|
||
var pts: Array = []
|
||
for item in items:
|
||
if not (item is Dictionary):
|
||
continue
|
||
var pos: Variant = (item as Dictionary).get("pos")
|
||
if pos is Array and (pos as Array).size() >= 3:
|
||
pts.append(Vector3(float(pos[0]), float(pos[1]), float(pos[2])))
|
||
return pts
|
||
|
||
|
||
## Abstand, damit die Umkugel (Radius um den Ziel-Punkt) komplett sichtbar
|
||
## bleibt: d = R / sin(halber FOV der SCHMALEREN Bildachse) + 2 % Reserve.
|
||
func _fit_distance(radius: float) -> float:
|
||
var vfov_half := deg_to_rad(fov * 0.5) # fov ist vertikal (KEEP_HEIGHT)
|
||
var aspect := 16.0 / 9.0
|
||
var vp := get_viewport()
|
||
if vp != null:
|
||
var vs := vp.get_visible_rect().size
|
||
if vs.y > 0.0:
|
||
aspect = vs.x / vs.y
|
||
var hfov_half := atan(aspect * tan(vfov_half))
|
||
var half := minf(vfov_half, hfov_half)
|
||
if half <= 0.0 or half >= PI * 0.5:
|
||
half = deg_to_rad(37.5)
|
||
return maxf(radius, 1.0) / sin(half) * 1.02
|
||
|
||
|
||
func _handle_home_key(key: InputEventKey) -> void:
|
||
if key.pressed and not key.echo and key.keycode == KEY_HOME:
|
||
if _fit_bounds_valid:
|
||
fit_to_bounds(_fit_bounds_mn, _fit_bounds_mx)
|
||
_status("Pos1: ganze Karte ins Bild.")
|
||
|
||
|
||
func _apply() -> void:
|
||
var pitch := deg_to_rad(pitch_deg)
|
||
var yaw := deg_to_rad(yaw_deg)
|
||
var offset := Vector3(
|
||
sin(yaw) * cos(pitch),
|
||
sin(pitch),
|
||
cos(yaw) * cos(pitch)
|
||
) * distance
|
||
position = target + offset
|
||
look_at(target, Vector3.UP)
|
||
|
||
|
||
func _status(text: String) -> void:
|
||
if status_cb.is_valid():
|
||
status_cb.call(text) |