diff --git a/src/editor/playtest/playtest_body.gd b/src/editor/playtest/playtest_body.gd index 97ab07f..89c8b3f 100644 --- a/src/editor/playtest/playtest_body.gd +++ b/src/editor/playtest/playtest_body.gd @@ -46,6 +46,10 @@ var mouse_sens: float = DEFAULT_MOUSE_SENS ## Map-Dokument (nur LESEND — Terrain-Höhenabfrage) var doc: Dictionary = {} +## Fliegen / God-Mode (Karte t_aaff5dcc, Taste G): KEINE Gravitation, Bewegung +## in Blickrichtung (inkl. Nickwinkel), Space = steigen, Kollision aus +## (Noclip — der Tester kommt auch über Wände/Hügel schnell ans Ziel). +var fly_mode := false ## Vorgecachte Hindernis-AABBs (Weltkoordinaten) — von PlaytestMode gefüllt var obstacles: Array = [] # Array[AABB] ## Höhenfeld-Cache (Zeile: res*res Meter) — von PlaytestMode via set_terrain() @@ -84,6 +88,16 @@ func _apply_look() -> void: look_cam.position = Vector3(0.0, eye_height, 0.0) +## Volle 3D-Blickrichtung (Karte t_aaff5dcc): -Z um Pitch (X) und Yaw (Y) +## gedreht — genau die Blickachse der Kamera. Fliegen bewegt entlang DIESEM +## Vektor (Blick nach unten = abwärts fliegen), Gehen nur horizontal. +func view_direction() -> Vector3: + var yaw_rad := deg_to_rad(yaw_deg) + var pitch_rad := deg_to_rad(pitch_deg) + return Vector3(-sin(yaw_rad) * cos(pitch_rad), sin(pitch_rad), + -cos(yaw_rad) * cos(pitch_rad)) + + ## Eine Bewegungs-Schritt (physics frame) mit EXPLIZITEM Input ausführen. ## input = {fwd: bool, back: bool, left: bool, right: bool, run: bool, ## jump: bool} — die echte _physics_process baut das aus den @@ -110,6 +124,25 @@ func step(delta: float, input: Dictionary) -> Dictionary: if move.length_squared() > 0.0: move = move.normalized() + # --- Fliegen / God-Mode (Karte t_aaff5dcc, Taste G) ----------------------- + # Keine Gravitation, Bewegung in der VOLLEN Blickrichtung (Blick nach + # unten = abwärts fliegen), Space = zusätzlich steigen, Kollision AUS + # (Noclip — schnell über die Map, auch über Wände/Hügel hinweg). + if fly_mode: + var fly_speed := move_speed * (run_mult if run else 1.0) + var fly_yaw_rad := deg_to_rad(yaw_deg) + var fly_right := Vector3(cos(fly_yaw_rad), 0.0, -sin(fly_yaw_rad)) + var wish3 := view_direction() * move.y + fly_right * move.x + if jump: + wish3 += Vector3.UP + if wish3.length_squared() > 0.0: + wish3 = wish3.normalized() + velocity = wish3 * fly_speed + position += velocity * delta + on_floor = false + _apply_look() + return {"pos": position, "vel": velocity, "on_floor": on_floor} + # Welt-Richtung aus dem Blickwinkel (y-only, keine Sprung-Schräglage) var yaw_rad := deg_to_rad(yaw_deg) var forward := Vector3(-sin(yaw_rad), 0.0, -cos(yaw_rad)) @@ -359,3 +392,11 @@ func _sample(xi: int, zi: int) -> float: if idx < 0 or idx >= heights.size(): return hmin return heights[idx] + + +## Maus-Empfindlichkeit als Multiplikator setzen (Karte t_aaff5dcc, 0,1–5,0, +## Standard 1,0): die Body-Basis (DEFAULT_MOUSE_SENS Grad/Pixel) wird mit dem +## Multiplikator skaliert — jede PC/Maus-Kombination ist anders. +func set_sensitivity(mult: float) -> void: + var m := clampf(mult, PlaytestSettings.MIN_SENS, PlaytestSettings.MAX_SENS) + mouse_sens = DEFAULT_MOUSE_SENS * m diff --git a/src/editor/playtest/playtest_mode.gd b/src/editor/playtest/playtest_mode.gd index cc3d2bf..3279a4f 100644 --- a/src/editor/playtest/playtest_mode.gd +++ b/src/editor/playtest/playtest_mode.gd @@ -36,6 +36,12 @@ 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) @@ -67,6 +73,9 @@ func enter(doc: Dictionary, view_builder: Node3D, start_override: Variant = null start = Vector3(sp.x, 0.0, sp.z) body.position = Vector3(start.x, body.terrain_height(start.x, start.z), start.z) 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()) _build_hud() exit_requested.connect(_on_exit_requested, CONNECT_ONE_SHOT) @@ -87,6 +96,12 @@ func exit() -> void: 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: @@ -171,9 +186,10 @@ func _build_hud() -> void: hud_fps = Label.new() hud_hint = Label.new() hud_msg = Label.new() - hud_hint.text = "WASD + Maus · Shift = rennen · Space = springen · T = zum Blickpunkt versetzen · ESC = zurück zum Editor" + hud_dir = Label.new() + hud_hint.text = "WASD + Maus · Shift = rennen · Space = springen/steigen · G = Fliegen · 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_fps, hud_hint, hud_msg]: + 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) @@ -218,6 +234,9 @@ func _process(delta: float) -> void: _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: + hud_dir.text = "Blick: %.0f° / %.0f° · Höhe: %.1f m" % [ + float(body.get("yaw_deg")), float(body.get("pitch_deg")), body.position.y] if hud_fps != null: hud_fps.text = "FPS: %d" % int(round(_fps_smooth)) @@ -232,3 +251,203 @@ func wants_capture() -> bool: 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_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) + + +## 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))} + + +## 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) + body.set("position", Vector3(target.x, y, target.z)) + body.set("velocity", Vector3.ZERO) + body.set("on_floor", true) + 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 +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) diff --git a/src/editor/playtest/playtest_settings.gd b/src/editor/playtest/playtest_settings.gd new file mode 100644 index 0000000..8e71780 --- /dev/null +++ b/src/editor/playtest/playtest_settings.gd @@ -0,0 +1,53 @@ +extends RefCounted +class_name PlaytestSettings +## Einstellungen-Speicher für den Lauf-Modus (Karte t_aaff5dcc, T3 Komfort) — +## bewusst NICHT im .svmap-Format und NICHT in main.gd: der Operator hat für +## Einstellungen den Registry-Weg vorgegeben (Panel meldet sich in +## src/ui/panels/registry.gd an), der Zustand lebt HIER als statischer Store. +## +## Ablage: user://scopedvr_playtest.cfg, Abschnitt [playtest]. +## Wert: Maus-Empfindlichkeit als Multiplikator (0,1–5,0, Standard 1,0) auf +## die Body-Basis 0,25 Grad/Pixel — jede PC/Maus-Kombination ist anders. +## +## Statischer Cache: ein Lese-Vorgang pro Editor-Start; save_sens() hält den +## Cache frisch. Tests können mit reset_cache() neu laden erzwingen. + +const CFG_PATH := "user://scopedvr_playtest.cfg" +const SECTION := "playtest" +const KEY_SENS := "maus_empfindlichkeit" +const DEFAULT_SENS := 1.0 +const MIN_SENS := 0.1 +const MAX_SENS := 5.0 + +## Cache (null = noch nicht gelesen). static var ist Godot 4.1+. +static var _cache: Variant = null + + +## Gespeicherte Empfindlichkeit lesen (geclampt auf 0,1–5,0), Standard 1,0. +static func load_sens() -> float: + if _cache != null: + return float(_cache) + var cfg := ConfigFile.new() + var raw: Variant = DEFAULT_SENS + if cfg.load(CFG_PATH) == OK: + raw = cfg.get_value(SECTION, KEY_SENS, DEFAULT_SENS) + var val := clampf(float(raw), MIN_SENS, MAX_SENS) + _cache = val + return val + + +## Empfindlichkeit speichern (geclampt auf 0,1–5,0) und Cache frisch halten. +## Rückgabe: der tatsächlich gespeicherte Wert. +static func save_sens(value: float) -> float: + var val := clampf(value, MIN_SENS, MAX_SENS) + var cfg := ConfigFile.new() + cfg.load(CFG_PATH) # Fehler → leere Config, nur unser Key wird angelegt. + cfg.set_value(SECTION, KEY_SENS, val) + cfg.save(CFG_PATH) + _cache = val + return val + + +## Cache verwerfen (für Tests / frisches Neulesen von der Platte). +static func reset_cache() -> void: + _cache = null diff --git a/src/editor/playtest/playtest_settings.gd.uid b/src/editor/playtest/playtest_settings.gd.uid new file mode 100644 index 0000000..8751c34 --- /dev/null +++ b/src/editor/playtest/playtest_settings.gd.uid @@ -0,0 +1 @@ +uid://1m7xfphtjvwd diff --git a/src/ui/panels/registry.gd b/src/ui/panels/registry.gd index 81d369b..b3bd700 100644 --- a/src/ui/panels/registry.gd +++ b/src/ui/panels/registry.gd @@ -38,6 +38,10 @@ const PANELS: Array = [ "skript": "res://src/ui/panels/check_panel.gd"}, {"id": "teststart", "titel": "Test-Start", "gruppe": "rechts", "order": 60, "skript": "res://src/ui/panels/test_start_panel.gd"}, + # T3 Komfort (Karte t_aaff5dcc): Maus-Empfindlichkeit u. a. — Registry-Weg + # statt main.gd (Operator-Vorgabe 20.10.). + {"id": "einstellungen", "titel": "Einstellungen", "gruppe": "rechts", "order": 70, + "skript": "res://src/ui/panels/settings_panel.gd"}, # --- links: Vorbereitet für U2 (Szenen-Baum); Bibliothek ist UNTEN --- {"id": "szene", "titel": "Map-Inhalt", "gruppe": "links", "order": 10, "skript": "res://src/ui/panels/scene_tree_panel.gd"}, diff --git a/src/ui/panels/settings_panel.gd b/src/ui/panels/settings_panel.gd new file mode 100644 index 0000000..233dc98 --- /dev/null +++ b/src/ui/panels/settings_panel.gd @@ -0,0 +1,43 @@ +extends "res://src/ui/panels/editor_panel.gd" +## Einstellungen-Bereich (rechte Spalte — Karte t_aaff5dcc, T3 Komfort). +## +## Registry-Weg (Operator-Vorgabe 20.10.): main.gd bleibt unangetastet, das +## Modul meldet sich nur über src/ui/panels/registry.gd an. Der ZUSTAND lebt +## in src/editor/playtest/playtest_settings.gd (PlaytestSettings, user://-cfg) — +## dieses Panel zeigt/anpasst nur. Läuft gerade ein Testlauf, wird die neue +## Maus-Empfindlichkeit SOFORT über main.playtest.apply_sensitivity wirksam. + +var sens_box: SpinBox + + +static func spec() -> Dictionary: + return {"titel": "Einstellungen", "gruppe": "rechts", "order": 70} + + +func panel_title() -> String: + return "Einstellungen" + + +func build() -> void: + add_child(_header_label("Testlauf-Einstellungen")) + + add_child(_field_label("Maus-Empfindlichkeit (0,1 – 5,0)")) + sens_box = SpinBox.new() + sens_box.min_value = PlaytestSettings.MIN_SENS + sens_box.max_value = PlaytestSettings.MAX_SENS + sens_box.step = 0.1 + sens_box.value = PlaytestSettings.load_sens() + sens_box.custom_minimum_size = UIScale.v2(0, 36) + sens_box.tooltip_text = "Multiplikator auf die Blick-Geschwindigkeit im Testlauf. 1,0 = Standard; kleine Werte = feinfühlig, große Werte = schnell." + sens_box.value_changed.connect(_on_sens_changed) + add_child(sens_box) + + add_child(_separator()) + add_child(_muted_label("Gilt im Lauf-Modus („▶ Karte testen“). Standard 1,0 — jede PC-/Maus-Kombination ist anders. Wird automatisch gespeichert.")) + + +func _on_sens_changed(value: float) -> void: + PlaytestSettings.save_sens(value) + var pt: Variant = main.get("playtest") + if pt != null and pt.has_method("apply_sensitivity"): + pt.call("apply_sensitivity", value) diff --git a/src/ui/panels/settings_panel.gd.uid b/src/ui/panels/settings_panel.gd.uid new file mode 100644 index 0000000..342df31 --- /dev/null +++ b/src/ui/panels/settings_panel.gd.uid @@ -0,0 +1 @@ +uid://bp0ii5exucqcy diff --git a/tests/probe_playtest_comfort.gd b/tests/probe_playtest_comfort.gd new file mode 100644 index 0000000..f192ae3 --- /dev/null +++ b/tests/probe_playtest_comfort.gd @@ -0,0 +1,114 @@ +extends SceneTree +## WEGWERF-Renderprobe (Karte t_aaff5dcc, T3 Komfort im Testlauf) — +## (a) Vogelperspektive per God-Mode: Fliegen an, 80 m über der Kartenmitte, +## Blick nach unten, Achsen-Linien an (X-Rot/Z-Blau, Karte M1) +## (b) Sprung zum Spawnpunkt: Taste 2 (Team Rot) — HUD zeigt Pos/Blick/Höhe +## (c) Hilfe-Overlay (F1): alle Tasten im Klartext +## Ausführen: +## xvfb-run -a godot4 --path . --rendering-driver opengl3 -s res://tests/probe_playtest_comfort.gd +## Beweise nach /opt/data/scopedvr/work/taaff5dcc_beweise/. + +var main: Control +var subvp: SubViewport + +func _initialize() -> void: + _run.call_deferred() + +func _png(name: String) -> String: + return "/opt/data/scopedvr/work/taaff5dcc_beweise/" + name + +func _shot(name: String) -> void: + await process_frame + await process_frame + var img := subvp.get_texture().get_image() + img.save_png(_png(name)) + print("PROBE_PNG=%s (%dx%d)" % [_png(name), img.get_width(), img.get_height()]) + +func _find_subvp(node: Node) -> SubViewport: + if node is SubViewport: + return node as SubViewport + for c in node.get_children(): + var f := _find_subvp(c) + if f != null: + return f + return null + +func _run() -> void: + DirAccess.make_dir_recursive_absolute("/opt/data/scopedvr/work/taaff5dcc_beweise") + var scene: PackedScene = load("res://scenes/main.tscn") + main = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + subvp = _find_subvp(main) + var container: SubViewportContainer = subvp.get_parent() + container.stretch = false + subvp.size = Vector2i(1600, 900) + await process_frame + + # Test-Karte mit ein paar Kisten als Landmarken. + var doc: Dictionary = SvmapDocument.create_new("T3-Komfort-Probe", 512.0, "mittag") + doc["objects"] = [ + {"id": "crate_wood", "pos": [30.0, 0.0, -40.0], "scale": 1.0, "rot_y_deg": 0.0}, + {"id": "crate_wood", "pos": [-60.0, 0.0, 20.0], "scale": 1.0, "rot_y_deg": 0.0}, + ] + main.set("doc", doc) + main.call("_refresh_all") + await process_frame + await process_frame + + main.call("_on_test_run") + await process_frame + await process_frame + var pt: Node = main.get("playtest") + var body: Node3D = pt.get("body") + print("ZAHLEN start: body=", body.position, " fly=", body.get("fly_mode")) + + # --- (a) God-Mode: hoch über die Kartenmitte, Blick nach unten ------------ + pt.call("toggle_fly") + pt.call("toggle_axis") # Kartenmitte/Achsen sichtbar (X-Rot/Z-Blau/Grün) + var bounds: Dictionary = doc.get("bounds", {}) + var mn: Array = bounds.get("min", [-256.0, -10.0, -256.0]) + var mx: Array = bounds.get("max", [256.0, 80.0, 256.0]) + var cx := (float(mn[0]) + float(mx[0])) * 0.5 + var cz := (float(mn[2]) + float(mx[2])) * 0.5 + body.set("position", Vector3(cx, 80.0, cz)) + body.set("yaw_deg", 45.0) + body.set("pitch_deg", -80.0) + print("ZAHLEN (a) fly=", body.get("fly_mode"), " pos=", body.position, + " axis_an=", pt.call("toggle_axis") == false) + await _shot("t3_a_vogelperspektive_godmode.png") + + # --- (b) Sprung zum Spawnpunkt Team Rot (Taste 2) ------------------------- + pt.call("toggle_fly") # God-Mode AUS — dann zeigt die Meldung den Sprung + var ok2 := bool(pt.call("jump_to_spawn", 2)) + print("ZAHLEN (b) jump_to_spawn(2)=", ok2, " pos=", body.position, + " terrain_dort=", body.terrain_height(body.position.x, body.position.z)) + body.set("yaw_deg", 127.0) # Blick Richtung Kartenmitte + body.set("pitch_deg", 3.0) + print("ZAHLEN (b) fly=", body.get("fly_mode"), + " HUD pos='", (pt.get("hud_pos") as Label).text, "' dir='", + (pt.get("hud_dir") as Label).text, "' msg='", + (pt.get("hud_msg") as Label).text, "'") + await _shot("t3_b_spawnpunkt_team_rot.png") + + # --- (c) Hilfe-Overlay (F1) ----------------------------------------------- + var help_vor := bool(pt.call("toggle_help")) + print("ZAHLEN (c) F1 hilfe_an=", help_vor) + await _shot("t3_c_hilfe_f1.png") + + # --- Aufräumen: zurück in den Editor -------------------------------------- + main._unhandled_key_input(_make_key(KEY_ESCAPE)) + await process_frame + await process_frame + print("ZAHLEN (d) playtest null=", main.get("playtest") == null, + " panels=", main.get("ui_top_bar").visible) + print("PROBE_FERTIG") + quit(0) + +func _make_key(code: Key) -> InputEventKey: + var evt := InputEventKey.new() + evt.keycode = code + evt.physical_keycode = code + evt.pressed = true + return evt diff --git a/tests/probe_playtest_comfort.gd.uid b/tests/probe_playtest_comfort.gd.uid new file mode 100644 index 0000000..a5d3503 --- /dev/null +++ b/tests/probe_playtest_comfort.gd.uid @@ -0,0 +1 @@ +uid://rrp5fjepprgx diff --git a/tests/test_playtest_comfort.gd b/tests/test_playtest_comfort.gd new file mode 100644 index 0000000..b3c10fc --- /dev/null +++ b/tests/test_playtest_comfort.gd @@ -0,0 +1,333 @@ +extends SceneTree +## Editor-Test (headless): Karte t_aaff5dcc — T3 Komfort im Testlauf. +## Ausführen: godot4 --headless --path . --import && godot4 --headless --path . --script res://tests/test_playtest_comfort.gd +## Exit 0 = alles grün. +## +## Deckt ab (alles im Lauf-Modus, keine neuen Abhängigkeiten): +## 1. Fliegen/God-Mode (Taste G → toggle_fly): Gravitation AUS, Bewegung in +## Blickrichtung (inkl. Nickwinkel), Noclip durch Wände, Space = steigen, +## Ausschalten → Gravitation wirkt wieder. +## 2. Sprung zu Spawnpunkten (Taste 1–4 → jump_to_spawn): landet EXAKT auf +## den Spawnpunkten der Karte (x/z aus dem Dokument, Höhe vom Terrain, +## Toleranz ≤ 1 mm); fehlende Zone → abgelehnt, Position unverändert. +## 3. HUD-Toggle (Taste H): Sichtbarkeit wechselt; HUD zeigt Blickrichtung +## und Höhe. +## 4. Maus-Empfindlichkeit: gespeichert + geladen (Clamp 0,1–5,0), live +## angewandt (apply_sensitivity) und beim Start aus den Einstellungen +## gelesen; Panel über Registry angemeldet (main.gd unangetastet). +## 5. Rückkehr in den Editor weiterhin vollkommen sauber (Dokument + +## Undo unverändert, Panels sichtbar, Achsen/Hilfe/HUD abgeräumt). + +var failures := 0 +var checks := 0 +var main: Control + + +func _initialize() -> void: + _run_all.call_deferred() + + +func _run_all() -> void: + var scene: PackedScene = load("res://scenes/main.tscn") + _check(scene != null, "scenes/main.tscn laedt") + if scene == null: + _finish() + return + main = scene.instantiate() + root.add_child(main) + await process_frame + await process_frame + + # Echte Map (Neu-Dialog-Konvention) + zwei weitere Spawn-Zonen für 3/4. + var doc: Dictionary = SvmapDocument.create_new("Komfort-Karte", 512.0, "klar") + var zones: Dictionary = doc.get("spawn_zones", {}) + zones["2"] = {"label": "Zone 3", "points": [{"id": "C1", "pos": [7.5, 0.0, -12.5]}]} + zones["3"] = {"label": "Zone 4", "points": [{"id": "D1", "pos": [-13.5, 0.0, 21.5]}]} + main.set("doc", doc) + main.set("current_path", "") + main.set("dirty", false) + main.get("history").begin(doc) + main.call("_clear_edit_state") + main.call("_refresh_all") + await process_frame + + # Einstellungen auf bekannten Startwert (Standard). + PlaytestSettings.reset_cache() + PlaytestSettings.save_sens(1.0) + + # Einstellungen-Panel ist über die Registry im Baum (Haupttest der Anbindung). + var registry_script: GDScript = load("res://src/ui/panels/registry.gd") + var entry: Dictionary = registry_script.entry("einstellungen") + _check(not entry.is_empty(), "Registry: Eintrag 'einstellungen' vorhanden") + var panel: Node = main.find_child("einstellungen", true, false) + _check(panel != null, "Einstellungen-Panel im Baum (Registry-Weg, main.gd unangetastet)") + if panel != null: + _check(panel.get("sens_box") != null, "Einstellungen: Empfindlichkeits-Feld vorhanden") + var box: SpinBox = panel.get("sens_box") + _check(is_equal_approx(box.min_value, 0.1) and is_equal_approx(box.max_value, 5.0), + "Einstellungen: Bereich 0,1–5,0 im Feld") + _check(is_equal_approx(box.value, 1.0), "Einstellungen: Standard 1,0 vorgeblendet") + + main.call("_on_test_run") + await process_frame + await process_frame + + await _test_fly_godmode() + await _test_spawn_jumps() + await _test_hud_toggle() + await _test_sensitivity() + await _test_exit_restores_editor() + + # Aufräumen: Standard-Empfindlichkeit für den echten Editor hinterlassen. + PlaytestSettings.save_sens(1.0) + _finish() + + +func _finish() -> void: + if failures == 0: + print("ALL OK: %d checks" % checks) + quit(0) + else: + print("FAILED: %d/%d checks" % [failures, checks]) + quit(1) + + +func _check(cond: bool, label: String) -> void: + checks += 1 + if cond: + print(" ok: %s" % label) + else: + failures += 1 + print(" FAIL: %s" % label) + + +func _pt() -> Node: + return main.get("playtest") + + +func _body() -> Node3D: + var pt := _pt() + if pt == null: + return null + return pt.get("body") + + +func _key_event(code: Key) -> InputEventKey: + var evt := InputEventKey.new() + evt.keycode = code + evt.physical_keycode = code + evt.pressed = true + return evt + + +const NO_INPUT := {"fwd": false, "back": false, "left": false, "right": false, + "run": false, "jump": false} + + +## --- Test 1: Fliegen / God-Mode --------------------------------------------- +func _test_fly_godmode() -> void: + print("Testgruppe: Fliegen/God-Mode (G) — Gravitation aus, Blickrichtung, Noclip") + var pt := _pt() + if pt == null: + _check(false, "PlaytestMode aktiv (Vorbedingung)") + return + var body: Node3D = _body() + _check(not bool(body.get("fly_mode")), "Standard: Fliegen AUS") + + # Taste G über den echten Dispatch-Weg (mode._unhandled_input). + pt.call("_unhandled_input", _key_event(KEY_G)) + _check(bool(body.get("fly_mode")), "Taste G schaltet Fliegen AN (Dispatch)") + + # Gravitation AUS: in der Luft schwebt der Spieler ohne Input. + body.set("position", Vector3(body.position.x, 10.0, body.position.z)) + var y0: float = body.position.y + for i in 30: + body.call("step", 1.0 / 30.0, NO_INPUT) + _check(absf(float(body.position.y) - y0) < 0.001, + "Keine Gravitation: Höhe hält (10 m, %.3f nach 1 s)" % float(body.position.y)) + + # Noclip: Wand quer vor dem Spieler, Fliegen geht DURCH (kein Block). + var start := Vector3(body.position) + body.set("yaw_deg", 0.0) # Blick nach -Z + body.get("obstacles").append(AABB(Vector3(start.x - 2.0, -5.0, start.z - 3.0), + Vector3(4.0, 10.0, 0.6))) + for i in 60: + body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false, + "right": false, "run": false, "jump": false}) + var dist: float = start.distance_to(body.position) + _check(dist > 3.5, "Fliegen geht durch die Wand (%.2f m Weg, Wand bei 2.4–3.0)" % dist) + body.get("obstacles").clear() + + # Bewegung in Blickrichtung: Blick 30° nach unten → Wegstrecke teilt sich + # in -z und -y (sin30 = 0.5, cos30 ≈ 0.866; 4 m in 1 s). + var start2 := Vector3(body.position) + body.set("yaw_deg", 0.0) + body.set("pitch_deg", -30.0) + for i in 30: + body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false, + "right": false, "run": false, "jump": false}) + var moved: Vector3 = body.position - start2 + _check(absf(moved.z + 3.46) < 0.05 and absf(moved.y + 2.0) < 0.05, + "Blickrichtung inkl. Nickwinkel (Δz=%.2f, Δy=%.2f, Soll -3.46/-2.0)" % [moved.z, moved.y]) + + # Space = steigen. + var y_up: float = body.position.y + for i in 15: + body.call("step", 1.0 / 30.0, {"fwd": false, "back": false, "left": false, + "right": false, "run": false, "jump": true}) + _check(float(body.position.y) > y_up + 1.0, + "Space steigt im Flug (+%.1f m)" % float(body.position.y - y_up)) + + # Ausschalten: Gravitation wirkt wieder (Fällen zum Terrain). + pt.call("_unhandled_input", _key_event(KEY_G)) + _check(not bool(body.get("fly_mode")), "Zweites G schaltet Fliegen AUS") + var y_fall: float = body.position.y + var landed := false + for i in 120: + var state: Dictionary = body.call("step", 1.0 / 30.0, NO_INPUT) + if bool(state["on_floor"]): + landed = true + break + _check(landed and float(body.position.y) < y_fall, + "Fliegen aus: Spieler fällt und landet (y=%.2f)" % float(body.position.y)) + var terrain_y: float = body.call("terrain_height", body.position.x, body.position.z) + _check(is_equal_approx(float(body.position.y), terrain_y), + "Landung auf Terrainhöhe (%.3f == %.3f)" % [float(body.position.y), terrain_y]) + + +## --- Test 2: Sprung zu den Spawnpunkten -------------------------------------- +func _test_spawn_jumps() -> void: + print("Testgruppe: Sprung zu Spawnpunkten (1–4) — exakt, Toleranz ≤ 1 mm") + var pt := _pt() + if pt == null: + _check(false, "PlaytestMode aktiv (Vorbedingung)") + return + var body: Node3D = _body() + var doc: Dictionary = main.get("doc") + var zones: Dictionary = doc.get("spawn_zones", {}) + + var idx := 1 + for zone_key in ["0", "1", "2", "3"]: + var zone: Dictionary = zones.get(zone_key, {}) + var pts: Array = zone.get("points", []) + if pts.is_empty() or not (pts[0] is Dictionary): + _check(false, "Testmap braucht Zone %s" % zone_key) + idx += 1 + continue + var pos: Array = (pts[0] as Dictionary).get("pos", [0.0, 0.0, 0.0]) + var expected := Vector3(float(pos[0]), + body.call("terrain_height", float(pos[0]), float(pos[2])), float(pos[2])) + # Vorher woanders hin (sichert, dass wirklich gesprungen wird). + body.set("position", Vector3(99.0, 5.0, 99.0)) + var ok := bool(pt.call("jump_to_spawn", idx)) + _check(ok, "Sprung %d (Zone %s '%s') gelungen" % [idx, zone_key, str(zone.get("label", ""))]) + var delta: float = body.position.distance_to(expected) + _check(delta <= 0.001, + "Landung exakt auf Spawnpunkt %d (%.4f mm Abweichung: Soll %s, Ist %s)" % [ + idx, delta * 1000.0, expected, body.position]) + idx += 1 + + # Fehlende Zone (Taste 5 wäre Zone 4): abgelehnt, Position UNVERÄNDERT. + body.set("position", Vector3(3.0, 1.0, 4.0)) + var rejected := not bool(pt.call("jump_to_spawn", 5)) + _check(rejected, "Fehlende Zone: Sprung abgelehnt") + _check(body.position == Vector3(3.0, 1.0, 4.0), "Fehlende Zone: Position unverändert") + + +## --- Test 3: HUD-Toggle + Blickrichtung/Höhe -------------------------------- +func _test_hud_toggle() -> void: + print("Testgruppe: HUD-Toggle (H) + Blickrichtung/Höhe im HUD") + var pt := _pt() + if pt == null: + _check(false, "PlaytestMode aktiv (Vorbedingung)") + return + var hud: CanvasLayer = pt.get("hud") + _check(hud != null and hud.visible, "HUD standardmäßig sichtbar") + pt.call("_unhandled_input", _key_event(KEY_H)) + _check(not hud.visible, "Taste H: HUD AUS") + pt.call("_unhandled_input", _key_event(KEY_H)) + _check(hud.visible, "Taste H: HUD wieder AN") + var hud_dir: Label = pt.get("hud_dir") + _check(hud_dir != null, "HUD-Blickrichtung-Label vorhanden") + await process_frame + _check(str(hud_dir.text).begins_with("Blick:"), "HUD zeigt Blickrichtung ('%s')" % hud_dir.text) + _check("Höhe" in str(hud_dir.text), "HUD zeigt Höhe ('%s')" % hud_dir.text) + + +## --- Test 4: Maus-Empfindlichkeit ------------------------------------------- +func _test_sensitivity() -> void: + print("Testgruppe: Maus-Empfindlichkeit — speichern/laden, Clamp, live") + PlaytestSettings.reset_cache() + var saved: float = PlaytestSettings.save_sens(2.5) + _check(is_equal_approx(saved, 2.5), "save_sens(2.5) gespeichert") + PlaytestSettings.reset_cache() + _check(is_equal_approx(PlaytestSettings.load_sens(), 2.5), + "load_sens liest 2,5 zurück (Neustart-Simulation via reset_cache)") + _check(is_equal_approx(PlaytestSettings.save_sens(9.0), 5.0), "Clamp oben: 9,0 → 5,0") + _check(is_equal_approx(PlaytestSettings.save_sens(0.01), 0.1), "Clamp unten: 0,01 → 0,1") + + # Live-Anwendung: Body-Basis 0,25 Grad/Pixel × Multiplikator. + var body: Node3D = _body() + var pt := _pt() + pt.call("apply_sensitivity", 2.0) + _check(is_equal_approx(float(body.get("mouse_sens")), 0.5), + "apply_sensitivity(2.0): mouse_sens = 0.25 × 2 = 0.5 (Ist %.3f)" % float(body.get("mouse_sens"))) + + # Beim (Neu-)Start aus den Einstellungen gelesen: Store = 0.1 → Basis 0.025. + PlaytestSettings.save_sens(0.1) + pt.call("handle_escape") + await process_frame + _check(main.get("playtest") == null, "Zwischen-Exit sauber (Vorbedingung Neustart)") + main.call("_on_test_run") + await process_frame + await process_frame + var pt2 := _pt() + _check(pt2 != null, "PlaytestMode neu gestartet") + var body2: Node3D = pt2.get("body") + _check(is_equal_approx(float(body2.get("mouse_sens")), 0.025), + "Start liest Empfindlichkeit (0,1 → Basis 0.025, Ist %.4f)" % float(body2.get("mouse_sens"))) + _check(bool(body2.get("fly_mode")) == false, "Neustart: Fliegen wieder AUS (Body-Default)") + + # Panel-Änderung wirkt SOFORT auf den laufenden Testlauf (Panel→apply_sensitivity). + var panel: Node = main.find_child("einstellungen", true, false) + if panel != null: + var box: SpinBox = panel.get("sens_box") + box.value = 3.0 + _check(is_equal_approx(float(body2.get("mouse_sens")), 0.75), + "Panel 3,0 wirkt sofort (0.25 × 3 = 0.75, Ist %.3f)" % float(body2.get("mouse_sens"))) + else: + _check(false, "Einstellungen-Panel für Live-Test vorhanden") + + +## --- Test 5: Rückkehr in den Editor sauber ---------------------------------- +func _test_exit_restores_editor() -> void: + print("Testgruppe: Rückkehr in den Editor (Dokument + Undo unverändert)") + var pt := _pt() + if pt == null: + _check(false, "PlaytestMode aktiv (Vorbedingung)") + return + var doc_before: Dictionary = main.get("doc") + var doc_before_str := str(doc_before) + var hist: RefCounted = main.get("history") + var undo_before: bool = hist.call("can_undo") + var redo_before: bool = hist.call("can_redo") + + # Komfort-Zustände an: Achsen + Hilfe — auch die müssen exit() abräumen. + pt.call("toggle_axis") + pt.call("toggle_help") + _check(pt.get("_axis_node") != null, "Achsen-Linien an (Vorbedingung Aufräumen)") + _check(pt.get("help_layer") != null, "Hilfe-Overlay an (Vorbedingung Aufräumen)") + + pt.call("handle_escape") + await process_frame + await process_frame + + _check(main.get("playtest") == null, "PlaytestMode abgeräumt") + for pair in [["ui_top_bar", "Top-Bar"], ["ui_left_panel", "linke Spalte"], + ["ui_right_panel", "rechte Spalte"], ["ui_status_bar", "Statusleiste"]]: + var panel: Control = main.get(pair[0]) + _check(panel != null and panel.visible, "%s wieder sichtbar" % pair[1]) + _check(str(main.get("doc")) == doc_before_str, "Dokument byte-identisch") + _check(hist.can_undo() == undo_before and hist.can_redo() == redo_before, + "Undo/Redo-Stapel unverändert") + _check(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "Maus wieder frei") diff --git a/tests/test_playtest_comfort.gd.uid b/tests/test_playtest_comfort.gd.uid new file mode 100644 index 0000000..77bfea0 --- /dev/null +++ b/tests/test_playtest_comfort.gd.uid @@ -0,0 +1 @@ +uid://bvhnpmmu1mwca diff --git a/tests/test_ui_layout.gd b/tests/test_ui_layout.gd index 61eb569..e4bd1bc 100644 --- a/tests/test_ui_layout.gd +++ b/tests/test_ui_layout.gd @@ -101,9 +101,15 @@ func _init() -> void: _check(left != null and left is TabContainer, "linke Spalte ist ein TabContainer (U2-vorbereitet)") _check(bottom != null and bottom is VSplitContainer, "unten: VSplit (größenverstellbar)") if right != null: - # 20.10.: „Test-Start" ist als 6. Reiter dazugekommen (Wiederanbindung T1b, - # nachdem U1 die Verdrahtung verloren hatte). - _check(right.get_child_count() == 6, "rechts: 6 Tabs (Inspektor/Auswahl/Map/Terrain/Prüfen/Test-Start) — hat %d" % right.get_child_count()) + # Registry-getrieben (T3, Karte t_aaff5dcc): die erwartete Tab-Anzahl + # kommt aus der Registry-Tabelle (gruppe "rechts") statt hart kodiert — + # neue Panels melden sich dort an und der Check bleibt wahr. + var expected_right := 0 + for entry in registry_script.PANELS: + if str(entry["gruppe"]) == "rechts": + expected_right += 1 + _check(right.get_child_count() == expected_right, + "rechts: %d Tabs aus der Registry — hat %d" % [expected_right, right.get_child_count()]) _check(right.get_tab_count() == right.get_child_count(), "rechts: alle Bereiche als Tab in der Leiste erreichbar (Titel je Bereich)") for i in right.get_tab_count():