536 lines
21 KiB
GDScript
536 lines
21 KiB
GDScript
extends Node
|
||
## Lauf-Modus (Karte t_1cdc1a72): erste Person IM Map-Builder selbst —
|
||
## kein Client-Ordner, kein zweiter Prozess, kein VR/OpenXR, kein Server.
|
||
##
|
||
## Verantwortung dieses Zustands:
|
||
## - Beim Start: die map_view_builder-Ausgabe (Modelle + Pack-Texturen)
|
||
## 1:1 weiterverwenden (der Node3D-Baum bleibt stehen, nur Editor-UI/Input
|
||
## geht weg), Maus-Capture-Wunsch setzen, Panels ausblenden.
|
||
## - Kollider-Bau via KolliderBau (core/svmap/kollider_bau.gd — 1:1-Spiegel
|
||
## des Quest-Clients, Karte t_1da96275): Terrain-Trimesh + Objekt-Kollider
|
||
## (box/kugel/zylinder/verbund/trimesh inkl. library/kollisions-Quellen)
|
||
## + Bauteil-Trimeshe (bau_*-Objekte: Treppen/Türen/Wände begehbar
|
||
## wie echte Geometrie).
|
||
## - PlaytestBody (CharacterBody3D, IDENTISCHE Parameter wie der
|
||
## Testmap-Spieler inkl. Step-Up 0.45 m) anlegen, Terrain-Raster EINMAL
|
||
## dekodieren, Start = erster Spawnpunkt (Team Blau), sonst Kartenmitte.
|
||
## - ESC → exit_requested → main.gd stellt den Editor-Zustand 1:1 wieder her
|
||
## (Panels, Maus, Kamera, Auswahl, Dokument, Undo-Stapel unverändert).
|
||
## - HUD (Position, FPS, „ESC = zurück zum Editor") als CanvasLayer-Overlay.
|
||
##
|
||
## Bewusst KEIN _physics_process hier: alle Bewegung/Eingabe lebt im
|
||
## PlaytestBody; dieser Zustand managed nur Lifecycle + HUD.
|
||
|
||
const BODY_SCRIPT := "res://src/editor/playtest/playtest_body.gd"
|
||
const KOLLIDER_SCRIPT := "res://core/svmap/kollider_bau.gd"
|
||
const AVATAR_SCRIPT := "res://src/editor/playtest/playtest_avatar.gd"
|
||
|
||
## Der laufende Spieler (PlaytestBody, CharacterBody3D mit echter Physik)
|
||
var body: Node3D = null
|
||
## Welt-Kollider (Terrain + Objekte + Bauteile) — Kind dieses Modus,
|
||
## wird mit exit() abgeräumt.
|
||
var _kollider_root: Node3D = null
|
||
|
||
## Editor-UI-Verweise (vom main.gd-Handler gesetzt): die Panels, die
|
||
## AUSGEBLENDET werden sollen (Variant statt Control-Typ — keine UI-Kopie).
|
||
var ui_panels: Array = [] # Array[Control]
|
||
var ui_status: Variant = null # Statuszeilen-Label (nur visible umschalten)
|
||
var help_label: Variant = null # Hilfe-Label (nur visible umschalten)
|
||
|
||
## Maus-Capture-Wunsch (Suite testet diesen Zustand)
|
||
var _want_capture := false
|
||
## HUD-Elemente
|
||
var hud: CanvasLayer = null
|
||
var hud_pos: Label = null
|
||
var hud_fps: Label = null
|
||
var hud_hint: Label = null
|
||
var hud_msg: Label = null # Klartext-Nachricht (z. B. Test-Start via T)
|
||
var hud_dir: Label = null # Blickrichtung + Höhe (Karte t_aaff5dcc)
|
||
## Hilfe-Overlay (F1, Karte t_aaff5dcc): alle Tasten im Klartext.
|
||
var help_layer: CanvasLayer = null
|
||
## Kartenmitte/Achsen-Linien (Taste X, Karte t_aaff5dcc — praktisch beim
|
||
## gespiegelten Bauen, Karte M1): X-Rot/Z-Blau/Y-Grün durch die Kartenmitte.
|
||
var _axis_node: Node3D = null
|
||
## FPS-Smoothing (Anzeige, keine Physik)
|
||
var _fps_smooth := 0.0
|
||
## Ist der Lauf-Modus gerade aktiv? (Reentrancy-Schutz für enter/exit)
|
||
var active := false
|
||
## Avatar-Modelle waren beim Start verfügbar? (Karte t_17a35c59 — ohne
|
||
## Client-Klon bleibt Third Person gesperrt, graceful Fallback.)
|
||
var _avatar_da := false
|
||
|
||
signal exit_requested
|
||
|
||
|
||
## Starten: übernimmt die Sicht (Panels aus, Maus-Capture-Wunsch an) und
|
||
## legt den Spieler am Startpunkt an. Fallback-Reihenfolge (Karte t_96e3c4ee,
|
||
## je mit Klartext-Hinweis): gesetzter Test-Start (start_override) → erster
|
||
## Spawnpunkt (Team Blau) → Kartenmitte. start_override = Vector3 (nur x/z
|
||
## zählen, Höhe kommt automatisch vom Terrain) oder null (= altes Verhalten).
|
||
## view_builder = der map_view_builder-Node (Marker-Tree mit pick_radius/
|
||
## half_height-Metas bleibt stehen und liefert die Hindernis-Blöcke).
|
||
func enter(doc: Dictionary, view_builder: Node3D, start_override: Variant = null) -> void:
|
||
if active:
|
||
return
|
||
active = true
|
||
_set_ui_visible(false)
|
||
_set_status_capture(true)
|
||
|
||
body = load(BODY_SCRIPT).new()
|
||
body.call("set_terrain", doc)
|
||
var start := _spawn_start(doc)
|
||
if start_override != null:
|
||
var sp := start_override as Vector3
|
||
start = Vector3(sp.x, 0.0, sp.z)
|
||
# Spawn AUF der Terrainhöhe: Kapsel-Mitte = Füße + Halbhöhe (+ Toleranz,
|
||
# die Physik lässt den Body sanft auf den Trimesh fallen — wie im Spiel).
|
||
body.position = Vector3(start.x,
|
||
body.terrain_height(start.x, start.z) + PlaytestBody.HALF_H + 0.05,
|
||
start.z)
|
||
if start_override == null:
|
||
var first := _first_spawn(doc)
|
||
if first.has(SpawnOps.YAW_KEY):
|
||
body.yaw_deg = float(first[SpawnOps.YAW_KEY])
|
||
# Kollider 1:1 vom Quest-Client (Karte t_1da96275): Terrain-Trimesh +
|
||
# Objekt-Kollider aus der Registry + Bauteil-Trimeshe. Alles als Kind
|
||
# dieses Modus — exit() räumt es mit ab.
|
||
_baue_kollider(doc)
|
||
add_child(body)
|
||
# Maus-Empfindlichkeit aus den Einstellungen (Karte t_aaff5dcc): der
|
||
# gespeicherte Multiplikator (0,1–5,0) skaliert die Body-Basis.
|
||
body.call("set_sensitivity", PlaytestSettings.load_sens())
|
||
|
||
# Playtest-Avatar (Karte t_17a35c59): derselbe Rig wie die Bots im Spiel
|
||
# (GLB aus dem Nachbar-Klon ../scopedvr-client). Fehlt der Klon/Modell:
|
||
# graceful — First Person bleibt, Third Person wird per Meldung gesperrt.
|
||
var avatar: Node3D = load(AVATAR_SCRIPT).call("lade_avatar") as Node3D
|
||
_avatar_da = avatar != null
|
||
if _avatar_da:
|
||
body.call("set_avatar", avatar)
|
||
# Persistierte Perspektive (Taste V): Third nur mit Avatar — sonst
|
||
# First Person wie bisher + Klartext-Hinweis.
|
||
if PlaytestSettings.load_third():
|
||
if _avatar_da:
|
||
body.call("set_perspektive", true)
|
||
else:
|
||
show_message("Gespeicherte Third-Person-Einstellung ist ohne Avatar-Modelle nicht verfügbar (Client-Klon ../scopedvr-client fehlt) — First Person aktiv.")
|
||
|
||
_build_hud()
|
||
exit_requested.connect(_on_exit_requested, CONNECT_ONE_SHOT)
|
||
|
||
|
||
## Zurück in den Editor: HUD weg, Spieler weg, Panels sichtbar.
|
||
## Editor-Zustand (Kamera, Auswahl, Dokument, Undo-Stapel) wird NICHT hier
|
||
## angefasst — das macht main.gd nach dem Signal.
|
||
func exit() -> void:
|
||
if not active:
|
||
return
|
||
active = false
|
||
_set_status_capture(false)
|
||
_set_ui_visible(true)
|
||
if body != null:
|
||
body.queue_free()
|
||
body = null
|
||
if _kollider_root != null:
|
||
_kollider_root.queue_free()
|
||
_kollider_root = null
|
||
if hud != null:
|
||
hud.queue_free()
|
||
hud = null
|
||
if help_layer != null:
|
||
help_layer.queue_free()
|
||
help_layer = null
|
||
if _axis_node != null:
|
||
_axis_node.queue_free()
|
||
_axis_node = null
|
||
|
||
|
||
func _on_exit_requested() -> void:
|
||
exit()
|
||
|
||
|
||
## Status/Maus-Capture-Wunsch: nur der Zustand (Suite testet wants_capture).
|
||
func _set_status_capture(want: bool) -> void:
|
||
_want_capture = want
|
||
|
||
|
||
## Panelselemente ausblenden/einblenden. Nur die als ui_panels übergebenen
|
||
## Controls werden angefasst — keine weiteren.
|
||
func _set_ui_visible(vis: bool) -> void:
|
||
for p in ui_panels:
|
||
if p is Control:
|
||
(p as Control).visible = vis
|
||
if ui_status is Control:
|
||
(ui_status as Control).visible = vis
|
||
if help_label is Control:
|
||
(help_label as Control).visible = vis
|
||
|
||
|
||
## Start-Punkt: erster Spawn-Punkt aus dem Dokument (spawn_zones-Team „0",
|
||
## erster Eintrag der points-Liste); sonst Kartenmitte aus den Bounds.
|
||
## doc-Format (svmap): {"spawn_zones": {"0": {"points": [{"id", "pos": [x,y,z]}]}, ...}}
|
||
func _first_spawn(doc: Dictionary) -> Dictionary:
|
||
var team0 = (doc.get("spawn_zones", {}) as Dictionary).get("0", {})
|
||
if team0 is Dictionary:
|
||
var points: Array = (team0 as Dictionary).get("points", [])
|
||
if not points.is_empty() and points[0] is Dictionary:
|
||
return points[0]
|
||
return {}
|
||
|
||
|
||
func _spawn_start(doc: Dictionary) -> Vector3:
|
||
var zones: Dictionary = doc.get("spawn_zones", {})
|
||
var team0 = zones.get("0", {})
|
||
if team0 is Dictionary:
|
||
var points: Array = (team0 as Dictionary).get("points", [])
|
||
if not points.is_empty() and points[0] is Dictionary:
|
||
var pos = (points[0] as Dictionary).get("pos", [])
|
||
if pos is Array and (pos as Array).size() >= 3:
|
||
return Vector3(float(pos[0]), 0.0, float(pos[2]))
|
||
var bounds: Dictionary = doc.get("bounds", {})
|
||
var mn: Array = bounds.get("min", [-50.0, -10.0, -50.0])
|
||
var mx: Array = bounds.get("max", [50.0, 80.0, 50.0])
|
||
return Vector3((float(mn[0]) + float(mx[0])) * 0.5, 0.0,
|
||
(float(mn[2]) + float(mx[2])) * 0.5)
|
||
|
||
|
||
## Echter Kollider-Bau (Karte t_1da96275 — „Editor-Playtest-Physik = Quest 3
|
||
## identisch"): der alte AABB-Blocking-Code ist ENTFERNT. Es gibt genau EINEN
|
||
## Kollider-Pfad, denselben wie der Quest-Client:
|
||
## - Terrain + Registry-Objekte: KolliderBau.baue_welt_kollider (1:1-Spiegel
|
||
## von svmap_world.gd — box/kugel/zylinder/verbund/trimesh inkl.
|
||
## library/kollisions-Quelldateien).
|
||
## - Bauteile (bau_*) sind normale Registry-Objekte mit Trimesh aus
|
||
## library/kollisions — Türen/Fenster bleiben Durchgänge, Treppen begehbar.
|
||
func _baue_kollider(doc: Dictionary) -> void:
|
||
var kb := load(KOLLIDER_SCRIPT)
|
||
_kollider_root = kb.call("baue_welt_kollider", doc)
|
||
add_child(_kollider_root)
|
||
|
||
|
||
# ------------------------------------------------------------------- HUD ----
|
||
|
||
func _build_hud() -> void:
|
||
hud = CanvasLayer.new()
|
||
hud.layer = 100
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 16)
|
||
margin.add_theme_constant_override("margin_top", 16)
|
||
margin.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||
hud.add_child(margin)
|
||
var vbox := VBoxContainer.new()
|
||
margin.add_child(vbox)
|
||
hud_pos = Label.new()
|
||
hud_fps = Label.new()
|
||
hud_hint = Label.new()
|
||
hud_msg = Label.new()
|
||
hud_dir = Label.new()
|
||
hud_hint.text = "WASD + Maus · Shift = rennen · Space = springen/steigen · G = Fliegen · V = 1./3.-Person · 1-4 = Spawnpunkt · T = Test-Start · H = HUD · X = Achsen · F1 = Hilfe · ESC = zurück zum Editor"
|
||
hud_msg.text = ""
|
||
for l: Label in [hud_pos, hud_dir, hud_fps, hud_hint, hud_msg]:
|
||
l.add_theme_font_size_override("font_size", 16)
|
||
l.add_theme_color_override("font_color", Color(0.55, 0.8, 1.0))
|
||
vbox.add_child(l)
|
||
add_child(hud)
|
||
|
||
|
||
## Klartext-Nachricht im HUD (die Editor-Statuszeile ist im Lauf-Modus
|
||
## ausgeblendet — hier landet das Feedback für Taste T, Karte t_96e3c4ee).
|
||
func show_message(text: String) -> void:
|
||
if hud_msg != null and is_instance_valid(hud_msg):
|
||
hud_msg.text = text
|
||
|
||
|
||
## Taste T im Testlauf (Karte t_96e3c4ee, „jederzeit“): setzt den Test-Start
|
||
## auf den Punkt unterm Fadenkreuz und versetzt den Spieler SOFORT dorthin
|
||
## (Augenhöhe 1,70 m via look_cam-Offset, Kollision geprüft — nie in Objekten).
|
||
## Das Speichern (Sidecar) + Marker-Update macht main.gd mit der Rückgabe.
|
||
## Rückgabe: {ok, x, z, ground, msg} aus PlaytestBody.teleport_to_ground.
|
||
func teleport_crosshair() -> Dictionary:
|
||
if not active or body == null or body.look_cam == null:
|
||
return {"ok": false, "x": 0.0, "z": 0.0, "ground": 0.0, "msg": ""}
|
||
var vp: Viewport = body.look_cam.get_viewport()
|
||
var center: Vector2 = (vp.get_visible_rect().size if vp != null else Vector2(800, 600)) * 0.5
|
||
var origin: Vector3 = body.look_cam.project_ray_origin(center)
|
||
var dir: Vector3 = body.look_cam.project_ray_normal(center)
|
||
var res: Dictionary = body.call("teleport_to_ground", origin, dir)
|
||
if bool(res.get("ok")):
|
||
show_message("Test-Start gesetzt: x=%.0f z=%.0f — du bist jetzt dort (Höhe automatisch)." % [
|
||
float(res.get("x")), float(res.get("z"))])
|
||
else:
|
||
show_message(str(res.get("msg", "")))
|
||
return res
|
||
|
||
|
||
func _process(delta: float) -> void:
|
||
if not active or body == null:
|
||
return
|
||
_speise_avatar()
|
||
# FPS glätten (Anzeige-Genauigkeit, keine Physik-Betroffenheit)
|
||
if _fps_smooth <= 0.0:
|
||
_fps_smooth = 1.0 / maxf(delta, 0.0001)
|
||
else:
|
||
_fps_smooth = lerpf(_fps_smooth, 1.0 / maxf(delta, 0.0001), 0.1)
|
||
if hud_pos != null:
|
||
hud_pos.text = "Pos: %.1f, %.1f, %.1f" % [body.position.x, body.position.y, body.position.z]
|
||
if hud_dir != null:
|
||
# Höhe = FUSSPUNKT (Kapsel-Mitte minus Halbhöhe) — direkt vergleichbar
|
||
# mit der Terrainhöhe unter den Füßen (Karte t_1da96275).
|
||
hud_dir.text = "Blick: %.0f° / %.0f° · Höhe: %.1f m" % [
|
||
float(body.get("yaw_deg")), float(body.get("pitch_deg")),
|
||
body.position.y - PlaytestBody.HALF_H]
|
||
if hud_fps != null:
|
||
hud_fps.text = "FPS: %d" % int(round(_fps_smooth))
|
||
|
||
|
||
## Maus-Capture-Wunsch für die Suite/Debug sichtbar machen.
|
||
func wants_capture() -> bool:
|
||
return _want_capture
|
||
|
||
|
||
## ESC → Signal an main.gd (die Esc-Priorität gehört in main.gd, damit die
|
||
## normale Esc-Kette im Editor-Modus unangetastet bleibt).
|
||
func handle_escape() -> void:
|
||
if active and body != null:
|
||
exit_requested.emit()
|
||
|
||
|
||
# ------------------------------------------------- Komfort (t_aaff5dcc) -----
|
||
|
||
## Tasten im Lauf-Modus (Karte t_aaff5dcc): G = Fliegen/God-Mode, 1–4 =
|
||
## Sprung zu Spawnpunkt, H = HUD an/aus, F1 = Hilfe, X = Achsen. ESC/T bleiben
|
||
## bewusst bei main.gd (Esc-Kette + Test-Start-Vertrag). Die Editor-Tasten
|
||
## sind im Lauf-Modus eingefroren (main gibt hier nichts anderes durch).
|
||
func _unhandled_input(event: InputEvent) -> void:
|
||
if not active or body == null:
|
||
return
|
||
var key_evt := event as InputEventKey
|
||
if key_evt == null or not key_evt.is_pressed() or key_evt.echo:
|
||
return
|
||
var key := key_evt.keycode
|
||
var handled := true
|
||
match key:
|
||
KEY_G:
|
||
toggle_fly()
|
||
KEY_V:
|
||
toggle_perspektive()
|
||
KEY_1:
|
||
jump_to_spawn(1)
|
||
KEY_2:
|
||
jump_to_spawn(2)
|
||
KEY_3:
|
||
jump_to_spawn(3)
|
||
KEY_4:
|
||
jump_to_spawn(4)
|
||
KEY_H:
|
||
toggle_hud()
|
||
KEY_F1:
|
||
toggle_help()
|
||
KEY_X:
|
||
toggle_axis()
|
||
_:
|
||
handled = false
|
||
if handled:
|
||
get_viewport().set_input_as_handled()
|
||
|
||
|
||
## Empfindlichkeit live setzen (aus dem Einstellungen-Panel, Registry-Weg —
|
||
## main.gd bleibt unangetastet). mult = Multiplikator 0,1–5,0.
|
||
func apply_sensitivity(mult: float) -> void:
|
||
if body != null:
|
||
body.call("set_sensitivity", mult)
|
||
|
||
|
||
## Taste V (Karte t_17a35c59): First/Third-Person umschalten. Der neue
|
||
## Zustand wird persistiert (PlaytestSettings, wirksam über Neustarts).
|
||
## Ohne Avatar-Modelle (kein Client-Klon ../scopedvr-client) bleibt First
|
||
## Person aktiv und der Grund steht im HUD — kein Crash (graceful Fallback).
|
||
func toggle_perspektive() -> bool:
|
||
if body == null:
|
||
return false
|
||
var neu: bool = body.call("toggle_perspektive")
|
||
if not _avatar_da:
|
||
show_message("Kein Avatar-Modell gefunden (Client-Klon ../scopedvr-client fehlt) — Third Person nicht verfügbar.")
|
||
return false
|
||
PlaytestSettings.save_third(neu)
|
||
if neu:
|
||
show_message("Dritte Person AN — derselbe Avatar wie die Bots im Spiel. V = zurück zur ersten Person.")
|
||
else:
|
||
show_message("Erste Person AN (Standard). V = dritte Person.")
|
||
return neu
|
||
|
||
|
||
## Avatar-Gait aus der ECHTEN Bewegung speisen (Kartenvorgabe: Stillstand/
|
||
## Walk/Jog aus der tatsächlichen Geschwindigkeit, Blickrichtung ==
|
||
## Bewegungsrichtung über die Body-Rotation). Jeden Frame — die Kadenz-
|
||
## Mathe des Avatars glättet selbst (BLEND_SMOOTH_S, wie die Bots).
|
||
func _speise_avatar() -> void:
|
||
if body == null:
|
||
return
|
||
var av: Node3D = body.get("avatar") as Node3D
|
||
if av == null or not is_instance_valid(av):
|
||
return
|
||
var speed: float = body.call("horizontale_geschwindigkeit")
|
||
av.call("apply_state", PlaytestAvatar.zustand_fuer(speed), speed)
|
||
|
||
|
||
## Fliegen / God-Mode umschalten (Taste G). Rückgabe: neuer Zustand.
|
||
func toggle_fly() -> bool:
|
||
if body == null:
|
||
return false
|
||
var new_state: bool = not bool(body.get("fly_mode"))
|
||
body.set("fly_mode", new_state)
|
||
body.set("velocity", Vector3.ZERO) # kein Fall-Übernehmen aus dem Gang
|
||
if new_state:
|
||
show_message("Fliegen AN (God-Mode): keine Gravitation — Blickrichtung fliegen, Space = steigen, G = aus.")
|
||
else:
|
||
show_message("Fliegen AUS — wieder mit Gravitation.")
|
||
return new_state
|
||
|
||
|
||
## Spawnpunkt-Punkt für Taste index (1 = Team A/Zone 0, 2 = Team B/Zone 1,
|
||
## 3/4 = weitere Zonen 2/3): erster Punkt der Zone. Rückgabe {} = keine Zone.
|
||
## pos = [x,y,z] aus dem Dokument.
|
||
func _spawn_point_for(index: int) -> Dictionary:
|
||
if body == null:
|
||
return {}
|
||
var zones: Dictionary = (body.get("doc") as Dictionary).get("spawn_zones", {})
|
||
var zone: Variant = zones.get(str(index - 1))
|
||
if not (zone is Dictionary):
|
||
return {}
|
||
var points: Array = (zone as Dictionary).get("points", [])
|
||
if points.is_empty() or not (points[0] is Dictionary):
|
||
return {}
|
||
var p := points[0] as Dictionary
|
||
var pos: Array = p.get("pos", [])
|
||
if pos.size() < 3:
|
||
return {}
|
||
return {"pos": Vector3(float(pos[0]), float(pos[1]), float(pos[2])),
|
||
"label": str((zone as Dictionary).get("label", "Zone %d" % index)),
|
||
"yaw": p.get(SpawnOps.YAW_KEY, null)}
|
||
|
||
|
||
## Sprung zu einem Spawnpunkt (Tasten 1–4): exakt auf den Punkt gesetzt
|
||
## (x/z aus dem Dokument, Höhe vom Terrain — dieselbe Konvention wie der
|
||
## Lauf-Modus-Start). Rückgabe true = gelandet.
|
||
func jump_to_spawn(index: int) -> bool:
|
||
var sp := _spawn_point_for(index)
|
||
if sp.is_empty() or body == null:
|
||
show_message("Kein Spawnpunkt für Taste %d — die Karte hat diese Zone nicht." % index)
|
||
return false
|
||
var target: Vector3 = sp["pos"]
|
||
var y: float = body.terrain_height(target.x, target.z)
|
||
# Kapsel-Mitte = Füße + Halbhöhe (+ Toleranz) — die Physik lässt den Body
|
||
# sanft auf den Trimesh fallen (wie beim Start, Karte t_1da96275).
|
||
body.set("position", Vector3(target.x, y + PlaytestBody.HALF_H + 0.02, target.z))
|
||
body.set("velocity", Vector3.ZERO)
|
||
body.set("on_floor", true)
|
||
if sp["yaw"] != null:
|
||
body.set("yaw_deg", float(sp["yaw"]))
|
||
show_message("Spawnpunkt %d = %s: x=%.1f z=%.1f (Höhe %.1f) — Blickrichtung steht im HUD." % [
|
||
index, str(sp["label"]), target.x, target.z, y])
|
||
return true
|
||
|
||
|
||
## HUD (Position/Blick/FPS/Hinweis) an/aus (Taste H). Rückgabe: sichtbar?
|
||
func toggle_hud() -> bool:
|
||
if hud == null:
|
||
return false
|
||
hud.visible = not hud.visible
|
||
if hud.visible:
|
||
show_message("HUD an.") # aus: Meldung wäre selbst unsichtbar
|
||
return hud.visible
|
||
|
||
|
||
## Hilfe-Overlay an/aus (Taste F1). Rückgabe: sichtbar?
|
||
func toggle_help() -> bool:
|
||
if help_layer == null:
|
||
_build_help()
|
||
help_layer.visible = not help_layer.visible
|
||
return help_layer.visible
|
||
|
||
|
||
## Kartenmitte/Achsen-Linien an/aus (Taste X). Rückgabe: sichtbar?
|
||
func toggle_axis() -> bool:
|
||
if _axis_node == null:
|
||
_build_axis()
|
||
_axis_node.visible = not _axis_node.visible
|
||
if _axis_node.visible:
|
||
show_message("Kartenmitte/Achsen AN (X = aus): X-Rot, Z-Blau, Mitte Grün.")
|
||
else:
|
||
show_message("Kartenmitte/Achsen AUS.")
|
||
return _axis_node.visible
|
||
|
||
|
||
## Hilfe-Overlay (F1): Panel mit allen Tasten im Klartext, mittig unten.
|
||
func _build_help() -> void:
|
||
help_layer = CanvasLayer.new()
|
||
help_layer.layer = 101
|
||
var center := CenterContainer.new()
|
||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
help_layer.add_child(center)
|
||
var panel := PanelContainer.new()
|
||
center.add_child(panel)
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 16)
|
||
margin.add_theme_constant_override("margin_right", 16)
|
||
margin.add_theme_constant_override("margin_top", 12)
|
||
margin.add_theme_constant_override("margin_bottom", 12)
|
||
panel.add_child(margin)
|
||
var label := Label.new()
|
||
label.text = "TESTLAUF — ALLE TASTEN
|
||
WASD — bewegen · Maus — umsehen
|
||
Shift — rennen · Space — springen / im Flug steigen
|
||
G — Fliegen (God-Mode) an/aus: keine Gravitation, Blickrichtung fliegen
|
||
V — Perspektive umschalten (erste / dritte Person)
|
||
1 — Sprung zu Team A (Spawnpunkt) · 2 — Team B
|
||
3 / 4 — weitere Spawnpunkte (Zonen 2/3)
|
||
T — Test-Start auf den Blickpunkt setzen (sofort dort)
|
||
H — HUD an/aus (Position, Blick, FPS, Höhe)
|
||
X — Kartenmitte/Achsen ein/aus (X-Rot, Z-Blau)
|
||
F1 — diese Hilfe · ESC — zurück zum Editor"
|
||
label.add_theme_font_size_override("font_size", 18)
|
||
margin.add_child(label)
|
||
add_child(help_layer)
|
||
help_layer.visible = false
|
||
|
||
|
||
## Achsen-Linien durch die Kartenmitte (Taste X): X-Achse rot, Z-Achse blau,
|
||
## senkrechte Mitte grün — beim gespiegelten Bauen (M1) auf einen Blick.
|
||
func _build_axis() -> void:
|
||
var doc: Dictionary = (body.get("doc") if body != null else {}) as Dictionary
|
||
var bounds: Dictionary = doc.get("bounds", {})
|
||
var mn: Array = bounds.get("min", [-50.0, -10.0, -50.0])
|
||
var mx: Array = bounds.get("max", [50.0, 80.0, 50.0])
|
||
var cx := (float(mn[0]) + float(mx[0])) * 0.5
|
||
var cz := (float(mn[2]) + float(mx[2])) * 0.5
|
||
var y0: float = body.terrain_height(cx, cz) if body != null else 0.0
|
||
var mesh := ImmediateMesh.new()
|
||
mesh.surface_begin(Mesh.PRIMITIVE_LINES)
|
||
# X-Achse (rot): ganze Kartenbreite durch die Mitte.
|
||
mesh.surface_set_color(Color(1.0, 0.25, 0.25))
|
||
mesh.surface_add_vertex(Vector3(float(mn[0]), y0, cz))
|
||
mesh.surface_add_vertex(Vector3(float(mx[0]), y0, cz))
|
||
# Z-Achse (blau): ganze Kartentiefe durch die Mitte.
|
||
mesh.surface_set_color(Color(0.3, 0.5, 1.0))
|
||
mesh.surface_add_vertex(Vector3(cx, y0, float(mn[2])))
|
||
mesh.surface_add_vertex(Vector3(cx, y0, float(mx[2])))
|
||
# Senkrechte Mitte (grün): vom Terrain bis zur oberen Grenze.
|
||
mesh.surface_set_color(Color(0.3, 1.0, 0.4))
|
||
mesh.surface_add_vertex(Vector3(cx, y0, cz))
|
||
mesh.surface_add_vertex(Vector3(cx, float(mx[1]), cz))
|
||
mesh.surface_end()
|
||
var mi := MeshInstance3D.new()
|
||
mi.name = "PlaytestAchsen"
|
||
mi.mesh = mesh
|
||
# Vertexfarben sichtbar machen: unshaded + vertex_color_use_as_albedo
|
||
# (ohne das ignoriert der Default-Material-Shader die Linien-Farben).
|
||
var mat := StandardMaterial3D.new()
|
||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
mat.vertex_color_use_as_albedo = true
|
||
mat.no_fog = true
|
||
mi.material_override = mat
|
||
mi.position = Vector3.ZERO
|
||
_axis_node = mi
|
||
add_child(_axis_node)
|