Compare commits
2 commits
969e928d74
...
b53e05a145
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b53e05a145 | ||
|
|
1b943ceb35 |
11 changed files with 1020 additions and 48 deletions
297
src/editor/playtest/playtest_body.gd
Normal file
297
src/editor/playtest/playtest_body.gd
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
extends Node3D
|
||||||
|
class_name PlaytestBody
|
||||||
|
## Lauf-Modus Spieler (Karte t_1cdc1a72): erste Person, WASD + Maus,
|
||||||
|
## Shift = rennen, Space = springen, Gravitation.
|
||||||
|
##
|
||||||
|
## Kollision OHNE Physik-Welt (SubViewport ohne Physik-Tick):
|
||||||
|
## - Boden: Terrain-Raster des DOKUMENTS abtasten (SvmapDocument.
|
||||||
|
## terrain_height_at) — nichts neu berechnen.
|
||||||
|
## - Objekte/Bau-Teile: vorgecachte Welt-AABBs (playtest_mode.gd sammelt
|
||||||
|
## sie beim Start aus den Marker-Metas + BuildOps.piece_aabb) als
|
||||||
|
## einfache Blöcke. Blöcke mit Oberkante ≤ Schritt-Höhe sind begehbar
|
||||||
|
## (Dach wird Boden), alles was darüber ragt blockt horizontal.
|
||||||
|
##
|
||||||
|
## Steuerung bewusst als reine step()-Methode: die echte _physics_process
|
||||||
|
## pollt nur die Tasten und ruft step() mit einem Input-Dictionary —
|
||||||
|
## die Suite (tests/test_playtest.gd) ruft step() mit simulierten Inputs
|
||||||
|
## und testet Mathe/Kollision OHNE echten Input.
|
||||||
|
##
|
||||||
|
## ESC wird NICHT hier gefangen: main.gd behandelt Esc VOR der normalen
|
||||||
|
## Esc-Kette, solange der Lauf-Modus aktiv ist („Esc = zurück zum Editor“).
|
||||||
|
|
||||||
|
signal exit_requested
|
||||||
|
|
||||||
|
const DEFAULT_MOVE_SPEED := 4.0 # m/s Gehen
|
||||||
|
const DEFAULT_RUN_MULT := 2.0 # Shift = rennen
|
||||||
|
const DEFAULT_JUMP_SPEED := 4.5 # m/s Sprungimpuls
|
||||||
|
const DEFAULT_GRAVITY := 12.0 # m/s² — Spiele-Wert, kein Physik-Server
|
||||||
|
const DEFAULT_EYE_HEIGHT := 1.7 # Kamera-Augenhöhe über dem Fußpunkt
|
||||||
|
const DEFAULT_STEP_HEIGHT := 0.45 # Stufen/Trittkanten, die begehbar sind
|
||||||
|
const DEFAULT_RADIUS := 0.35 # Spieler-Kolisionsradius (AABB-Aufblas)
|
||||||
|
const DEFAULT_BODY_HEIGHT := 1.85 # Kopf-Raum (Block-ab-Prüfung)
|
||||||
|
const DEFAULT_MOUSE_SENS := 0.25 # Grad pro Pixel Mausbewegung
|
||||||
|
|
||||||
|
const PITCH_LIMIT := 89.0 # Kopfüber/Nackenschmerz-Grenze
|
||||||
|
|
||||||
|
## Spielbare Parameter (PlaytestMode setzt die Defaults; Suite kann nachsetzen)
|
||||||
|
var move_speed: float = DEFAULT_MOVE_SPEED
|
||||||
|
var run_mult: float = DEFAULT_RUN_MULT
|
||||||
|
var jump_speed: float = DEFAULT_JUMP_SPEED
|
||||||
|
var gravity: float = DEFAULT_GRAVITY
|
||||||
|
var eye_height: float = DEFAULT_EYE_HEIGHT
|
||||||
|
var step_height: float = DEFAULT_STEP_HEIGHT
|
||||||
|
var radius: float = DEFAULT_RADIUS
|
||||||
|
var body_height: float = DEFAULT_BODY_HEIGHT
|
||||||
|
var mouse_sens: float = DEFAULT_MOUSE_SENS
|
||||||
|
|
||||||
|
## Map-Dokument (nur LESEND — Terrain-Höhenabfrage)
|
||||||
|
var doc: Dictionary = {}
|
||||||
|
## 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()
|
||||||
|
## gefüllt, damit die Boden-Abfrage NICHT jeden Schritt das Base64 dekodiert.
|
||||||
|
var heights: PackedFloat32Array = PackedFloat32Array()
|
||||||
|
var res: int = 0
|
||||||
|
var size_x: float = 0.0
|
||||||
|
var size_z: float = 0.0
|
||||||
|
var hmin: float = 0.0
|
||||||
|
var hmax: float = 0.0
|
||||||
|
|
||||||
|
## Bewegungszustand
|
||||||
|
var yaw_deg: float = 0.0 # Gierwinkel Blickrichtung (Grad)
|
||||||
|
var pitch_deg: float = 0.0 # Nickwinkel Blickrichtung (Grad)
|
||||||
|
var velocity: Vector3 = Vector3.ZERO # m/s
|
||||||
|
var on_floor: bool = false
|
||||||
|
|
||||||
|
## Kinder: die Spiel-Kamera (Augen) — PlaytestMode hängt sie an und macht current
|
||||||
|
var look_cam: Camera3D
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
look_cam = Camera3D.new()
|
||||||
|
look_cam.near = 0.1
|
||||||
|
look_cam.far = 6000.0
|
||||||
|
look_cam.current = true
|
||||||
|
add_child(look_cam)
|
||||||
|
_apply_look()
|
||||||
|
|
||||||
|
|
||||||
|
## Blickrichtung auf die Kamera übertragen (first person: Kamera schaut -Z).
|
||||||
|
func _apply_look() -> void:
|
||||||
|
if look_cam == null:
|
||||||
|
return
|
||||||
|
look_cam.rotation = Vector3(deg_to_rad(pitch_deg), deg_to_rad(yaw_deg), 0.0)
|
||||||
|
look_cam.position = Vector3(0.0, eye_height, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
## Tasten, die Suite aus simulierten Flags. delta in Sekunden.
|
||||||
|
## Rückgabe: {pos, vel, on_floor} nach dem Schritt.
|
||||||
|
func step(delta: float, input: Dictionary) -> Dictionary:
|
||||||
|
var fwd: bool = bool(input.get("fwd", false))
|
||||||
|
var back: bool = bool(input.get("back", false))
|
||||||
|
var left: bool = bool(input.get("left", false))
|
||||||
|
var right: bool = bool(input.get("right", false))
|
||||||
|
var run: bool = bool(input.get("run", false))
|
||||||
|
var jump: bool = bool(input.get("jump", false))
|
||||||
|
|
||||||
|
var speed := move_speed * (run_mult if run else 1.0)
|
||||||
|
var move := Vector2.ZERO
|
||||||
|
if fwd:
|
||||||
|
move.y += 1.0
|
||||||
|
if back:
|
||||||
|
move.y -= 1.0
|
||||||
|
if right:
|
||||||
|
move.x += 1.0
|
||||||
|
if left:
|
||||||
|
move.x -= 1.0
|
||||||
|
if move.length_squared() > 0.0:
|
||||||
|
move = move.normalized()
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
var right_v := Vector3(cos(yaw_rad), 0.0, -sin(yaw_rad))
|
||||||
|
var wish := (forward * move.y + right_v * move.x) * speed
|
||||||
|
|
||||||
|
# Horizontal ist snappy (kein Anlauf-Trägheit-Momentum): direkt setzen.
|
||||||
|
velocity.x = wish.x
|
||||||
|
velocity.z = wish.z
|
||||||
|
|
||||||
|
# Vertikal: Gravitation + Sprung (nur vom Boden).
|
||||||
|
if jump and on_floor:
|
||||||
|
velocity.y = jump_speed
|
||||||
|
on_floor = false
|
||||||
|
elif on_floor:
|
||||||
|
velocity.y = 0.0
|
||||||
|
else:
|
||||||
|
velocity.y = velocity.y - gravity * delta
|
||||||
|
|
||||||
|
var was_floor := on_floor
|
||||||
|
|
||||||
|
# --- Horizontal X/Z SEPARAT auflösen (Wand-Sliding) ---
|
||||||
|
var next := position
|
||||||
|
var nx := next.x + velocity.x * delta
|
||||||
|
if not _blocked_at(nx, next.z, next.y):
|
||||||
|
next.x = nx
|
||||||
|
else:
|
||||||
|
velocity.x = 0.0
|
||||||
|
var nz := next.z + velocity.z * delta
|
||||||
|
if not _blocked_at(next.x, nz, next.y):
|
||||||
|
next.z = nz
|
||||||
|
else:
|
||||||
|
velocity.z = 0.0
|
||||||
|
|
||||||
|
# --- Vertikal: Boden finden (Terrain + begehbare Block-Decken) ---
|
||||||
|
var ground := _ground_height_at(next.x, next.z, next.y)
|
||||||
|
var ny := next.y + velocity.y * delta
|
||||||
|
if velocity.y <= 0.0:
|
||||||
|
# Fallen oder am Boden: Bodenhöhe greifen (auch beim Bergab-Gehen,
|
||||||
|
# wenn der Boden unter der Füßen „wegrutscht“ — snap-down nur bis
|
||||||
|
# step_height pro Schritt, sonst fällt man Abhänge frei).
|
||||||
|
if ny <= ground:
|
||||||
|
ny = ground
|
||||||
|
velocity.y = 0.0
|
||||||
|
on_floor = true
|
||||||
|
elif was_floor and ny - ground <= step_height:
|
||||||
|
ny = ground # Bergab-Übergang (kleine Kante mitnehmen)
|
||||||
|
velocity.y = 0.0
|
||||||
|
on_floor = true
|
||||||
|
else:
|
||||||
|
on_floor = false
|
||||||
|
else:
|
||||||
|
# Sprung-Phase: kein Boden-Greifen; auf_floaten lassen
|
||||||
|
on_floor = false
|
||||||
|
next.y = ny
|
||||||
|
|
||||||
|
position = next
|
||||||
|
_apply_look()
|
||||||
|
return {"pos": position, "vel": velocity, "on_floor": on_floor}
|
||||||
|
|
||||||
|
|
||||||
|
## Maus-Bewegung (nur der Spieler selbst fängt sie — die Editor-Werkzeuge
|
||||||
|
## sind im Lauf-Modus eingefroren).
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseMotion:
|
||||||
|
var mm := event as InputEventMouseMotion
|
||||||
|
yaw_deg = wrapf(yaw_deg - mm.relative.x * mouse_sens, -180.0, 180.0)
|
||||||
|
pitch_deg = clampf(pitch_deg - mm.relative.y * mouse_sens,
|
||||||
|
-PITCH_LIMIT, PITCH_LIMIT)
|
||||||
|
_apply_look()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
## ESC-Request — wird von main.gd angerufen (die Esc-Priorität gehört dort
|
||||||
|
## hin, damit die normale Esc-Kette NICHT auch noch zieht).
|
||||||
|
func request_exit() -> void:
|
||||||
|
exit_requested.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Terrain-Raster EINMALIG aus dem Dokument dekodieren (R16/LE/Base64 →
|
||||||
|
## Meter-Array). Nutzt den Standard-Decoder SvmapIO.decode_heights (derselbe
|
||||||
|
## Code wie beim Editor-Rendering) — nichts neu rechnen.
|
||||||
|
func set_terrain(d: Dictionary) -> void:
|
||||||
|
doc = d
|
||||||
|
heights = PackedFloat32Array()
|
||||||
|
res = 0
|
||||||
|
size_x = 0.0
|
||||||
|
size_z = 0.0
|
||||||
|
hmin = 0.0
|
||||||
|
hmax = 0.0
|
||||||
|
if d.is_empty():
|
||||||
|
return
|
||||||
|
var terrain: Dictionary = d.get("terrain", {})
|
||||||
|
res = int(terrain.get("resolution", 0))
|
||||||
|
size_x = float(terrain.get("size_x_m", 0.0))
|
||||||
|
size_z = float(terrain.get("size_z_m", 0.0))
|
||||||
|
hmin = float(terrain.get("min_height_m", 0.0))
|
||||||
|
hmax = float(terrain.get("max_height_m", 0.0))
|
||||||
|
if res < 2 or size_x <= 0.0 or size_z <= 0.0:
|
||||||
|
return
|
||||||
|
var decoded: PackedFloat32Array = SvmapIO.decode_heights(terrain)
|
||||||
|
if decoded.size() < res * res:
|
||||||
|
return
|
||||||
|
heights = decoded
|
||||||
|
|
||||||
|
|
||||||
|
## Realer physics step: Tasten poll → step() aufrufen.
|
||||||
|
func _physics_process(_delta: float) -> void:
|
||||||
|
var input := {
|
||||||
|
"fwd": Input.is_key_pressed(KEY_W),
|
||||||
|
"back": Input.is_key_pressed(KEY_S),
|
||||||
|
"left": Input.is_key_pressed(KEY_A),
|
||||||
|
"right": Input.is_key_pressed(KEY_D),
|
||||||
|
"run": Input.is_key_pressed(KEY_SHIFT),
|
||||||
|
"jump": Input.is_key_pressed(KEY_SPACE),
|
||||||
|
}
|
||||||
|
step(_delta, input)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Kollision -------
|
||||||
|
|
||||||
|
## Ist die Position (x, z) mit Füßen bei feet_y von einem BLOCKIERNDEM
|
||||||
|
## Hindernis belegt? Blockierend = Oberkante über der Trittkante
|
||||||
|
## (feet_y + step_height) UND Unterkante unter der Kopf-Höhe.
|
||||||
|
## Aufblas um den Spieler-Radius: die AABB wächst um `radius` in x/z.
|
||||||
|
func _blocked_at(x: float, z: float, feet_y: float) -> bool:
|
||||||
|
var head := feet_y + body_height
|
||||||
|
for ab in obstacles:
|
||||||
|
var box: AABB = ab
|
||||||
|
if box.end.y <= feet_y + step_height:
|
||||||
|
continue # begehbar (Trittkante)
|
||||||
|
if box.position.y >= head:
|
||||||
|
continue # oberhalb des Kopfes — drunter durchlaufen
|
||||||
|
if x < box.position.x - radius or x > box.end.x + radius:
|
||||||
|
continue
|
||||||
|
if z < box.position.z - radius or z > box.end.z + radius:
|
||||||
|
continue
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## Boden-Höhe an (x, z): Terrain-Raster + alle begehbaren Block-Decken,
|
||||||
|
## deren Oberkante zwischen Terrain und feet_y + step_height liegt.
|
||||||
|
func _ground_height_at(x: float, z: float, feet_y: float) -> float:
|
||||||
|
var g := terrain_height(x, z)
|
||||||
|
var limit := feet_y + step_height
|
||||||
|
for ab in obstacles:
|
||||||
|
var box: AABB = ab
|
||||||
|
if box.end.y > limit or box.end.y <= g:
|
||||||
|
continue
|
||||||
|
if x < box.position.x - radius or x > box.end.x + radius:
|
||||||
|
continue
|
||||||
|
if z < box.position.z - radius or z > box.end.z + radius:
|
||||||
|
continue
|
||||||
|
g = box.end.y
|
||||||
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
## Terrain-Höhe an Welt-X/Z — aus dem DOCUMENT-Raster (nichts neu rechnen).
|
||||||
|
func terrain_height(x: float, z: float) -> float:
|
||||||
|
if res < 2 or size_x <= 0.0 or size_z <= 0.0:
|
||||||
|
return hmin
|
||||||
|
if heights.size() < res * res:
|
||||||
|
return hmin
|
||||||
|
var last := float(res - 1)
|
||||||
|
var fx := clampf((x / size_x + 0.5) * last, 0.0, last)
|
||||||
|
var fz := clampf((z / size_z + 0.5) * last, 0.0, last)
|
||||||
|
var xi := int(floor(fx))
|
||||||
|
var zi := int(floor(fz))
|
||||||
|
var tx := fx - float(xi)
|
||||||
|
var tz := fz - float(zi)
|
||||||
|
var xi1 := mini(xi + 1, res - 1)
|
||||||
|
var zi1 := mini(zi + 1, res - 1)
|
||||||
|
var h00 := _sample(xi, zi)
|
||||||
|
var h10 := _sample(xi1, zi)
|
||||||
|
var h01 := _sample(xi, zi1)
|
||||||
|
var h11 := _sample(xi1, zi1)
|
||||||
|
return lerpf(lerpf(h00, h10, tx), lerpf(h01, h11, tx), tz)
|
||||||
|
|
||||||
|
|
||||||
|
func _sample(xi: int, zi: int) -> float:
|
||||||
|
var idx := zi * res + xi
|
||||||
|
if idx < 0 or idx >= heights.size():
|
||||||
|
return hmin
|
||||||
|
return heights[idx]
|
||||||
1
src/editor/playtest/playtest_body.gd.uid
Normal file
1
src/editor/playtest/playtest_body.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://csv8o2g8wdtue
|
||||||
196
src/editor/playtest/playtest_mode.gd
Normal file
196
src/editor/playtest/playtest_mode.gd
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
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.
|
||||||
|
## - PlaytestBody (src/editor/playtest/playtest_body.gd) 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 BUILD_OPS_SCRIPT := "res://src/editor/build_ops.gd"
|
||||||
|
|
||||||
|
## Der laufende Spieler (PlaytestBody, step-basiert)
|
||||||
|
var body: 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
|
||||||
|
## 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
|
||||||
|
|
||||||
|
signal exit_requested
|
||||||
|
|
||||||
|
|
||||||
|
## Starten: übernimmt die Sicht (Panels aus, Maus-Capture-Wunsch an) und
|
||||||
|
## legt den Spieler am ersten Spawnpunkt (Team Blau) an — sonst Kartenmitte.
|
||||||
|
## 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) -> void:
|
||||||
|
if active:
|
||||||
|
return
|
||||||
|
active = true
|
||||||
|
_set_ui_visible(false)
|
||||||
|
_set_status_capture(true)
|
||||||
|
|
||||||
|
body = load(BODY_SCRIPT).new()
|
||||||
|
body.call("set_terrain", doc)
|
||||||
|
body.obstacles = _collect_obstacles(doc, view_builder)
|
||||||
|
var start := _spawn_start(doc)
|
||||||
|
body.position = Vector3(start.x, body.terrain_height(start.x, start.z), start.z)
|
||||||
|
add_child(body)
|
||||||
|
|
||||||
|
_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 hud != null:
|
||||||
|
hud.queue_free()
|
||||||
|
hud = 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 _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)
|
||||||
|
|
||||||
|
|
||||||
|
## Hindernis-AABBs sammeln (nur LESEND, keine neue Physik-Welt):
|
||||||
|
## - Objekt-Marker: die VORHANDENEN Viewport-Metas — pick_radius (x/z-
|
||||||
|
## Halbausdehnung, Kugel-Approximation konservativ) + half_height
|
||||||
|
## (Center-Höhe über dem Fußpunkt → Objekt-Top = 2× half_height).
|
||||||
|
## - Bau-Teile: exakt über BuildOps.piece_aabb (Dokument-Teile → AABB).
|
||||||
|
func _collect_obstacles(doc: Dictionary, view_builder: Node3D) -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
if view_builder != null:
|
||||||
|
for child in view_builder.get_children():
|
||||||
|
if child is Node3D and (child as Node3D).has_meta("obj_index"):
|
||||||
|
var m := child as Node3D
|
||||||
|
var r := float(m.get_meta("pick_radius", 1.0))
|
||||||
|
var ch := float(m.get_meta("half_height", 0.5))
|
||||||
|
var p := m.position
|
||||||
|
out.append(AABB(Vector3(p.x - r, p.y, p.z - r),
|
||||||
|
Vector3(r * 2.0, ch * 2.0, r * 2.0)))
|
||||||
|
var build_ops := load(BUILD_OPS_SCRIPT)
|
||||||
|
if build_ops != null:
|
||||||
|
for teil in doc.get("gebaeude", []):
|
||||||
|
if teil is Dictionary:
|
||||||
|
var ab: AABB = build_ops.call("piece_aabb", teil)
|
||||||
|
if ab.size.x > 0.0 or ab.size.y > 0.0 or ab.size.z > 0.0:
|
||||||
|
out.append(ab)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- 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_hint.text = "WASD + Maus · Shift = rennen · Space = springen · ESC = zurück zum Editor"
|
||||||
|
for l: Label in [hud_pos, hud_fps, hud_hint]:
|
||||||
|
l.add_theme_font_size_override("font_size", 16)
|
||||||
|
vbox.add_child(l)
|
||||||
|
add_child(hud)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not active or body == null:
|
||||||
|
return
|
||||||
|
# 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_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()
|
||||||
1
src/editor/playtest/playtest_mode.gd.uid
Normal file
1
src/editor/playtest/playtest_mode.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://spymwdq2qcvr
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
class_name TestRun
|
class_name TestRun
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
## „▶ Karte testen" (Karte t_b84af40f): Export der AKTUELLEN Map nach Temp
|
## ALT (Karte t_1cdc1a72): dieser Client-Start ist seit dem Lauf-Modus im
|
||||||
|
## Map-Builder selbst NICHT mehr der Standard-Weg — „▶ Karte testen" startet
|
||||||
|
## jetzt IN DIESER APP (src/editor/playtest/, kein Client-Ordner, kein
|
||||||
|
## zweiter Prozess, kein VR). Der Rückbau dieser Datei kommt in einer
|
||||||
|
## späteren Karte; bis dahin bleibt die Export-/Such-Logik als Fallback
|
||||||
|
## erhalten und getestet (tests/test_testrun.gd).
|
||||||
|
##
|
||||||
|
## Ursprünglicher Zweck (Karte t_b84af40f): Export der AKTUELLEN Map nach Temp
|
||||||
## + Start des scopedvr-client als EIGENES Fenster im Testlauf-Modus
|
## + Start des scopedvr-client als EIGENES Fenster im Testlauf-Modus
|
||||||
## (--testmap <pfad>). Der Editor bleibt offen — der Client ist ein eigener
|
## (--testmap <pfad>). Der Editor bleibt offen — der Client ist ein eigener
|
||||||
## Prozess; das Spiel beendet sich selbst sauber (ESC = „Zurück zum Editor").
|
## Prozess; das Spiel beendet sich selbst sauber (ESC = „Zurück zum Editor").
|
||||||
|
|
|
||||||
134
src/ui/main.gd
134
src/ui/main.gd
|
|
@ -151,6 +151,18 @@ var check_text: Label
|
||||||
# Karte testen (Karte t_b84af40f)
|
# Karte testen (Karte t_b84af40f)
|
||||||
var test_btn: Button
|
var test_btn: Button
|
||||||
|
|
||||||
|
# Lauf-Modus „▶ Karte testen" im Builder selbst (Karte t_1cdc1a72): erste
|
||||||
|
# Person, WASD + Maus — OHNE Client-Ordner, ohne zweiten Prozess, ohne
|
||||||
|
# VR/OpenXR, ohne Server (Slys verbindliche Vorgabe). Der alte Client-Start
|
||||||
|
# bleibt als „alt" in src/editor/test_run.gd; der Rúckbau kommt später.
|
||||||
|
var playtest: Node = null # src/editor/playtest/playtest_mode.gd Instanz
|
||||||
|
var _playtest_tool_mode := "" # gemerkter Werkzeug-Modus für die Rückkehr
|
||||||
|
var ui_top_bar: Control # Top-Bar (im Lauf-Modus aus)
|
||||||
|
var ui_left_panel: Control # linke Bibliothek (im Lauf-Modus aus)
|
||||||
|
var ui_right_panel: Control # rechte Werkzeug-Panel-Spalte (im Lauf-Modus aus)
|
||||||
|
var ui_status_bar: Control # Statusleiste (im Lauf-Modus aus)
|
||||||
|
var world3d: Node3D # 3D-Wurzel im SubViewport (Spieler lebt hier)
|
||||||
|
|
||||||
# Karte 3/3 (t_11b5c25b): Dirty-Schutz + Kurzhilfe + Werkzeug-Statuszeile.
|
# Karte 3/3 (t_11b5c25b): Dirty-Schutz + Kurzhilfe + Werkzeug-Statuszeile.
|
||||||
var dirty_dialog: ConfirmationDialog # „Ungespeicherte Änderungen verwerfen?“
|
var dirty_dialog: ConfirmationDialog # „Ungespeicherte Änderungen verwerfen?“
|
||||||
var _pending_after_dirty := Callable() # was nach „Verwerfen“ ausgeführt wird
|
var _pending_after_dirty := Callable() # was nach „Verwerfen“ ausgeführt wird
|
||||||
|
|
@ -230,6 +242,7 @@ func _build_ui() -> void:
|
||||||
top_h.add_theme_constant_override("separation", UIScale.px(10))
|
top_h.add_theme_constant_override("separation", UIScale.px(10))
|
||||||
top.add_child(top_h)
|
top.add_child(top_h)
|
||||||
vbox.add_child(top)
|
vbox.add_child(top)
|
||||||
|
ui_top_bar = top # Lauf-Modus (t_1cdc1a72): Top-Bar ausblenden
|
||||||
_update_banner_build(vbox)
|
_update_banner_build(vbox)
|
||||||
|
|
||||||
var file_menu := MenuButton.new()
|
var file_menu := MenuButton.new()
|
||||||
|
|
@ -371,16 +384,23 @@ func _build_ui() -> void:
|
||||||
split.split_offset = UIScale.px(-320)
|
split.split_offset = UIScale.px(-320)
|
||||||
vbox.add_child(split)
|
vbox.add_child(split)
|
||||||
|
|
||||||
split.add_child(_build_left_panel())
|
# Lauf-Modus (t_1cdc1a72): linke/rechte Spalte als Member — werden im
|
||||||
|
# Lauf-Modus ausgeblendet, die 3D-Ansicht (Mitte) bleibt stehen.
|
||||||
|
var left := _build_left_panel()
|
||||||
|
ui_left_panel = left
|
||||||
|
split.add_child(left)
|
||||||
split.add_child(_build_center_viewport())
|
split.add_child(_build_center_viewport())
|
||||||
split.add_child(_build_right_panel())
|
var right := _build_right_panel()
|
||||||
|
ui_right_panel = right
|
||||||
|
split.add_child(right)
|
||||||
|
|
||||||
# --- Statusleiste: Hilfs-Meldungen + Budget ---
|
# --- Statusleiste: Hilfs-Meldungen + Werkzeug-Info ---
|
||||||
var status_panel := PanelContainer.new()
|
var status_panel := PanelContainer.new()
|
||||||
var status_h := HBoxContainer.new()
|
var status_h := HBoxContainer.new()
|
||||||
status_h.add_theme_constant_override("separation", UIScale.px(16))
|
status_h.add_theme_constant_override("separation", UIScale.px(16))
|
||||||
status_panel.add_child(status_h)
|
status_panel.add_child(status_h)
|
||||||
vbox.add_child(status_panel)
|
vbox.add_child(status_panel)
|
||||||
|
ui_status_bar = status_panel # Lauf-Modus (t_1cdc1a72): Statusleiste aus
|
||||||
|
|
||||||
status_label = Label.new()
|
status_label = Label.new()
|
||||||
status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
@ -803,6 +823,7 @@ func _build_center_viewport() -> SubViewportContainer:
|
||||||
|
|
||||||
var world := Node3D.new()
|
var world := Node3D.new()
|
||||||
viewport.add_child(world)
|
viewport.add_child(world)
|
||||||
|
world3d = world # Lauf-Modus (t_1cdc1a72): Spieler-Body lebt in diesem Baum
|
||||||
|
|
||||||
view_builder = Node3D.new()
|
view_builder = Node3D.new()
|
||||||
view_builder.set_script(load("res://src/viewport/map_view_builder.gd"))
|
view_builder.set_script(load("res://src/viewport/map_view_builder.gd"))
|
||||||
|
|
@ -1705,50 +1726,74 @@ func _update_finished(ok: bool, title: String, details: Array) -> void:
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
## „▶ Karte testen" (Karte t_b84af40f): aktuelle Map (auch ungespeichert!)
|
## „▶ Karte testen" (Karte t_1cdc1a72): Lauf-Modus IM Map-Builder selbst —
|
||||||
## nach Temp exportieren und den scopedvr-client als EIGENES Fenster starten.
|
## erste Person, WASD + Maus, ohne Client-Ordner/VR/Server (Slys verbindlicher
|
||||||
## Der Editor bleibt offen; das Spiel beendet sich selbst sauber (ESC).
|
## Wunsch: der Map-Builder ist überall installierbar und testet die Map ohne
|
||||||
|
## jegliche ScopedVR-Client-Abhängigkeit). Der ALTE Client-Start (Export nach
|
||||||
|
## Temp + eigener Client-Prozess, Karte t_b84af40f) bleibt als „alt" in
|
||||||
|
## src/editor/test_run.gd — der Rückbau kommt in einer späteren Karte.
|
||||||
func _on_test_run() -> void:
|
func _on_test_run() -> void:
|
||||||
if doc.is_empty():
|
if doc.is_empty():
|
||||||
_set_status("Erst eine Map anlegen (Datei → Neue Map), dann testen.", true)
|
_set_status("Erst eine Map anlegen (Datei → Neue Map), dann testen.", true)
|
||||||
return
|
return
|
||||||
var cfg := TestRun.load_cfg()
|
_start_playtest()
|
||||||
var client_dir := TestRun.find_client(ProjectSettings.globalize_path("res://"), cfg)
|
|
||||||
if client_dir.is_empty():
|
|
||||||
_show_errors("Karte testen — das Spiel wurde nicht gefunden:", [
|
## Lauf-Modus anwerfen: Werkzeuge einfrieren, Panels ausblenden, Maus einfangen,
|
||||||
"Der ScopedVR-Client (Ordner „scopedvr-client“) wurde nicht neben dem Editor gefunden.",
|
## PlaytestMode in der 3D-Welt anlegen (Spieler am ersten Spawnpunkt der Map).
|
||||||
"So geht es: Den Client-Ordner neben den Editor-Ordner legen (gleicher Überordner),",
|
func _start_playtest() -> void:
|
||||||
"ODER in der Datei scopedvr_editor.cfg im Ordner „ScopedVR Map Editor“ (Godot-App-Daten) eintragen:",
|
if playtest != null:
|
||||||
" [test]",
|
|
||||||
" client_dir=\"/pfad/zu/scopedvr-client\"",
|
|
||||||
" godot_path=\"/pfad/zu/godot\" (nur nötig, wenn der Editor NICHT über Godot gestartet wird)",
|
|
||||||
])
|
|
||||||
_set_status("Karte testen: Spiel nicht gefunden — siehe Fenster.", true)
|
|
||||||
return
|
return
|
||||||
var godot := TestRun.find_godot(cfg)
|
# Editor-Zustand einfrieren (ohne Auswahl/Dokument/History anzufassen):
|
||||||
if godot.is_empty():
|
# laufende Ghost-Platzierung zurückrollen, Gizmo-Zug abbrechen (beides
|
||||||
_show_errors("Karte testen — Godot-Programm nicht gefunden:", [
|
# rollt ohne History-Eintrag zurück), Werkzeug-Modus auf Auswahl umschalten.
|
||||||
"Der Editor läuft gerade nicht über ein Godot-Programm, deshalb weiß er nicht,",
|
interaction.call("cancel_placement")
|
||||||
"wie er das Spiel-Fenster starten soll. Trage in scopedvr_editor.cfg ein:",
|
if gizmo != null and gizmo.has_method("cancel_drag_if_active"):
|
||||||
" [test]",
|
gizmo.call("cancel_drag_if_active")
|
||||||
" godot_path=\"/pfad/zu/godot\"",
|
_playtest_tool_mode = tool_mode
|
||||||
])
|
tool_mode = "objects"
|
||||||
_set_status("Karte testen: Godot-Programm nicht gefunden — siehe Fenster.", true)
|
_playtest_set_frozen(true)
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
playtest = load("res://src/editor/playtest/playtest_mode.gd").new()
|
||||||
|
playtest.ui_panels = [ui_top_bar, ui_left_panel, ui_right_panel, ui_status_bar]
|
||||||
|
playtest.ui_status = status_label
|
||||||
|
playtest.help_label = help_label
|
||||||
|
playtest.exit_requested.connect(_on_playtest_exit_requested)
|
||||||
|
world3d.add_child(playtest)
|
||||||
|
camera.current = false # Editor-Orbit abgeben — die Spiel-Kamera übernimmt
|
||||||
|
playtest.enter(doc, view_builder)
|
||||||
|
_set_status("▶ Testlauf läuft: WASD laufen, Shift rennen, Space springen, Maus umsehen — ESC zurück zum Editor.")
|
||||||
|
|
||||||
|
|
||||||
|
## Editor-Input einfrieren/freigeben (Kamera-Flug, Werkzeuge, Gizmo).
|
||||||
|
## Nur auf die Werkzeug-Nodes — die Sicht (view_builder) bleibt 1:1 stehen.
|
||||||
|
func _playtest_set_frozen(on: bool) -> void:
|
||||||
|
for t in [camera, interaction, terrain_tool, spawn_tool, build_tool]:
|
||||||
|
if t == null:
|
||||||
|
continue
|
||||||
|
t.set_process_unhandled_input(not on)
|
||||||
|
t.set_process(not on)
|
||||||
|
if gizmo != null:
|
||||||
|
gizmo.set_process_unhandled_input(not on)
|
||||||
|
gizmo.set_process(not on)
|
||||||
|
gizmo.visible = not on
|
||||||
|
|
||||||
|
|
||||||
|
## ESC im Lauf-Modus: sauber zurück in den Editor — Werkzeuge + Panels wieder
|
||||||
|
## an, Maus frei, Kamera + Auswahl unverändert, Dokument + Undo-Stapel unverändert.
|
||||||
|
func _on_playtest_exit_requested() -> void:
|
||||||
|
if playtest == null:
|
||||||
return
|
return
|
||||||
var exported := TestRun.export_temp_map(doc)
|
var mode: Node = playtest
|
||||||
if not bool(exported["ok"]):
|
playtest = null
|
||||||
_show_errors("Karte testen — die Map ist noch nicht lauffähig:", exported["errors"])
|
mode.exit()
|
||||||
_set_status("Testlauf nicht möglich — siehe Fenster für die fehlenden Punkte (z. B. Spawn-Zonen).", true)
|
mode.queue_free()
|
||||||
return
|
_playtest_set_frozen(false)
|
||||||
var map_name := str(doc.get("info", {}).get("name", "?"))
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
var args := TestRun.launch_args(client_dir, str(exported["path"]))
|
camera.current = true
|
||||||
var pid := OS.create_process(godot, args)
|
tool_mode = _playtest_tool_mode
|
||||||
if pid <= 0:
|
_update_tool_label()
|
||||||
_show_errors("Karte testen — Start fehlgeschlagen:", [
|
_set_status("Testlauf beendet — zurück im Editor (Auswahl und Dokument unverändert).")
|
||||||
"Das Spiel-Fenster konnte nicht gestartet werden (Programm: %s)." % godot])
|
|
||||||
_set_status("Karte testen: Start fehlgeschlagen.", true)
|
|
||||||
return
|
|
||||||
_set_status("▶ Karte „%s“ läuft im Test-Fenster — WASD laufen, ESC zurück zum Editor. Der Editor bleibt offen." % map_name)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_new_map_confirmed() -> void:
|
func _on_new_map_confirmed() -> void:
|
||||||
|
|
@ -1961,6 +2006,13 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
# bei jeder Taste, GUI-Start-Befund 15.10.). Die Godot-4-Ersatz-API ist
|
# bei jeder Taste, GUI-Start-Befund 15.10.). Die Godot-4-Ersatz-API ist
|
||||||
# is_command_or_control_pressed(): Ctrl auf Linux/Windows, Cmd auf macOS.
|
# is_command_or_control_pressed(): Ctrl auf Linux/Windows, Cmd auf macOS.
|
||||||
var ctrl: bool = key_evt.is_command_or_control_pressed()
|
var ctrl: bool = key_evt.is_command_or_control_pressed()
|
||||||
|
# Lauf-Modus (t_1cdc1a72): nur ESC behandeln — ALLE anderen Editor-Tasten
|
||||||
|
# (F1-F5, Undo/Redo, Werkzeuge) sind im Spiel eingefroren.
|
||||||
|
if playtest != null and playtest.active:
|
||||||
|
if key == KEY_ESCAPE:
|
||||||
|
playtest.handle_escape()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
match key:
|
match key:
|
||||||
KEY_Z:
|
KEY_Z:
|
||||||
if ctrl:
|
if ctrl:
|
||||||
|
|
|
||||||
122
tests/probe_playtest_render.gd
Normal file
122
tests/probe_playtest_render.gd
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
extends SceneTree
|
||||||
|
## WEGWERF-Renderprobe (Karte t_1cdc1a72, Lauf-Modus im Map Builder selbst) —
|
||||||
|
## (a) Spieleraugen-Startansicht am Team-0-Spawnpunkt (Kisten voraus, HUD an)
|
||||||
|
## (b) Nach 8 m Gehen (60 × step 1/30 s) — Spieler bewegt, bleibt auf Terrain
|
||||||
|
## (c) Nach ESC: Editor-Panels wieder da, Spiel-Kamera weg, Status meldet
|
||||||
|
## Ausführen:
|
||||||
|
## xvfb-run -a godot4 --path . --rendering-driver opengl3 -s res://tests/probe_playtest_render.gd
|
||||||
|
## Wegwerf-Probe (nicht im Repo-Commit — Datei-Grenzen der Karte), Beweise nach
|
||||||
|
## /opt/data/scopedvr/work/t1cdc_beweise/.
|
||||||
|
|
||||||
|
var main: Control
|
||||||
|
var subvp: SubViewport
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
func _png(name: String) -> String:
|
||||||
|
return "/opt/data/scopedvr/work/t1cdc_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 _press_key(code: Key) -> InputEventKey:
|
||||||
|
var key := InputEventKey.new()
|
||||||
|
key.keycode = code
|
||||||
|
key.physical_keycode = code
|
||||||
|
key.pressed = true
|
||||||
|
return key
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
DirAccess.make_dir_recursive_absolute("/opt/data/scopedvr/work/t1cdc_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-Szene: 1024er Map, 3 Kisten vor dem Spieler (Richtung -Z),
|
||||||
|
# Team-0-Spawn am Ursprung — der Spieler spawnt dort mit Blick auf die Kisten.
|
||||||
|
var doc: Dictionary = SvmapDocument.create_new("T1-Probe", 1024.0, "mittag")
|
||||||
|
doc["objects"] = [
|
||||||
|
{"id": "crate_wood", "pos": [0.0, 0.0, -150.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||||
|
{"id": "crate_wood", "pos": [-120.0, 0.0, -280.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||||
|
{"id": "crate_wood", "pos": [150.0, 0.0, -320.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||||
|
]
|
||||||
|
doc["spawn_zones"] = {"0": {"points": [{"id": "spawn0", "pos": [0.0, 0.0, 0.0]}]}}
|
||||||
|
main.set("doc", doc)
|
||||||
|
main.call("_refresh_all")
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
# --- (a) Lauf-Modus starten (F5-Route: _on_test_run) ----------------------
|
||||||
|
main.call("_on_test_run")
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var pt: Node = main.get("playtest")
|
||||||
|
print("ZAHLEN (a) playtest aktiv: ", pt != null, " | mode.active=", pt.get("active"))
|
||||||
|
var body: Node3D = pt.get("body")
|
||||||
|
print("ZAHLEN (a) Spawn: body.position=", body.position,
|
||||||
|
" | Terrainhöhe dort=", body.terrain_height(0.0, 0.0))
|
||||||
|
print("ZAHLEN (a) yaw=", body.get("yaw_deg"), " pitch=", body.get("pitch_deg"),
|
||||||
|
" eye_height=", body.get("eye_height"), " radius=", body.get("radius"))
|
||||||
|
var look_cam: Camera3D = body.get("look_cam")
|
||||||
|
var editor_cam: Camera3D = main.get("camera")
|
||||||
|
print("ZAHLEN (a) Spiel-Kamera current=", look_cam.current,
|
||||||
|
" | Editor-Kamera current=", editor_cam.current)
|
||||||
|
print("ZAHLEN (a) Kiste (0,0.6,-150) im Frustum=",
|
||||||
|
look_cam.is_position_in_frustum(Vector3(0.0, 0.6, -150.0)),
|
||||||
|
" | Kiste (−120,0.6,−280) im Frustum=",
|
||||||
|
look_cam.is_position_in_frustum(Vector3(-120.0, 0.6, -280.0)))
|
||||||
|
var hp: Label = pt.get("hud_pos")
|
||||||
|
print("ZAHLEN (a) HUD: hud=", pt.get("hud") != null, " pos-text='", hp.text, "'")
|
||||||
|
print("ZAHLEN (a) Top-Bar sichtbar=", main.get("ui_top_bar").visible,
|
||||||
|
" links=", main.get("ui_left_panel").visible,
|
||||||
|
" rechts=", main.get("ui_right_panel").visible)
|
||||||
|
await _shot("t1_a_spieler_startansicht.png")
|
||||||
|
|
||||||
|
# --- (b) 2 s Gehen (60 × step 1/30 s, fwd) — Bewegung + Terrainanpassung --
|
||||||
|
var pos_vor: Vector3 = body.position
|
||||||
|
for i in 60:
|
||||||
|
body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false,
|
||||||
|
"right": false, "run": false, "jump": false})
|
||||||
|
var pos_nach: Vector3 = body.position
|
||||||
|
print("ZAHLEN (b) Position vor/nach 60 steps: ", pos_vor, " → ", pos_nach,
|
||||||
|
" (Δ=", pos_vor.distance_to(pos_nach), " m)")
|
||||||
|
print("ZAHLEN (b) on_floor=", body.get("on_floor"),
|
||||||
|
" y-nach=", pos_nach.y,
|
||||||
|
" Terrainhöhe dort=", body.terrain_height(pos_nach.x, pos_nach.z))
|
||||||
|
await _shot("t1_b_spieler_gelaufen.png")
|
||||||
|
|
||||||
|
# --- (c) ESC: zurück in den Editor -----------------------------------------
|
||||||
|
main._unhandled_key_input(_press_key(KEY_ESCAPE))
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
print("ZAHLEN (c) playtest nach ESC null=", main.get("playtest") == null)
|
||||||
|
print("ZAHLEN (c) Top-Bar sichtbar=", main.get("ui_top_bar").visible,
|
||||||
|
" links=", main.get("ui_left_panel").visible,
|
||||||
|
" rechts=", main.get("ui_right_panel").visible,
|
||||||
|
" Statusleiste=", main.get("ui_status_bar").visible)
|
||||||
|
print("ZAHLEN (c) Editor-Kamera current=", editor_cam.current)
|
||||||
|
print("ZAHLEN (c) Status: ", main.status_label.text)
|
||||||
|
await _shot("t1_c_editor_zurueck.png")
|
||||||
|
print("PROBE_FERTIG")
|
||||||
|
quit(0)
|
||||||
1
tests/probe_playtest_render.gd.uid
Normal file
1
tests/probe_playtest_render.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://barv4phjmebje
|
||||||
|
|
@ -262,13 +262,19 @@ func _test_shortcuts_karte3() -> void:
|
||||||
main._unhandled_key_input(_press_key(KEY_H, false))
|
main._unhandled_key_input(_press_key(KEY_H, false))
|
||||||
_check(not main.help_overlay.visible, "H zweimal: Overlay wieder zu (Toggle)")
|
_check(not main.help_overlay.visible, "H zweimal: Overlay wieder zu (Toggle)")
|
||||||
|
|
||||||
# --- F5: Karte testen (echter Flow existiert — hier läuft er in den
|
# --- F5: Karte testen (Karte t_1cdc1a72): startet den Lauf-Modus im
|
||||||
# „Client nicht gefunden“-Zweig, Hauptsache die Route ist verdrahtet) -----
|
# Builder selbst — kein Client, kein Fehler-Dialog. ESC verlässt ihn
|
||||||
|
# sauber (Panels wieder sichtbar), danach geht der Test weiter. ---------
|
||||||
main._unhandled_key_input(_press_key(KEY_F5, false))
|
main._unhandled_key_input(_press_key(KEY_F5, false))
|
||||||
_check(str(main.status_label.text).contains("Karte testen"),
|
_check(str(main.status_label.text).contains("Testlauf läuft"),
|
||||||
"F5: Karte-testen-Flow angesprungen (Statuszeile meldet)")
|
"F5: Lauf-Modus gestartet (Statuszeile meldet)")
|
||||||
_check(main.error_dialog.visible, "F5: Fehler-Dialog im nicht-gefunden-Zweig geöffnet")
|
_check(not main.error_dialog.visible,
|
||||||
main.error_dialog.hide()
|
"F5: kein Fehler-Dialog (Lauf-Modus braucht keinen Client)")
|
||||||
|
_check(main.get("playtest") != null, "F5: PlaytestMode ist aktiv")
|
||||||
|
main._unhandled_key_input(_press_key(KEY_ESCAPE, false))
|
||||||
|
_check(main.get("playtest") == null, "F5+ESC: Lauf-Modus sauber beendet")
|
||||||
|
_check(main.error_dialog == null or not main.error_dialog.visible,
|
||||||
|
"F5+ESC: kein Fehler-Dialog offen")
|
||||||
|
|
||||||
# --- Dirty-Schutz: Strg+N bei ungespeicherten Änderungen FRAGT ZUERST ----
|
# --- Dirty-Schutz: Strg+N bei ungespeicherten Änderungen FRAGT ZUERST ----
|
||||||
main.dirty = true
|
main.dirty = true
|
||||||
|
|
|
||||||
288
tests/test_playtest.gd
Normal file
288
tests/test_playtest.gd
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Editor-Test (headless): Karte t_1cdc1a72 — Lauf-Modus IM Map-Builder selbst
|
||||||
|
## (erste Person, WASD + Maus, OHNE Client-Ordner/VR/Server).
|
||||||
|
## Ausführen: godot4 --headless --path . --script res://tests/test_playtest.gd
|
||||||
|
## Exit 0 = alles grün.
|
||||||
|
##
|
||||||
|
## Instanziert die ECHTE Main-UI (wie test_gui_smoke) und deckt ab:
|
||||||
|
## 1. Start/Zurück-Zustand: Modus-Flags, Panels aus/an, Maus-Capture-Wunsch,
|
||||||
|
## Spieler lebt im world3d, Kamera übernimmt.
|
||||||
|
## 2. Bodenhöhe == Terrainhöhe an der Spielerposition (Body-Decoder ==
|
||||||
|
## SvmapDocument-Decoder am selben Dokument).
|
||||||
|
## 3. Spieler bleibt vor einem Objekt-AABB stehen (Block-Kollision).
|
||||||
|
## 4. Rennen/Springen wirkt korrekt (step-Input-Dictionary).
|
||||||
|
## 5. Rückkehr stellt den Editor-Zustand VOLLSTÄNDIG wieder her
|
||||||
|
## (Dokument byte-identisch, Undo-Stapel unverändert, Panels sichtbar).
|
||||||
|
|
||||||
|
var failures := 0
|
||||||
|
var checks := 0
|
||||||
|
var main: Control
|
||||||
|
var camera: Camera3D
|
||||||
|
var subvp: SubViewport
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
camera = main.get("camera")
|
||||||
|
_check(camera != null, "Editor-Kamera gefunden")
|
||||||
|
var container := _find_viewport_container(main)
|
||||||
|
if container != null:
|
||||||
|
subvp = container.get_child(0) as SubViewport
|
||||||
|
_check(subvp != null, "SubViewport gefunden")
|
||||||
|
|
||||||
|
# Echte Map anlegen (Neu-Dialog-Konvention wie _on_new_map_confirmed).
|
||||||
|
var doc: Dictionary = SvmapDocument.create_new("Testlauf-Karte", 100.0, "klar")
|
||||||
|
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
|
||||||
|
|
||||||
|
_test_start_state()
|
||||||
|
_test_terrain_height_matches_document()
|
||||||
|
_test_obstacle_blocks_player()
|
||||||
|
_test_run_and_jump()
|
||||||
|
_test_exit_restores_editor()
|
||||||
|
|
||||||
|
_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 _find_viewport_container(node: Node) -> SubViewportContainer:
|
||||||
|
if node is SubViewportContainer:
|
||||||
|
return node as SubViewportContainer
|
||||||
|
for child in node.get_children():
|
||||||
|
var found := _find_viewport_container(child)
|
||||||
|
if found != null:
|
||||||
|
return found
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## PlaytestMode-Instanz der aktuellen Karte (main.playtest).
|
||||||
|
func _pt() -> Node:
|
||||||
|
return main.get("playtest")
|
||||||
|
|
||||||
|
|
||||||
|
func _body() -> Node:
|
||||||
|
var pt := _pt()
|
||||||
|
if pt == null:
|
||||||
|
return null
|
||||||
|
return pt.get("body")
|
||||||
|
|
||||||
|
|
||||||
|
## --- Test 1: Start/Zurück-Zustand -------------------------------------------
|
||||||
|
func _test_start_state() -> void:
|
||||||
|
print("Testgruppe: Start-Zustand (Modus-Flags, Panels, Maus, Kamera)")
|
||||||
|
main.call("_on_test_run")
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var pt := _pt()
|
||||||
|
_check(pt != null, "PlaytestMode angelegt (main.playtest)")
|
||||||
|
if pt == null:
|
||||||
|
return
|
||||||
|
_check(bool(pt.get("active")), "Modus-Flag active == true")
|
||||||
|
_check(pt.get("body") != null, "Spieler-Body existiert")
|
||||||
|
_check(pt.wants_capture(), "Maus-Capture-Wunsch gesetzt")
|
||||||
|
|
||||||
|
# Panels AUS: Top-Bar, linke/rechte Spalte, Statusleiste.
|
||||||
|
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 not panel.visible, "%s ausgeblendet" % pair[1])
|
||||||
|
|
||||||
|
# Spieler lebt in der 3D-Welt (world3d) und ist an den PlaytestMode gehängt.
|
||||||
|
var body: Node3D = pt.get("body")
|
||||||
|
_check(body != null and body.get_parent() == pt, "Body ist Kind des PlaytestMode")
|
||||||
|
_check(pt.get_parent() == main.get("world3d"), "PlaytestMode lebt im world3d")
|
||||||
|
|
||||||
|
# Spiel-Kamera ist current, Editor-Kamera abgegeben.
|
||||||
|
var body_cam: Camera3D = body.get("look_cam")
|
||||||
|
_check(body_cam != null and body_cam.current, "Spiel-Kamera ist current")
|
||||||
|
_check(not camera.current, "Editor-Kamera abgegeben")
|
||||||
|
|
||||||
|
# Start = erster Spawnpunkt (Team Blau) — sonst Kartenmitte.
|
||||||
|
var doc: Dictionary = main.get("doc")
|
||||||
|
var zones: Dictionary = doc.get("spawn_zones", {})
|
||||||
|
var team0: Dictionary = zones.get("0", {})
|
||||||
|
var pts: Array = team0.get("points", [])
|
||||||
|
if pts.is_empty():
|
||||||
|
_check(false, "Testmap braucht Spawn-Punkte (Team 0)")
|
||||||
|
return
|
||||||
|
var first: Dictionary = pts[0]
|
||||||
|
var pos: Array = first.get("pos", [0.0, 0.0, 0.0])
|
||||||
|
var expected := Vector3(float(pos[0]), 0.0, float(pos[2]))
|
||||||
|
var body_pos: Vector3 = body.position
|
||||||
|
_check(is_equal_approx(body_pos.x, expected.x) and is_equal_approx(body_pos.z, expected.z),
|
||||||
|
"Start == erster Spawnpunkt (%.1f, %.1f)" % [expected.x, expected.z])
|
||||||
|
|
||||||
|
# Kein History-Eintrag durch den Start (Dokument/Undo-Stapel unangetastet).
|
||||||
|
var hist: Node = main.get("history")
|
||||||
|
_check(not hist.can_undo(), "Kein Undo-Eintrag durch den Testlauf-Start")
|
||||||
|
|
||||||
|
|
||||||
|
## --- Test 2: Bodenhöhe == Terrainhöhe ---------------------------------------
|
||||||
|
func _test_terrain_height_matches_document() -> void:
|
||||||
|
print("Testgruppe: Bodenhöhe == Terrainhöhe an der Spielerposition")
|
||||||
|
var pt := _pt()
|
||||||
|
if pt == null:
|
||||||
|
_check(false, "PlaytestMode noch aktiv (Vorbedingung Test 1)")
|
||||||
|
return
|
||||||
|
var body: Node3D = pt.get("body")
|
||||||
|
var doc: Dictionary = main.get("doc")
|
||||||
|
# Schritt ausführen (Gravitation lässt den Spieler auf den Boden fallen).
|
||||||
|
body.call("step", 1.0 / 30.0, {"fwd": false, "back": false, "left": false,
|
||||||
|
"right": false, "run": false, "jump": false})
|
||||||
|
var feet_y: float = body.position.y
|
||||||
|
var terrain_y: float = SvmapDocument.terrain_height_at(doc, body.position.x, body.position.z)
|
||||||
|
var body_terrain: float = body.call("terrain_height", body.position.x, body.position.z)
|
||||||
|
_check(is_equal_approx(feet_y, terrain_y),
|
||||||
|
"Fusspunkt == Terrainhöhe am Spielerort (%.3f == %.3f)" % [feet_y, terrain_y])
|
||||||
|
_check(is_equal_approx(body_terrain, terrain_y),
|
||||||
|
"Body-Decoder == SvmapDocument-Decoder (%.3f == %.3f)" % [body_terrain, terrain_y])
|
||||||
|
|
||||||
|
|
||||||
|
## --- Test 3: Objekt-AABB blockiert den Spieler ------------------------------
|
||||||
|
func _test_obstacle_blocks_player() -> void:
|
||||||
|
print("Testgruppe: Spieler bleibt vor einem Objekt-AABB stehen")
|
||||||
|
var pt := _pt()
|
||||||
|
if pt == null:
|
||||||
|
_check(false, "PlaytestMode noch aktiv (Vorbedingung)")
|
||||||
|
return
|
||||||
|
var body: Node3D = pt.get("body")
|
||||||
|
# Hindernis VOR dem Spieler (Blick -Z bei yaw 0): Wand spannt
|
||||||
|
# z ∈ [start_z - 3.0, start_z - 2.4] — der Spieler kommt aus +Z und
|
||||||
|
# trifft zuerst die Face bei end.z (start_z - 2.4).
|
||||||
|
var start_x: float = body.position.x
|
||||||
|
var start_z: float = body.position.z
|
||||||
|
var wall := AABB(Vector3(body.position.x - 2.0, -5.0, start_z - 3.0),
|
||||||
|
Vector3(4.0, 10.0, 0.6))
|
||||||
|
body.get("obstacles").append(wall)
|
||||||
|
body.set("yaw_deg", 0.0) # Blick nach -Z
|
||||||
|
|
||||||
|
var ok := true
|
||||||
|
for i in 60: # 2 Sekunden laufen
|
||||||
|
body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false,
|
||||||
|
"right": false, "run": false, "jump": false})
|
||||||
|
# Abstand: führende Kante des Spielers (z - radius) zur dem Spieler
|
||||||
|
# zugewandten Wand-Face (end.z — er kommt aus +Z). >= 0 = davor geblieben.
|
||||||
|
var dist_to_wall: float = (
|
||||||
|
float(body.position.z) - float(body.get("radius"))
|
||||||
|
- float(wall.end.z))
|
||||||
|
_check(dist_to_wall >= -0.05,
|
||||||
|
"Spieler hält vor der Wand (Abstand %.2f m >= 0)" % dist_to_wall)
|
||||||
|
if dist_to_wall < -0.05:
|
||||||
|
ok = false
|
||||||
|
# Kein Drift in X (kein Links/Rechts-Input → Position unverändert).
|
||||||
|
_check(absf(float(body.position.x) - start_x) < 0.01, "Kein Drift in X")
|
||||||
|
# Hindernis wieder entfernen (sauberer Zustand für die Folgetests).
|
||||||
|
body.get("obstacles").erase(wall)
|
||||||
|
|
||||||
|
|
||||||
|
## --- Test 4: Rennen/Springen ------------------------------------------------
|
||||||
|
func _test_run_and_jump() -> void:
|
||||||
|
print("Testgruppe: Rennen/Springen wirkt korrekt")
|
||||||
|
var pt := _pt()
|
||||||
|
if pt == null:
|
||||||
|
_check(false, "PlaytestMode noch aktiv (Vorbedingung)")
|
||||||
|
return
|
||||||
|
var body: Node3D = pt.get("body")
|
||||||
|
|
||||||
|
# Gehen: 1 s bei 4 m/s → ~4 m Wegstrecke.
|
||||||
|
var start := Vector3(body.position)
|
||||||
|
for i in 30:
|
||||||
|
body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false,
|
||||||
|
"right": false, "run": false, "jump": false})
|
||||||
|
var walk_dist: float = (Vector3(body.position) - start).length()
|
||||||
|
_check(walk_dist > 3.0 and walk_dist < 5.0,
|
||||||
|
"Gehen ≈ 4 m/s (gemessen %.2f m in 1 s)" % walk_dist)
|
||||||
|
|
||||||
|
# Rennen: gleiche Zeit, Verdopplung → deutlich mehr Strecke.
|
||||||
|
var start2 := Vector3(body.position)
|
||||||
|
for i in 30:
|
||||||
|
body.call("step", 1.0 / 30.0, {"fwd": true, "back": false, "left": false,
|
||||||
|
"right": false, "run": true, "jump": false})
|
||||||
|
var run_dist: float = (Vector3(body.position) - start2).length()
|
||||||
|
_check(run_dist > walk_dist * 1.5,
|
||||||
|
"Rennen deutlich schneller (%.2f m > %.2f m)" % [run_dist, walk_dist])
|
||||||
|
|
||||||
|
# Springen: y-Impuls, Spieler verlässt den Boden und landet wieder.
|
||||||
|
var y0: float = body.position.y
|
||||||
|
var airborne := false
|
||||||
|
for i in 60:
|
||||||
|
var state: Dictionary = body.call("step", 1.0 / 30.0,
|
||||||
|
{"fwd": false, "back": false, "left": false, "right": false,
|
||||||
|
"run": false, "jump": i < 2})
|
||||||
|
if not bool(state["on_floor"]):
|
||||||
|
airborne = true
|
||||||
|
if i > 5 and bool(state["on_floor"]):
|
||||||
|
break
|
||||||
|
_check(airborne, "Sprung verlässt den Boden")
|
||||||
|
_check(is_equal_approx(float(body.position.y), y0), "Landung wieder auf Terrainhöhe")
|
||||||
|
|
||||||
|
|
||||||
|
## --- Test 5: Rückkehr stellt den Editor-Zustand wieder her ------------------
|
||||||
|
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 noch 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")
|
||||||
|
var tool_before: String = main.get("tool_mode")
|
||||||
|
var cam_target: Variant = camera.get("target")
|
||||||
|
var cam_yaw: float = camera.get("yaw_deg")
|
||||||
|
|
||||||
|
pt.call("handle_escape")
|
||||||
|
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(camera.current, "Editor-Kamera zurück im Bild")
|
||||||
|
_check(is_equal_approx(camera.get("yaw_deg"), cam_yaw), "Kamera-Yaw unverändert")
|
||||||
|
_check(camera.get("target") == cam_target, "Kamera-Ziel unverändert")
|
||||||
|
_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(str(main.get("tool_mode")) == tool_before, "Werkzeug-Modus unverändert")
|
||||||
|
_check(not pt.wants_capture() if pt != null else true,
|
||||||
|
"Maus-Capture-Wunsch zurückgenommen")
|
||||||
|
_check(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "Maus wieder frei")
|
||||||
1
tests/test_playtest.gd.uid
Normal file
1
tests/test_playtest.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://be0euw1c2aq60
|
||||||
Loading…
Reference in a new issue