T1b t_96e3c4ee: Test-Startpunkt per Klick — Toolbar/Taste T setzt den Punkt, wo F5 den Testlauf startet (Höhe automatisch vom Terrain, Klick auf Objekt/Bauteil = Boden darunter). Sidecar user://scopedvr_teststart.cfg je Kartenpfad, KEIN .svmap-Eintrag, kein Undo, kein Spawn-Check-Kontakt. F5-Fallback mit Klartext: Test-Start → erster Team-Spawnpunkt → Kartenmitte. T im Testlauf versetzt sofort zum Blickpunkt (Augenhöhe 1,70 m, Kollision geprüft) und setzt den Punkt neu. Marker (Säule+Ring, Akzentfarbe) sichtbar solange gesetzt. Suite tests/test_playtest_spawn.gd 56/56, Voll-Batterie 25/25 grün, Renderprobe (4 PNGs) unter /opt/data/scopedvr/work/t96e3_beweise/
This commit is contained in:
parent
b53e05a145
commit
a5d8f7f79d
16 changed files with 1256 additions and 11 deletions
|
|
@ -202,7 +202,8 @@ fixiert sie mit **L** oder dem Button „≈ Höhenlinien“ oben. Das Relief-Sh
|
||||||
| Eingabe | Wirkung |
|
| Eingabe | Wirkung |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **H** | Kurzhilfe-Overlay an/aus — alle Tasten dieser Datei als Panel über der Karte; Esc schließt ebenfalls. Nicht-blockierend: der Editor bleibt bedienbar |
|
| **H** | Kurzhilfe-Overlay an/aus — alle Tasten dieser Datei als Panel über der Karte; Esc schließt ebenfalls. Nicht-blockierend: der Editor bleibt bedienbar |
|
||||||
| **F5** | ▶ Karte testen (aktuelle Map sofort als Spiel durchlaufen; der Editor bleibt offen) |
|
| **T** | Werkzeug: Test-Startpunkt setzen (Karte t_96e3c4ee): Klick in die Karte = dort startet F5. Höhe kommt automatisch vom Terrain; Klick auf Objekt/Bauteil = Boden darunter; Entf im Werkzeug löscht den Punkt. **KEIN Spiel-Spawnpunkt** — er landet in einer Editor-Sidecar-Datei, nicht in der `.svmap` (Spawn-Checks/Analysen unberührt). Im Testlauf versetzt **T** dich sofort zum Blickpunkt und setzt dort den Test-Start |
|
||||||
|
| **F5** | ▶ Karte testen (aktuelle Map sofort als Spiel durchlaufen; der Editor bleibt offen). Start-Reihenfolge: gesetzter Test-Start → erster Team-Spawnpunkt → Kartenmitte (je mit Klartext-Hinweis) |
|
||||||
|
|
||||||
Die Statuszeile unten zeigt PERMANENT das aktive Werkzeug („Werkzeug: …“) plus
|
Die Statuszeile unten zeigt PERMANENT das aktive Werkzeug („Werkzeug: …“) plus
|
||||||
den Hinweis „Umschalt+F = Flugmodus“ — man sieht immer, wo man ist.
|
den Hinweis „Umschalt+F = Flugmodus“ — man sieht immer, wo man ist.
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,70 @@ func _ground_height_at(x: float, z: float, feet_y: float) -> float:
|
||||||
return g
|
return g
|
||||||
|
|
||||||
|
|
||||||
|
## Teleport zum Punkt unter dem Fadenkreuz (Karte t_96e3c4ee, Taste T im
|
||||||
|
## Testlauf): Strahl aus dem Auge → erster Treffer auf Terrain ODER Hindernis-
|
||||||
|
## AABB; gelandet wird auf dem Boden DARUNTER (Terrain + begehbare Block-Decken
|
||||||
|
## bis zum Ziel-Höhenpunkt). Nie im Objekt landen: ist der Zielpunkt von einem
|
||||||
|
## BLOCKIERENDEN Hindernis belegt, wird der Teleport abgelehnt (ok=false).
|
||||||
|
## Rückgabe: {ok, x, z, ground, msg} — msg im Klartext für HUD/Statuszeile.
|
||||||
|
func teleport_to_ground(origin: Vector3, dir: Vector3) -> Dictionary:
|
||||||
|
# Terrain-Treffer (derselbe Decoder wie im Editor-Werkzeug).
|
||||||
|
var t_hit: Variant = TerrainField.raycast(heights, res, size_x, size_z, origin, dir)
|
||||||
|
var t_dist := INF
|
||||||
|
if t_hit != null:
|
||||||
|
t_dist = origin.distance_to(t_hit as Vector3)
|
||||||
|
# Hindernis-Treffer: nähester AABB-Einstiegspunkt gewinnt gegen Terrain.
|
||||||
|
var o_dist := INF
|
||||||
|
var o_point := Vector3.ZERO
|
||||||
|
var d := dir
|
||||||
|
if d.length_squared() > 0.000000001:
|
||||||
|
d = d.normalized()
|
||||||
|
for ab in obstacles:
|
||||||
|
var box: AABB = ab
|
||||||
|
var t := TestStart.ray_aabb(origin, d, box)
|
||||||
|
if t >= 0.0 and t < o_dist:
|
||||||
|
o_dist = t
|
||||||
|
o_point = origin + d * t
|
||||||
|
# Ziel = der nähere der beiden Treffer.
|
||||||
|
var tx := 0.0
|
||||||
|
var tz := 0.0
|
||||||
|
var hit_y := 0.0
|
||||||
|
var found := false
|
||||||
|
if o_dist < t_dist:
|
||||||
|
tx = o_point.x
|
||||||
|
tz = o_point.z
|
||||||
|
hit_y = o_point.y
|
||||||
|
found = true
|
||||||
|
elif t_hit != null:
|
||||||
|
var th := t_hit as Vector3
|
||||||
|
tx = th.x
|
||||||
|
tz = th.z
|
||||||
|
hit_y = th.y
|
||||||
|
found = true
|
||||||
|
if not found:
|
||||||
|
return {"ok": false, "x": 0.0, "z": 0.0, "ground": 0.0,
|
||||||
|
"msg": "Kein Boden unterm Fadenkreuz — hier kann der Test-Start nicht hin."}
|
||||||
|
# Boden unter dem Zielpunkt: Terrain + begehbare Block-Decken bis zum
|
||||||
|
# Ziel-Höhenpunkt (Ziel auf einem Kisten-Dach = dort oben landen).
|
||||||
|
var ground := terrain_height(tx, tz)
|
||||||
|
var ceiling := maxf(hit_y, ground) + step_height
|
||||||
|
for ab in obstacles:
|
||||||
|
var box: AABB = ab
|
||||||
|
if box.end.y > ceiling or box.end.y <= ground:
|
||||||
|
continue
|
||||||
|
if tx < box.position.x or tx > box.end.x or tz < box.position.z or tz > box.end.z:
|
||||||
|
continue
|
||||||
|
ground = box.end.y
|
||||||
|
if _blocked_at(tx, tz, ground):
|
||||||
|
return {"ok": false, "x": tx, "z": tz, "ground": ground,
|
||||||
|
"msg": "Da steht etwas im Weg (%.1f, %.1f) — dort kannst du nicht landen." % [tx, tz]}
|
||||||
|
position = Vector3(tx, ground, tz)
|
||||||
|
velocity = Vector3.ZERO
|
||||||
|
on_floor = true
|
||||||
|
_apply_look()
|
||||||
|
return {"ok": true, "x": tx, "z": tz, "ground": ground, "msg": ""}
|
||||||
|
|
||||||
|
|
||||||
## Terrain-Höhe an Welt-X/Z — aus dem DOCUMENT-Raster (nichts neu rechnen).
|
## Terrain-Höhe an Welt-X/Z — aus dem DOCUMENT-Raster (nichts neu rechnen).
|
||||||
func terrain_height(x: float, z: float) -> float:
|
func terrain_height(x: float, z: float) -> float:
|
||||||
if res < 2 or size_x <= 0.0 or size_z <= 0.0:
|
if res < 2 or size_x <= 0.0 or size_z <= 0.0:
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ var hud: CanvasLayer = null
|
||||||
var hud_pos: Label = null
|
var hud_pos: Label = null
|
||||||
var hud_fps: Label = null
|
var hud_fps: Label = null
|
||||||
var hud_hint: Label = null
|
var hud_hint: Label = null
|
||||||
|
var hud_msg: Label = null # Klartext-Nachricht (z. B. Test-Start via T)
|
||||||
## FPS-Smoothing (Anzeige, keine Physik)
|
## FPS-Smoothing (Anzeige, keine Physik)
|
||||||
var _fps_smooth := 0.0
|
var _fps_smooth := 0.0
|
||||||
## Ist der Lauf-Modus gerade aktiv? (Reentrancy-Schutz für enter/exit)
|
## Ist der Lauf-Modus gerade aktiv? (Reentrancy-Schutz für enter/exit)
|
||||||
|
|
@ -44,10 +45,13 @@ signal exit_requested
|
||||||
|
|
||||||
|
|
||||||
## Starten: übernimmt die Sicht (Panels aus, Maus-Capture-Wunsch an) und
|
## Starten: übernimmt die Sicht (Panels aus, Maus-Capture-Wunsch an) und
|
||||||
## legt den Spieler am ersten Spawnpunkt (Team Blau) an — sonst Kartenmitte.
|
## legt den Spieler am Startpunkt an. Fallback-Reihenfolge (Karte t_96e3c4ee,
|
||||||
|
## je mit Klartext-Hinweis): gesetzter Test-Start (start_override) → erster
|
||||||
|
## Spawnpunkt (Team Blau) → Kartenmitte. start_override = Vector3 (nur x/z
|
||||||
|
## zählen, Höhe kommt automatisch vom Terrain) oder null (= altes Verhalten).
|
||||||
## view_builder = der map_view_builder-Node (Marker-Tree mit pick_radius/
|
## view_builder = der map_view_builder-Node (Marker-Tree mit pick_radius/
|
||||||
## half_height-Metas bleibt stehen und liefert die Hindernis-Blöcke).
|
## half_height-Metas bleibt stehen und liefert die Hindernis-Blöcke).
|
||||||
func enter(doc: Dictionary, view_builder: Node3D) -> void:
|
func enter(doc: Dictionary, view_builder: Node3D, start_override: Variant = null) -> void:
|
||||||
if active:
|
if active:
|
||||||
return
|
return
|
||||||
active = true
|
active = true
|
||||||
|
|
@ -58,6 +62,9 @@ func enter(doc: Dictionary, view_builder: Node3D) -> void:
|
||||||
body.call("set_terrain", doc)
|
body.call("set_terrain", doc)
|
||||||
body.obstacles = _collect_obstacles(doc, view_builder)
|
body.obstacles = _collect_obstacles(doc, view_builder)
|
||||||
var start := _spawn_start(doc)
|
var start := _spawn_start(doc)
|
||||||
|
if start_override != null:
|
||||||
|
var sp := start_override as Vector3
|
||||||
|
start = Vector3(sp.x, 0.0, sp.z)
|
||||||
body.position = Vector3(start.x, body.terrain_height(start.x, start.z), start.z)
|
body.position = Vector3(start.x, body.terrain_height(start.x, start.z), start.z)
|
||||||
add_child(body)
|
add_child(body)
|
||||||
|
|
||||||
|
|
@ -163,13 +170,44 @@ func _build_hud() -> void:
|
||||||
hud_pos = Label.new()
|
hud_pos = Label.new()
|
||||||
hud_fps = Label.new()
|
hud_fps = Label.new()
|
||||||
hud_hint = Label.new()
|
hud_hint = Label.new()
|
||||||
hud_hint.text = "WASD + Maus · Shift = rennen · Space = springen · ESC = zurück zum Editor"
|
hud_msg = Label.new()
|
||||||
for l: Label in [hud_pos, hud_fps, hud_hint]:
|
hud_hint.text = "WASD + Maus · Shift = rennen · Space = springen · T = zum Blickpunkt versetzen · ESC = zurück zum Editor"
|
||||||
|
hud_msg.text = ""
|
||||||
|
for l: Label in [hud_pos, hud_fps, hud_hint, hud_msg]:
|
||||||
l.add_theme_font_size_override("font_size", 16)
|
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)
|
vbox.add_child(l)
|
||||||
add_child(hud)
|
add_child(hud)
|
||||||
|
|
||||||
|
|
||||||
|
## Klartext-Nachricht im HUD (die Editor-Statuszeile ist im Lauf-Modus
|
||||||
|
## ausgeblendet — hier landet das Feedback für Taste T, Karte t_96e3c4ee).
|
||||||
|
func show_message(text: String) -> void:
|
||||||
|
if hud_msg != null and is_instance_valid(hud_msg):
|
||||||
|
hud_msg.text = text
|
||||||
|
|
||||||
|
|
||||||
|
## Taste T im Testlauf (Karte t_96e3c4ee, „jederzeit“): setzt den Test-Start
|
||||||
|
## auf den Punkt unterm Fadenkreuz und versetzt den Spieler SOFORT dorthin
|
||||||
|
## (Augenhöhe 1,70 m via look_cam-Offset, Kollision geprüft — nie in Objekten).
|
||||||
|
## Das Speichern (Sidecar) + Marker-Update macht main.gd mit der Rückgabe.
|
||||||
|
## Rückgabe: {ok, x, z, ground, msg} aus PlaytestBody.teleport_to_ground.
|
||||||
|
func teleport_crosshair() -> Dictionary:
|
||||||
|
if not active or body == null or body.look_cam == null:
|
||||||
|
return {"ok": false, "x": 0.0, "z": 0.0, "ground": 0.0, "msg": ""}
|
||||||
|
var vp: Viewport = body.look_cam.get_viewport()
|
||||||
|
var center: Vector2 = (vp.get_visible_rect().size if vp != null else Vector2(800, 600)) * 0.5
|
||||||
|
var origin: Vector3 = body.look_cam.project_ray_origin(center)
|
||||||
|
var dir: Vector3 = body.look_cam.project_ray_normal(center)
|
||||||
|
var res: Dictionary = body.call("teleport_to_ground", origin, dir)
|
||||||
|
if bool(res.get("ok")):
|
||||||
|
show_message("Test-Start gesetzt: x=%.0f z=%.0f — du bist jetzt dort (Höhe automatisch)." % [
|
||||||
|
float(res.get("x")), float(res.get("z"))])
|
||||||
|
else:
|
||||||
|
show_message(str(res.get("msg", "")))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if not active or body == null:
|
if not active or body == null:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
92
src/editor/playtest/test_start.gd
Normal file
92
src/editor/playtest/test_start.gd
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
extends RefCounted
|
||||||
|
class_name TestStart
|
||||||
|
## Test-Startpunkt (Karte t_96e3c4ee): der Punkt, an dem „▶ Karte testen“ (F5)
|
||||||
|
## den Spieler absetzt — KEIN Spiel-Spawnpunkt (taucht nicht in doc,
|
||||||
|
## Spawn-Checks/Analysen oder Validator auf, ändert keine Zonen).
|
||||||
|
##
|
||||||
|
## Bewusste Entscheidungen dieser Karte:
|
||||||
|
## - Der Punkt lebt NUR editor-seitig (Sidecar, test_start_store.gd) — das
|
||||||
|
## .svmap-Format (Validator V1–V16, Client/Server) bleibt UNBERÜHRT.
|
||||||
|
## - Gespeichert wird nur x/z. Die Höhe kommt IMMER automatisch aus dem
|
||||||
|
## Terrain an der Stelle (SvmapDocument.terrain_height_at) — Terrain
|
||||||
|
## formen lässt den Punkt nicht schweben und versinken.
|
||||||
|
## - Fallback-Reihenfolge F5 (je mit Klartext-Hinweis): gesetzter Test-Start
|
||||||
|
## → erster Team-Spawnpunkt (Team Blau, wie bisher) → Kartenmitte.
|
||||||
|
## - Trifft der Klick ein Objekt/Bauteil: landet der Punkt auf dem BODEN
|
||||||
|
## DARUNTER (nie im Objekt schwebend) — das Tool meldet es im Klartext.
|
||||||
|
##
|
||||||
|
## Alles pure Statics (kein UI, kein Node-Zugriff) → headless testbar in
|
||||||
|
## tests/test_playtest_spawn.gd.
|
||||||
|
|
||||||
|
## Kein Punkt gesetzt (Sentinel-Typ für Variant-Vergleiche in main.gd).
|
||||||
|
|
||||||
|
## Start-Punkt für den Testlauf auflösen (Fallback-Kette). test_point ist
|
||||||
|
## Vector3 (nur x/z zählen) oder null. Rückgabe:
|
||||||
|
## {pos: Vector3 (y = Terrainhöhe), quelle: "test"|"team"|"mitte",
|
||||||
|
## status: Klartext für die Statuszeile}
|
||||||
|
static func resolve_start(doc: Dictionary, test_point: Variant) -> Dictionary:
|
||||||
|
if test_point != null:
|
||||||
|
var p: Vector3 = test_point
|
||||||
|
var y := SvmapDocument.terrain_height_at(doc, p.x, p.z)
|
||||||
|
return {
|
||||||
|
"pos": Vector3(p.x, y, p.z),
|
||||||
|
"quelle": "test",
|
||||||
|
"status": "Start an deinem gesetzten Test-Startpunkt (%.0f, %.0f, Höhe automatisch)." % [p.x, p.z],
|
||||||
|
}
|
||||||
|
# Erster Team-Spawnpunkt (Team Blau = „0“) — exakt wie bisher (t_1cdc1a72).
|
||||||
|
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 {
|
||||||
|
"pos": Vector3(float(pos[0]), float(pos[1]), float(pos[2])),
|
||||||
|
"quelle": "team",
|
||||||
|
"status": "Kein Test-Start gesetzt — Start am ersten Team-Blau-Spawnpunkt.",
|
||||||
|
}
|
||||||
|
# Kartenmitte aus den Bounds.
|
||||||
|
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
|
||||||
|
return {
|
||||||
|
"pos": Vector3(cx, SvmapDocument.terrain_height_at(doc, cx, cz), cz),
|
||||||
|
"quelle": "mitte",
|
||||||
|
"status": "Weder Test-Start noch Team-Spawnpunkte — Start in der Kartenmitte.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Strahl → erster Treffer auf einer AABB (Slab-Methode) oder -1.0.
|
||||||
|
## Für „Klick trifft Bauteil“ im Editor-Tool und „Fadenkreuz trifft Hindernis“
|
||||||
|
## im Testlauf (kein Physik-Server im Editor/SubViewport).
|
||||||
|
## Rückgabe: Distanz t >= 0 (origin + dir * t) oder -1.0 = kein Treffer.
|
||||||
|
static func ray_aabb(origin: Vector3, dir: Vector3, box: AABB) -> float:
|
||||||
|
var d := dir
|
||||||
|
if d.length_squared() < 0.000000001:
|
||||||
|
return -1.0
|
||||||
|
d = d.normalized()
|
||||||
|
var tmin := 0.0
|
||||||
|
var tmax := INF
|
||||||
|
for axis in 3:
|
||||||
|
var o := origin[axis]
|
||||||
|
var dv := d[axis]
|
||||||
|
var lo := box.position[axis]
|
||||||
|
var hi := box.end[axis]
|
||||||
|
if absf(dv) < 0.0000001:
|
||||||
|
if o < lo or o > hi:
|
||||||
|
return -1.0
|
||||||
|
continue
|
||||||
|
var t1 := (lo - o) / dv
|
||||||
|
var t2 := (hi - o) / dv
|
||||||
|
if t1 > t2:
|
||||||
|
var tmp := t1
|
||||||
|
t1 = t2
|
||||||
|
t2 = tmp
|
||||||
|
tmin = maxf(tmin, t1)
|
||||||
|
tmax = minf(tmax, t2)
|
||||||
|
if tmin > tmax:
|
||||||
|
return -1.0
|
||||||
|
return tmin
|
||||||
1
src/editor/playtest/test_start.gd.uid
Normal file
1
src/editor/playtest/test_start.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b4barijsv6qs2
|
||||||
64
src/editor/playtest/test_start_store.gd
Normal file
64
src/editor/playtest/test_start_store.gd
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
extends RefCounted
|
||||||
|
class_name TestStartStore
|
||||||
|
## Sidecar-Speicher für den Test-Startpunkt (Karte t_96e3c4ee) — bewusst
|
||||||
|
## NICHT im .svmap-Format (Validator V1–V16 + Client/Server bleiben unberührt;
|
||||||
|
## eine Map ohne den Punkt verhält sich exakt wie bisher).
|
||||||
|
##
|
||||||
|
## Ablage: user://scopedvr_teststart.cfg, Abschnitt [test_start], je Kartenpfad
|
||||||
|
## ein Schlüssel (globalisierter Pfad). Ungespeicherte Maps landen unter dem
|
||||||
|
## Sammel-Schlüssel UNGESPEICHERT (Session-Speicher über Neustarts hinaus nur,
|
||||||
|
## wenn die Map gespeichert wird — der Punkt wandert beim Speichern mit).
|
||||||
|
## Gespeichert werden nur x/z: die Höhe kommt immer automatisch vom Terrain.
|
||||||
|
|
||||||
|
const CFG_PATH := "user://scopedvr_teststart.cfg"
|
||||||
|
const SECTION := "test_start"
|
||||||
|
const UNGESPEICHERT := "__ungespeichert__"
|
||||||
|
|
||||||
|
|
||||||
|
## Schlüssel für einen Kartenpfad (leer = ungespeicherte Map).
|
||||||
|
static func key_for(map_path: String) -> String:
|
||||||
|
if map_path.is_empty():
|
||||||
|
return UNGESPEICHERT
|
||||||
|
return ProjectSettings.globalize_path(map_path)
|
||||||
|
|
||||||
|
|
||||||
|
## Gespeicherten Punkt lesen (Vector3 mit y=0 — Höhe kommt vom Terrain)
|
||||||
|
## oder null, wenn keiner gesetzt ist.
|
||||||
|
static func load_point(map_path: String) -> Variant:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(CFG_PATH) != OK:
|
||||||
|
return null
|
||||||
|
var k := key_for(map_path)
|
||||||
|
var x: Variant = cfg.get_value(SECTION, k + "/x", null)
|
||||||
|
var z: Variant = cfg.get_value(SECTION, k + "/z", null)
|
||||||
|
if x == null or z == null:
|
||||||
|
return null
|
||||||
|
return Vector3(float(x), 0.0, float(z))
|
||||||
|
|
||||||
|
|
||||||
|
## Punkt speichern (nur x/z — Höhe automatisch).
|
||||||
|
static func save_point(map_path: String, pos: Vector3) -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
cfg.load(CFG_PATH) # Fehler → leere Config, Punkt wird neu angelegt.
|
||||||
|
var k := key_for(map_path)
|
||||||
|
cfg.set_value(SECTION, k + "/x", pos.x)
|
||||||
|
cfg.set_value(SECTION, k + "/z", pos.z)
|
||||||
|
cfg.save(CFG_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
## Punkt löschen (Datei bleibt — andere Maps sind unberührt).
|
||||||
|
## Bewusst Sektions-Neuaufbau statt erase_section_key: Godots
|
||||||
|
## erase_section_key löscht Keys mit "/" im Namen still NICHT (Probe 20.10.).
|
||||||
|
static func clear_point(map_path: String) -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(CFG_PATH) != OK:
|
||||||
|
return
|
||||||
|
var k := key_for(map_path)
|
||||||
|
var keep := {}
|
||||||
|
for key in cfg.get_section_keys(SECTION):
|
||||||
|
if not key.begins_with(k + "/"):
|
||||||
|
keep[key] = cfg.get_value(SECTION, key)
|
||||||
|
cfg.erase_section(SECTION)
|
||||||
|
for key in keep:
|
||||||
|
cfg.set_value(SECTION, key, keep[key])
|
||||||
|
cfg.save(CFG_PATH)
|
||||||
1
src/editor/playtest/test_start_store.gd.uid
Normal file
1
src/editor/playtest/test_start_store.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b3gc8f1aecsjl
|
||||||
272
src/editor/playtest/test_start_tool.gd
Normal file
272
src/editor/playtest/test_start_tool.gd
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
extends Node3D
|
||||||
|
## Test-Startpunkt-Werkzeug (Karte t_96e3c4ee): „hinklicken, wo man beim
|
||||||
|
## Testen spawnt“ — ein Klick auf Boden/Terrain setzt den Test-Startpunkt.
|
||||||
|
## KEIN Spiel-Spawnpunkt: der Punkt lebt NICHT im .svmap (Sidecar via
|
||||||
|
## TestStartStore), taucht in keinem Spawn-Check/Keiner Analyse auf und hat
|
||||||
|
## KEINEN Undo-Eintrag (editor-seitiger Session-Zustand, wie die UI-Größe).
|
||||||
|
##
|
||||||
|
## Bedienung (Einbindung in main.gd, wie T1 den Lauf-Modus angemeldet hat):
|
||||||
|
## Toolbar-Knopf „🎯 Test-Start“ oder Taste T → Werkzeug an
|
||||||
|
## Linksklick auf den Boden → Test-Start setzen (Höhe automatisch)
|
||||||
|
## Linksklick auf Objekt/Bauteil → Punkt auf den Boden DARUNTER (Klartext-
|
||||||
|
## Hinweis: „auf dem Boden unter dem Objekt“)
|
||||||
|
## Entf (im Werkzeug) / „Löschen“-Knopf rechts → Test-Start löschen
|
||||||
|
## ESC → Werkzeug beenden
|
||||||
|
## Rechte Maustaste: bewusst NICHT behandelt — sie gehört dem Freelook-Flug
|
||||||
|
## (Karte t_2470de89, absolute Priorität wie in jedem anderen Werkzeug).
|
||||||
|
##
|
||||||
|
## Marker (schmale Säule + Ring, Editor-Akzentfarbe) bleibt sichtbar, solange
|
||||||
|
## ein Punkt gesetzt ist — auch außerhalb des Werkzeugs. Vorschau-Ring folgt
|
||||||
|
## dem Cursor nur IM Werkzeug (wie der Spawn-Zonen-Ghost).
|
||||||
|
|
||||||
|
## Editor-Akzentfarbe (wie Statuszeile/Kurzhilfe in main.gd) — „Farbe aus dem
|
||||||
|
## Theme“ laut Karte; dieselbe Konstante, keine neue Farbwelt.
|
||||||
|
const ACCENT := Color(0.55, 0.8, 1.0)
|
||||||
|
|
||||||
|
var main # Hauptfenster (script-erweitert, Variant)
|
||||||
|
var camera: Camera3D
|
||||||
|
var view_builder # Welt-Bauer (script-erweitert, Variant)
|
||||||
|
var interaction # Objekt-Interaktion (für Objekt-Treffer-Test, nur LESEND)
|
||||||
|
|
||||||
|
var active := false
|
||||||
|
|
||||||
|
# Terrain-Cache für Cursor-Raycast (nur LESEN — dieses Werkzeug formt nichts)
|
||||||
|
var _heights := PackedFloat32Array()
|
||||||
|
var _res := 0
|
||||||
|
var _sx := 0.0
|
||||||
|
var _sz := 0.0
|
||||||
|
|
||||||
|
var _ghost: Node3D
|
||||||
|
var _marker: Node3D
|
||||||
|
var _cursor_valid := false
|
||||||
|
var _cursor := Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func setup(p_main, p_camera: Camera3D, p_builder, p_interaction) -> void:
|
||||||
|
main = p_main
|
||||||
|
camera = p_camera
|
||||||
|
view_builder = p_builder
|
||||||
|
interaction = p_interaction
|
||||||
|
_ensure_ghost()
|
||||||
|
_ensure_marker()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Aktivierung ---
|
||||||
|
|
||||||
|
func activate() -> bool:
|
||||||
|
active = true
|
||||||
|
refresh_from_doc()
|
||||||
|
return true # nie verweigert — der Test-Start braucht kein Auflösungs-Limit
|
||||||
|
|
||||||
|
|
||||||
|
func deactivate() -> void:
|
||||||
|
active = false
|
||||||
|
_cursor_valid = false
|
||||||
|
if _ghost != null and is_instance_valid(_ghost):
|
||||||
|
_ghost.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
## Nach Laden/Neu/Speichern (Pfadwechsel → anderer Sidecar-Schlüssel) und
|
||||||
|
## nach Terrain-Änderungen: Cache + Marker aus main.test_start_point frisch.
|
||||||
|
func refresh_from_doc() -> void:
|
||||||
|
var terrain: Dictionary = main.doc.get("terrain", {})
|
||||||
|
_res = int(terrain.get("resolution", 0))
|
||||||
|
_sx = float(terrain.get("size_x_m", 0.0))
|
||||||
|
_sz = float(terrain.get("size_z_m", 0.0))
|
||||||
|
if _res >= 2 and _sx > 0.0 and _sz > 0.0:
|
||||||
|
_heights = SvmapIO.decode_heights(terrain)
|
||||||
|
else:
|
||||||
|
_heights = PackedFloat32Array()
|
||||||
|
_update_marker()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- Eingaben ---
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not active:
|
||||||
|
return
|
||||||
|
if event is InputEventMouseMotion:
|
||||||
|
_update_cursor((event as InputEventMouseMotion).position)
|
||||||
|
return
|
||||||
|
if event is InputEventMouseButton:
|
||||||
|
var mb := event as InputEventMouseButton
|
||||||
|
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
_click(mb.position)
|
||||||
|
|
||||||
|
|
||||||
|
func _click(viewport_pos: Vector2) -> void:
|
||||||
|
# 1) Objekt-Treffer? (Kugel-Approx der interaction — derselbe Pfad wie die
|
||||||
|
# Auswahl; bewusst nur GERUFEN, interaction.gd bleibt unangetastet.)
|
||||||
|
var obj_idx: Variant = interaction.call("_pick_object", viewport_pos)
|
||||||
|
var hit_obj := false
|
||||||
|
if obj_idx is int and int(obj_idx) >= 0:
|
||||||
|
hit_obj = true
|
||||||
|
# 2) Bauteil-Treffer? (AABB-Strahl über BuildOps.piece_aabb — build_ops.gd
|
||||||
|
# wird nur gelesen/nutzend gerufen, nicht geändert.)
|
||||||
|
var origin := camera.project_ray_origin(viewport_pos)
|
||||||
|
var dir := camera.project_ray_normal(viewport_pos)
|
||||||
|
var hit_build := false
|
||||||
|
if not hit_obj:
|
||||||
|
var t_terrain := INF
|
||||||
|
var ground: Variant = _ray_hit(viewport_pos)
|
||||||
|
if ground != null:
|
||||||
|
t_terrain = origin.distance_to(ground as Vector3)
|
||||||
|
var best_t := INF
|
||||||
|
for teil in main.doc.get("gebaeude", []):
|
||||||
|
if not (teil is Dictionary):
|
||||||
|
continue
|
||||||
|
var ab: AABB = BuildOps.piece_aabb(teil)
|
||||||
|
if ab.size.length_squared() <= 0.0:
|
||||||
|
continue
|
||||||
|
var t := TestStart.ray_aabb(origin, dir, ab)
|
||||||
|
if t >= 0.0 and t < best_t and t < t_terrain + 0.01:
|
||||||
|
best_t = t
|
||||||
|
hit_build = true
|
||||||
|
# 3) Punkt setzen: x/z vom Strahl-Treffer, Höhe IMMER vom Terrain (nie
|
||||||
|
# im Objekt schwebend, nie versunken).
|
||||||
|
var hit: Variant = _ray_hit(viewport_pos)
|
||||||
|
if hit == null:
|
||||||
|
main.call("_set_status", "Da war kein Boden unter dem Mauszeiger — klicke IN die Karte (die grüne Fläche).", true)
|
||||||
|
return
|
||||||
|
var p: Vector3 = _clamped(hit as Vector3)
|
||||||
|
var hinweis := ""
|
||||||
|
if hit_obj:
|
||||||
|
hinweis = "auf dem Boden unter dem Objekt"
|
||||||
|
elif hit_build:
|
||||||
|
hinweis = "auf dem Boden unter dem Bauteil"
|
||||||
|
main.call("_test_start_apply", p.x, p.z, hinweis)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_cursor(viewport_pos: Vector2) -> void:
|
||||||
|
var hit: Variant = _ray_hit(viewport_pos)
|
||||||
|
if hit == null:
|
||||||
|
_cursor_valid = false
|
||||||
|
if _ghost != null and is_instance_valid(_ghost):
|
||||||
|
_ghost.visible = false
|
||||||
|
return
|
||||||
|
_cursor_valid = true
|
||||||
|
_cursor = _clamped(hit as Vector3)
|
||||||
|
if _ghost != null and is_instance_valid(_ghost):
|
||||||
|
_ghost.position = Vector3(_cursor.x, _cursor.y + 0.06, _cursor.z)
|
||||||
|
_ghost.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
## Strahl → Punkt auf dem Terrain (wie Terrain-/Spawn-Werkzeug;
|
||||||
|
## TerrainField.raycast — favorisiert echte Geländeberührung).
|
||||||
|
func _ray_hit(viewport_pos: Vector2) -> Variant:
|
||||||
|
if _res >= 2 and _heights.size() >= _res * _res:
|
||||||
|
return TerrainField.raycast(_heights, _res, _sx, _sz,
|
||||||
|
camera.project_ray_origin(viewport_pos), camera.project_ray_normal(viewport_pos))
|
||||||
|
var terrain: Dictionary = main.doc.get("terrain", {})
|
||||||
|
if terrain.is_empty():
|
||||||
|
return null
|
||||||
|
var plane_y := float(terrain.get("min_height_m", 0.0))
|
||||||
|
var origin := camera.project_ray_origin(viewport_pos)
|
||||||
|
var dir := camera.project_ray_normal(viewport_pos)
|
||||||
|
if absf(dir.y) < 0.0001:
|
||||||
|
return null
|
||||||
|
var t := (plane_y - origin.y) / dir.y
|
||||||
|
if t <= 0.0:
|
||||||
|
return null
|
||||||
|
return origin + dir * t
|
||||||
|
|
||||||
|
|
||||||
|
func _clamped(p: Vector3) -> Vector3:
|
||||||
|
var half := Vector2(_sx, _sz) * 0.5
|
||||||
|
p.x = clampf(p.x, -half.x, half.x)
|
||||||
|
p.z = clampf(p.z, -half.y, half.y)
|
||||||
|
p.y = SvmapDocument.terrain_height_at(main.doc, p.x, p.z)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- Marker ---
|
||||||
|
|
||||||
|
## Marker an den gesetzten Punkt hängen (oder ausblenden). Höhe IMMER
|
||||||
|
## automatisch: Terrainhöhe an (x, z) — nach Terrain-Formen bleibt der
|
||||||
|
## Punkt auf dem Boden (kein Schweben, kein Versinken).
|
||||||
|
func _update_marker() -> void:
|
||||||
|
_ensure_marker()
|
||||||
|
var point: Variant = main.get("test_start_point")
|
||||||
|
if point == null:
|
||||||
|
_marker.visible = false
|
||||||
|
return
|
||||||
|
var p: Vector3 = point
|
||||||
|
p.y = SvmapDocument.terrain_height_at(main.doc, p.x, p.z)
|
||||||
|
_marker.position = p
|
||||||
|
_marker.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------- Optik (Ghost/Marker) --
|
||||||
|
|
||||||
|
func _accent_mat(alpha: float) -> StandardMaterial3D:
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = Color(ACCENT.r, ACCENT.g, ACCENT.b, alpha)
|
||||||
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
mat.emission_enabled = true
|
||||||
|
mat.emission = ACCENT * 0.35
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_ghost() -> void:
|
||||||
|
if _ghost != null and is_instance_valid(_ghost):
|
||||||
|
return
|
||||||
|
_ghost = Node3D.new()
|
||||||
|
_ghost.visible = false
|
||||||
|
add_child(_ghost)
|
||||||
|
var ring := MeshInstance3D.new()
|
||||||
|
var torus := TorusMesh.new()
|
||||||
|
torus.inner_radius = 0.65
|
||||||
|
torus.outer_radius = 0.85
|
||||||
|
torus.rings = 48
|
||||||
|
torus.ring_segments = 6
|
||||||
|
ring.mesh = torus
|
||||||
|
ring.position.y = 0.05
|
||||||
|
ring.material_override = _accent_mat(0.6)
|
||||||
|
_ghost.add_child(ring)
|
||||||
|
|
||||||
|
|
||||||
|
## Marker: schmale Säule + Ring am Boden (Karten-Vorgabe), Akzentfarbe.
|
||||||
|
func _ensure_marker() -> void:
|
||||||
|
if _marker != null and is_instance_valid(_marker):
|
||||||
|
return
|
||||||
|
_marker = Node3D.new()
|
||||||
|
_marker.visible = false
|
||||||
|
add_child(_marker)
|
||||||
|
var pillar := MeshInstance3D.new()
|
||||||
|
var cyl := CylinderMesh.new()
|
||||||
|
cyl.top_radius = 0.12
|
||||||
|
cyl.bottom_radius = 0.12
|
||||||
|
cyl.height = 4.0
|
||||||
|
pillar.mesh = cyl
|
||||||
|
pillar.position.y = 2.0 # Fußpunkt auf dem Terrain
|
||||||
|
pillar.material_override = _accent_mat(0.55)
|
||||||
|
_marker.add_child(pillar)
|
||||||
|
var ring := MeshInstance3D.new()
|
||||||
|
var torus := TorusMesh.new()
|
||||||
|
torus.inner_radius = 0.55
|
||||||
|
torus.outer_radius = 0.75
|
||||||
|
torus.rings = 48
|
||||||
|
torus.ring_segments = 6
|
||||||
|
ring.mesh = torus
|
||||||
|
ring.position.y = 0.05
|
||||||
|
ring.material_override = _accent_mat(0.85)
|
||||||
|
_marker.add_child(ring)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Demo/Tests ----
|
||||||
|
|
||||||
|
## Demo-Hook (Suite/Renderprobe): Punkt programmatisch setzen — macht exakt,
|
||||||
|
## was ein Klick machen würde (ohne echte Maus-Ereignisse). hinweis = derselbe
|
||||||
|
## Klartext wie beim echten Klick ("" = freier Boden, sonst „unter dem Objekt“).
|
||||||
|
func demo_set(x: float, z: float, hinweis := "") -> void:
|
||||||
|
main.call("_test_start_apply", x, z, hinweis)
|
||||||
|
|
||||||
|
|
||||||
|
## Demo-Hook: Ghost an Position parken (Renderprobe ohne Maus).
|
||||||
|
func demo_park_ghost(x: float, z: float) -> void:
|
||||||
|
_cursor = _clamped(Vector3(x, 0.0, z))
|
||||||
|
_cursor_valid = true
|
||||||
|
_ghost.position = Vector3(_cursor.x, _cursor.y + 0.06, _cursor.z)
|
||||||
|
_ghost.visible = true
|
||||||
1
src/editor/playtest/test_start_tool.gd.uid
Normal file
1
src/editor/playtest/test_start_tool.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://byjsw857bu1k8
|
||||||
|
|
@ -65,7 +65,8 @@ static func text() -> String:
|
||||||
["F2", "Werkzeug: Objekte platzieren/auswählen"],
|
["F2", "Werkzeug: Objekte platzieren/auswählen"],
|
||||||
["F3", "Werkzeug: Bauen (Häuser)"],
|
["F3", "Werkzeug: Bauen (Häuser)"],
|
||||||
["F4", "Werkzeug: Spawn-Zonen malen"],
|
["F4", "Werkzeug: Spawn-Zonen malen"],
|
||||||
["F5", "▶ Karte testen"],
|
["T", "Werkzeug: Test-Startpunkt setzen — Klick in die Karte = dort startet F5 (KEIN Spiel-Spawnpunkt; im Testlauf versetzt T dich sofort zum Blickpunkt)"],
|
||||||
|
["F5", "▶ Karte testen (Start: Test-Start → erster Team-Spawnpunkt → Kartenmitte)"],
|
||||||
["H", "Diese Kurzhilfe an/aus (Esc schließt auch)"],
|
["H", "Diese Kurzhilfe an/aus (Esc schließt auch)"],
|
||||||
["L", "Höhenlinien FIXIEREN (immer sichtbar/unsichtbar) — ohne Fix erscheinen sie nur im Terrain-Werkzeug (F1)"],
|
["L", "Höhenlinien FIXIEREN (immer sichtbar/unsichtbar) — ohne Fix erscheinen sie nur im Terrain-Werkzeug (F1)"],
|
||||||
]))
|
]))
|
||||||
|
|
|
||||||
193
src/ui/main.gd
193
src/ui/main.gd
|
|
@ -163,6 +163,18 @@ var ui_right_panel: Control # rechte Werkzeug-Panel-Spalte (im Lauf-Modus a
|
||||||
var ui_status_bar: Control # Statusleiste (im Lauf-Modus aus)
|
var ui_status_bar: Control # Statusleiste (im Lauf-Modus aus)
|
||||||
var world3d: Node3D # 3D-Wurzel im SubViewport (Spieler lebt hier)
|
var world3d: Node3D # 3D-Wurzel im SubViewport (Spieler lebt hier)
|
||||||
|
|
||||||
|
# Test-Startpunkt (Karte t_96e3c4ee): „hinklicken, wo man beim Testen spawnt“.
|
||||||
|
# Editor-seitiger Session-Punkt — Sidecar-Datei (TestStartStore), bewusst
|
||||||
|
# NICHT im .svmap (Validator V1–V16 + Client/Server unberührt) und KEIN
|
||||||
|
# Spiel-Spawnpunkt (kein Undo, keine Spawn-Checks/Analysen). null = keiner
|
||||||
|
# gesetzt → F5 verhält sich exakt wie bisher (erster Team-Spawn, sonst Mitte).
|
||||||
|
var test_start_point: Variant = null # Vector3 (nur x/z) oder null
|
||||||
|
var test_start_tool # src/editor/playtest/test_start_tool.gd
|
||||||
|
var test_start_btn: Button # „🎯 Test-Start“ in der Top-Bar
|
||||||
|
var test_start_panel: VBoxContainer # rechte Spalte: Info + Löschen
|
||||||
|
var test_start_info: Label
|
||||||
|
var test_start_delete_btn: Button
|
||||||
|
|
||||||
# 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
|
||||||
|
|
@ -333,6 +345,17 @@ func _build_ui() -> void:
|
||||||
test_btn.pressed.connect(_on_test_run)
|
test_btn.pressed.connect(_on_test_run)
|
||||||
top_h.add_child(test_btn)
|
top_h.add_child(test_btn)
|
||||||
|
|
||||||
|
# Test-Startpunkt (Karte t_96e3c4ee): Taste T oder dieser Knopf aktiviert
|
||||||
|
# das Werkzeug — ein Klick in die Karte setzt den Punkt, an dem F5 den
|
||||||
|
# Testlauf startet (KEIN Spiel-Spawnpunkt, kein .svmap-Eintrag).
|
||||||
|
test_start_btn = Button.new()
|
||||||
|
test_start_btn.text = "🎯 Test-Start"
|
||||||
|
test_start_btn.toggle_mode = true
|
||||||
|
test_start_btn.custom_minimum_size = UIScale.v2(160, 40)
|
||||||
|
test_start_btn.tooltip_text = "Test-Startpunkt setzen (Taste T): Klicke in die Karte, wo der Testlauf (F5) starten soll. Kein Spiel-Spawnpunkt — gilt nur fürs Testen im Editor."
|
||||||
|
test_start_btn.pressed.connect(_on_test_start_btn_pressed)
|
||||||
|
top_h.add_child(test_start_btn)
|
||||||
|
|
||||||
# UI-Größe (Karte t_297221b5): Klein/Mittel/Groß — Sly stellt das selbst um,
|
# UI-Größe (Karte t_297221b5): Klein/Mittel/Groß — Sly stellt das selbst um,
|
||||||
# es wirkt SOFORT (kompletter UI-Neuaufbau mit dem neuen Faktor) und bleibt
|
# es wirkt SOFORT (kompletter UI-Neuaufbau mit dem neuen Faktor) und bleibt
|
||||||
# in user://scopedvr_editor.cfg gespeichert.
|
# in user://scopedvr_editor.cfg gespeichert.
|
||||||
|
|
@ -869,6 +892,13 @@ func _build_center_viewport() -> SubViewportContainer:
|
||||||
world.add_child(build_tool)
|
world.add_child(build_tool)
|
||||||
build_tool.setup(self, camera, view_builder, interaction)
|
build_tool.setup(self, camera, view_builder, interaction)
|
||||||
|
|
||||||
|
# Test-Startpunkt-Werkzeug (Karte t_96e3c4ee) — ditto eigener Eingabe-Knoten
|
||||||
|
# (Taste T / Toolbar): Klick setzt den Test-Start, Marker bleibt sichtbar.
|
||||||
|
test_start_tool = Node3D.new()
|
||||||
|
test_start_tool.set_script(load("res://src/editor/playtest/test_start_tool.gd"))
|
||||||
|
world.add_child(test_start_tool)
|
||||||
|
test_start_tool.setup(self, camera, view_builder, interaction)
|
||||||
|
|
||||||
# Bedien-Hinweis im Viewport (Overlay)
|
# Bedien-Hinweis im Viewport (Overlay)
|
||||||
help_label = Label.new()
|
help_label = Label.new()
|
||||||
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR / Mausrad: drehen · Strg+D: duplizieren · Bild↑/↓: anheben/absenken · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
|
help_label.text = "Linksklick: auswählen & ziehen · Umschalt+Klick: Auswahl erweitern · Rahmen aufziehen: mehrere wählen · Strg+G: gruppieren\nR / Mausrad: drehen · Strg+D: duplizieren · Bild↑/↓: anheben/absenken · Spiegeln rechts · Entf: löschen · Strg+Z: zurück"
|
||||||
|
|
@ -1279,6 +1309,26 @@ func _build_right_panel() -> PanelContainer:
|
||||||
+ "(Wände aufs Fundament stapeln, Dach auf die Wände). Jedes Bauteil ist "
|
+ "(Wände aufs Fundament stapeln, Dach auf die Wände). Jedes Bauteil ist "
|
||||||
+ "EIN Rückgängig-Schritt (Strg+Z). ESC beendet den Bauen-Modus."))
|
+ "EIN Rückgängig-Schritt (Strg+Z). ESC beendet den Bauen-Modus."))
|
||||||
|
|
||||||
|
# --- Test-Startpunkt (Karte t_96e3c4ee): Info + Löschen im Werkzeug ---
|
||||||
|
test_start_panel = VBoxContainer.new()
|
||||||
|
test_start_panel.add_theme_constant_override("separation", UIScale.px(6))
|
||||||
|
test_start_panel.visible = false
|
||||||
|
vbox.add_child(test_start_panel)
|
||||||
|
|
||||||
|
test_start_info = Label.new()
|
||||||
|
test_start_info.text = ""
|
||||||
|
test_start_info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
test_start_panel.add_child(test_start_info)
|
||||||
|
test_start_delete_btn = Button.new()
|
||||||
|
test_start_delete_btn.text = "🗑 Test-Start löschen"
|
||||||
|
test_start_delete_btn.custom_minimum_size = UIScale.v2(0, 40)
|
||||||
|
test_start_delete_btn.tooltip_text = "Test-Startpunkt entfernen — F5 startet dann wieder am ersten Team-Spawnpunkt (sonst Kartenmitte). Entf geht auch."
|
||||||
|
test_start_delete_btn.pressed.connect(_on_test_start_delete_pressed)
|
||||||
|
test_start_panel.add_child(test_start_delete_btn)
|
||||||
|
test_start_panel.add_child(_muted_label(
|
||||||
|
"Der Test-Start ist KEIN Spiel-Spawnpunkt: er landet nur in einer "
|
||||||
|
+ "Editor-Datei, nicht in der Map — Spawn-Checks und Analysen bleiben unberührt."))
|
||||||
|
|
||||||
# Signale (Änderungen → Dokument + Ansicht)
|
# Signale (Änderungen → Dokument + Ansicht)
|
||||||
prop_name.text_changed.connect(_on_prop_changed.bind("name"))
|
prop_name.text_changed.connect(_on_prop_changed.bind("name"))
|
||||||
prop_author.text_changed.connect(_on_prop_changed.bind("author"))
|
prop_author.text_changed.connect(_on_prop_changed.bind("author"))
|
||||||
|
|
@ -1410,6 +1460,11 @@ func _rebuild_ui_for_scale() -> void:
|
||||||
if str(_update_result.get("status")) == "update_available":
|
if str(_update_result.get("status")) == "update_available":
|
||||||
_show_update_banner(str(_update_result.get("local", "")), str(_update_result.get("remote", "")))
|
_show_update_banner(str(_update_result.get("local", "")), str(_update_result.get("remote", "")))
|
||||||
_sync_size_menu()
|
_sync_size_menu()
|
||||||
|
# Karte t_96e3c4ee: Werkzeug + Marker für den Test-Start neu aufsetzen
|
||||||
|
# (das Rebuild hat einen frischen Tool-Node ohne Marker angelegt).
|
||||||
|
if test_start_tool != null:
|
||||||
|
test_start_tool.call("refresh_from_doc")
|
||||||
|
_sync_test_start_panel()
|
||||||
|
|
||||||
|
|
||||||
## Fenster beim Start: maximiert (Default, kein Vollbild-Zwang) oder die
|
## Fenster beim Start: maximiert (Default, kein Vollbild-Zwang) oder die
|
||||||
|
|
@ -1761,14 +1816,17 @@ func _start_playtest() -> void:
|
||||||
playtest.exit_requested.connect(_on_playtest_exit_requested)
|
playtest.exit_requested.connect(_on_playtest_exit_requested)
|
||||||
world3d.add_child(playtest)
|
world3d.add_child(playtest)
|
||||||
camera.current = false # Editor-Orbit abgeben — die Spiel-Kamera übernimmt
|
camera.current = false # Editor-Orbit abgeben — die Spiel-Kamera übernimmt
|
||||||
playtest.enter(doc, view_builder)
|
# Karte t_96e3c4ee: Fallback-Reihenfolge mit Klartext-Hinweis — gesetzter
|
||||||
_set_status("▶ Testlauf läuft: WASD laufen, Shift rennen, Space springen, Maus umsehen — ESC zurück zum Editor.")
|
# Test-Start → erster Team-Spawnpunkt → Kartenmitte.
|
||||||
|
var resolved: Dictionary = TestStart.resolve_start(doc, test_start_point)
|
||||||
|
playtest.enter(doc, view_builder, resolved["pos"])
|
||||||
|
_set_status("▶ Testlauf läuft: WASD laufen, Shift rennen, Space springen, Maus umsehen, T = zum Blickpunkt versetzen — ESC zurück zum Editor. %s" % resolved["status"])
|
||||||
|
|
||||||
|
|
||||||
## Editor-Input einfrieren/freigeben (Kamera-Flug, Werkzeuge, Gizmo).
|
## Editor-Input einfrieren/freigeben (Kamera-Flug, Werkzeuge, Gizmo).
|
||||||
## Nur auf die Werkzeug-Nodes — die Sicht (view_builder) bleibt 1:1 stehen.
|
## Nur auf die Werkzeug-Nodes — die Sicht (view_builder) bleibt 1:1 stehen.
|
||||||
func _playtest_set_frozen(on: bool) -> void:
|
func _playtest_set_frozen(on: bool) -> void:
|
||||||
for t in [camera, interaction, terrain_tool, spawn_tool, build_tool]:
|
for t in [camera, interaction, terrain_tool, spawn_tool, build_tool, test_start_tool]:
|
||||||
if t == null:
|
if t == null:
|
||||||
continue
|
continue
|
||||||
t.set_process_unhandled_input(not on)
|
t.set_process_unhandled_input(not on)
|
||||||
|
|
@ -1803,6 +1861,7 @@ func _on_new_map_confirmed() -> void:
|
||||||
history.begin(doc)
|
history.begin(doc)
|
||||||
_clear_edit_state()
|
_clear_edit_state()
|
||||||
_refresh_all()
|
_refresh_all()
|
||||||
|
_test_start_reload() # Karte t_96e3c4ee: neue Map → kein Test-Start gesetzt
|
||||||
_set_status("Neue Map „%s“ erstellt (%.0f × %.0f m, Wetter: %s). Wähle links ein Objekt, um zu bauen!" % [
|
_set_status("Neue Map „%s“ erstellt (%.0f × %.0f m, Wetter: %s). Wähle links ein Objekt, um zu bauen!" % [
|
||||||
new_dialog.get_map_name(), new_dialog.get_size_m(), new_dialog.get_size_m(), new_dialog.get_weather()])
|
new_dialog.get_map_name(), new_dialog.get_size_m(), new_dialog.get_size_m(), new_dialog.get_weather()])
|
||||||
|
|
||||||
|
|
@ -1819,6 +1878,7 @@ func _on_open_selected(path: String) -> void:
|
||||||
history.begin(doc)
|
history.begin(doc)
|
||||||
_clear_edit_state()
|
_clear_edit_state()
|
||||||
_refresh_all()
|
_refresh_all()
|
||||||
|
_test_start_reload() # Karte t_96e3c4ee: Sidecar der geladenen Map lesen
|
||||||
var map_name := str(doc.get("info", {}).get("name", "?"))
|
var map_name := str(doc.get("info", {}).get("name", "?"))
|
||||||
_set_status("Map „%s“ geladen (%d Objekte). Klicke ein Objekt in der Karte an, um es anzupassen." % [map_name, SvmapDocument.object_count(doc)])
|
_set_status("Map „%s“ geladen (%d Objekte). Klicke ein Objekt in der Karte an, um es anzupassen." % [map_name, SvmapDocument.object_count(doc)])
|
||||||
|
|
||||||
|
|
@ -1856,6 +1916,7 @@ func _save_to(path: String) -> void:
|
||||||
return
|
return
|
||||||
current_path = path
|
current_path = path
|
||||||
dirty = false
|
dirty = false
|
||||||
|
_test_start_reload() # Karte t_96e3c4ee: Punkt wandert beim Speichern mit
|
||||||
_refresh_file_label()
|
_refresh_file_label()
|
||||||
_set_status("Map gespeichert: %s" % path)
|
_set_status("Map gespeichert: %s" % path)
|
||||||
_refresh_tree()
|
_refresh_tree()
|
||||||
|
|
@ -1926,6 +1987,7 @@ func _update_tool_label() -> void:
|
||||||
"terrain": "Terrain (F1)",
|
"terrain": "Terrain (F1)",
|
||||||
"bauen": "Bauen (F3)",
|
"bauen": "Bauen (F3)",
|
||||||
"spawns": "Spawn-Zonen (F4)",
|
"spawns": "Spawn-Zonen (F4)",
|
||||||
|
"teststart": "Test-Start (T)",
|
||||||
}
|
}
|
||||||
tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode))
|
tool_label.text = "Werkzeug: %s · Umschalt+F = Flugmodus" % str(names.get(tool_mode, tool_mode))
|
||||||
|
|
||||||
|
|
@ -2006,12 +2068,20 @@ 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
|
# Lauf-Modus (t_1cdc1a72): nur ESC + T behandeln — ALLE anderen Editor-
|
||||||
# (F1-F5, Undo/Redo, Werkzeuge) sind im Spiel eingefroren.
|
# Tasten (F1-F5, Undo/Redo, Werkzeuge) sind im Spiel eingefroren.
|
||||||
if playtest != null and playtest.active:
|
if playtest != null and playtest.active:
|
||||||
if key == KEY_ESCAPE:
|
if key == KEY_ESCAPE:
|
||||||
playtest.handle_escape()
|
playtest.handle_escape()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
elif key == KEY_T:
|
||||||
|
# Karte t_96e3c4ee, „jederzeit“: T im Testlauf = Test-Start auf den
|
||||||
|
# Punkt unterm Fadenkreuz setzen + SOFORT dorthin versetzen
|
||||||
|
# (Augenhöhe 1,70 m, Kollision geprüft). Speichern + Marker: unten.
|
||||||
|
var tp: Dictionary = playtest.call("teleport_crosshair")
|
||||||
|
if bool(tp.get("ok")):
|
||||||
|
_test_start_apply(float(tp.get("x")), float(tp.get("z")), "")
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
match key:
|
match key:
|
||||||
KEY_Z:
|
KEY_Z:
|
||||||
|
|
@ -2061,6 +2131,12 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
# (Karte t_b84af40f) — F5 ruft ihn direkt auf.
|
# (Karte t_b84af40f) — F5 ruft ihn direkt auf.
|
||||||
_on_test_run()
|
_on_test_run()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
KEY_T:
|
||||||
|
# Karte t_96e3c4ee: T = Test-Start-Werkzeug an/aus (Toggle, wie F1–F4).
|
||||||
|
# Nur ohne Modifier + gleiche Freigabe wie F1–F5/H.
|
||||||
|
if not ctrl and _tool_key_allowed():
|
||||||
|
_set_tool_mode("objects" if tool_mode == "teststart" else "teststart")
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
KEY_H:
|
KEY_H:
|
||||||
# Karte 3/3: Kurzhilfe-Overlay an/aus (Esc schließt ebenfalls).
|
# Karte 3/3: Kurzhilfe-Overlay an/aus (Esc schließt ebenfalls).
|
||||||
if _tool_key_allowed():
|
if _tool_key_allowed():
|
||||||
|
|
@ -2126,6 +2202,14 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
if gizmo != null and gizmo.cancel_drag_if_active():
|
if gizmo != null and gizmo.cancel_drag_if_active():
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
# Karte t_96e3c4ee: Das Test-Start-Werkzeug hat keinen ShortcutPolicy-
|
||||||
|
# Eintrag (Datei-Grenze der Karte) — Esc verlässt es direkt. Der
|
||||||
|
# gesetzte Punkt bleibt dabei erhalten (nur das Werkzeug endet).
|
||||||
|
if tool_mode == "teststart":
|
||||||
|
_set_tool_mode("objects")
|
||||||
|
_set_status("Test-Start-Werkzeug beendet — der gesetzte Punkt bleibt gesetzt (F5 startet dort).")
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
# Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar):
|
# Esc-Priorität über pure Funktion (Karte t_c46d31c2, testbar):
|
||||||
# Werkzeug verlassen → Greifen zurückrollen → Ghost-Preview
|
# Werkzeug verlassen → Greifen zurückrollen → Ghost-Preview
|
||||||
# abbrechen → Auswahl aufheben (Greifen: Karte t_55257792).
|
# abbrechen → Auswahl aufheben (Greifen: Karte t_55257792).
|
||||||
|
|
@ -2161,6 +2245,10 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
spawn_tool.call("delete_selected")
|
spawn_tool.call("delete_selected")
|
||||||
elif tool_mode == "bauen":
|
elif tool_mode == "bauen":
|
||||||
build_tool.call("delete_selected")
|
build_tool.call("delete_selected")
|
||||||
|
elif tool_mode == "teststart":
|
||||||
|
# Karte t_96e3c4ee: Entf im Test-Start-Werkzeug = Punkt löschen.
|
||||||
|
_test_start_delete()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
else:
|
else:
|
||||||
interaction.delete_selected()
|
interaction.delete_selected()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
@ -2320,24 +2408,34 @@ func _set_tool_mode(mode: String) -> void:
|
||||||
_sync_terrain_button()
|
_sync_terrain_button()
|
||||||
_sync_spawn_button()
|
_sync_spawn_button()
|
||||||
_sync_build_button()
|
_sync_build_button()
|
||||||
|
_sync_test_start_button()
|
||||||
_update_tool_label()
|
_update_tool_label()
|
||||||
return
|
return
|
||||||
if mode == "terrain":
|
if mode == "terrain":
|
||||||
_leave_spawn_mode()
|
_leave_spawn_mode()
|
||||||
_leave_build_mode()
|
_leave_build_mode()
|
||||||
|
_leave_test_start_mode()
|
||||||
_enter_terrain_mode()
|
_enter_terrain_mode()
|
||||||
elif mode == "spawns":
|
elif mode == "spawns":
|
||||||
_leave_terrain_mode()
|
_leave_terrain_mode()
|
||||||
_leave_build_mode()
|
_leave_build_mode()
|
||||||
|
_leave_test_start_mode()
|
||||||
_enter_spawn_mode()
|
_enter_spawn_mode()
|
||||||
elif mode == "bauen":
|
elif mode == "bauen":
|
||||||
_leave_terrain_mode()
|
_leave_terrain_mode()
|
||||||
_leave_spawn_mode()
|
_leave_spawn_mode()
|
||||||
|
_leave_test_start_mode()
|
||||||
_enter_build_mode()
|
_enter_build_mode()
|
||||||
|
elif mode == "teststart":
|
||||||
|
_leave_terrain_mode()
|
||||||
|
_leave_spawn_mode()
|
||||||
|
_leave_build_mode()
|
||||||
|
_enter_test_start_mode()
|
||||||
else:
|
else:
|
||||||
_leave_terrain_mode()
|
_leave_terrain_mode()
|
||||||
_leave_spawn_mode()
|
_leave_spawn_mode()
|
||||||
_leave_build_mode()
|
_leave_build_mode()
|
||||||
|
_leave_test_start_mode()
|
||||||
_enter_objects_mode()
|
_enter_objects_mode()
|
||||||
_update_tool_label()
|
_update_tool_label()
|
||||||
# Karte t_89c0ec95: Werkzeugwechsel = Höhenlinien-Umschalter — nach dem
|
# Karte t_89c0ec95: Werkzeugwechsel = Höhenlinien-Umschalter — nach dem
|
||||||
|
|
@ -2441,6 +2539,91 @@ func _sync_build_button() -> void:
|
||||||
build_edit_btn.button_pressed = tool_mode == "bauen"
|
build_edit_btn.button_pressed = tool_mode == "bauen"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------- Test-Startpunkt -------
|
||||||
|
# (Karte t_96e3c4ee — „hinklicken, wo man beim Testen spawnt“. Editor-seitig,
|
||||||
|
# Sidecar statt .svmap, KEIN Spiel-Spawnpunkt: kein Undo, keine Spawn-Checks.)
|
||||||
|
|
||||||
|
func _on_test_start_btn_pressed() -> void:
|
||||||
|
if test_start_btn.button_pressed:
|
||||||
|
_set_tool_mode("teststart")
|
||||||
|
else:
|
||||||
|
_set_tool_mode("objects")
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_test_start_mode() -> void:
|
||||||
|
interaction.call("cancel_placement")
|
||||||
|
interaction.call("clear_selection", true)
|
||||||
|
tool_mode = "teststart"
|
||||||
|
test_start_panel.visible = true
|
||||||
|
help_label.text = "Linksklick: Test-Start setzen (Höhe automatisch aufs Terrain) · Auf Objekt/Bauteil: Boden darunter\nEntf: Test-Start löschen · Mittlere Maustaste: Kamera · ESC: Werkzeug beenden"
|
||||||
|
test_start_btn.button_pressed = true
|
||||||
|
test_start_tool.call("activate")
|
||||||
|
_sync_test_start_panel()
|
||||||
|
_set_status("Test-Start-Werkzeug an: Klicke in die Karte, wo der Testlauf (F5) starten soll. Der Punkt ist KEIN Spiel-Spawnpunkt — er landet nur in einer Editor-Datei, nicht in der Map.")
|
||||||
|
|
||||||
|
|
||||||
|
func _leave_test_start_mode() -> void:
|
||||||
|
if test_start_tool != null:
|
||||||
|
test_start_tool.call("deactivate")
|
||||||
|
if test_start_panel != null:
|
||||||
|
test_start_panel.visible = false
|
||||||
|
_sync_test_start_button()
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_test_start_button() -> void:
|
||||||
|
test_start_btn.button_pressed = tool_mode == "teststart"
|
||||||
|
|
||||||
|
|
||||||
|
## Panel-Zeile rechts (Info + Löschen-Knopf) mit dem Punkt-Zustand syncen.
|
||||||
|
func _sync_test_start_panel() -> void:
|
||||||
|
if test_start_info == null or test_start_delete_btn == null:
|
||||||
|
return
|
||||||
|
if test_start_point == null:
|
||||||
|
test_start_info.text = "Kein Test-Start gesetzt.\nF5 startet dann am ersten Team-Spawnpunkt (sonst Kartenmitte)."
|
||||||
|
test_start_delete_btn.disabled = true
|
||||||
|
else:
|
||||||
|
var p := test_start_point as Vector3
|
||||||
|
test_start_info.text = "Test-Start: x=%.0f z=%.0f\n(Höhe kommt automatisch vom Terrain)" % [p.x, p.z]
|
||||||
|
test_start_delete_btn.disabled = false
|
||||||
|
|
||||||
|
|
||||||
|
## Klick (oder Taste T im Testlauf): Test-Start setzen. NUR x/z — die Höhe
|
||||||
|
## kommt immer automatisch vom Terrain (Marker holt sie je nach Stand).
|
||||||
|
## Kein History-Eintrag, kein doc-Eintrag, kein Spawn-Check-Kontakt.
|
||||||
|
func _test_start_apply(x: float, z: float, hinweis: String) -> void:
|
||||||
|
test_start_point = Vector3(x, 0.0, z)
|
||||||
|
TestStartStore.save_point(current_path, test_start_point)
|
||||||
|
test_start_tool.call("refresh_from_doc")
|
||||||
|
_sync_test_start_panel()
|
||||||
|
var ort := " (Höhe automatisch)" if hinweis.is_empty() else " (Höhe automatisch — %s)" % hinweis
|
||||||
|
_set_status("Test-Start: x=%.0f z=%.0f%s — F5 startet den Testlauf genau dort." % [x, z, ort])
|
||||||
|
|
||||||
|
|
||||||
|
## Test-Start löschen (Entf im Werkzeug oder Knopf rechts).
|
||||||
|
func _on_test_start_delete_pressed() -> void:
|
||||||
|
_test_start_delete()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_start_delete() -> void:
|
||||||
|
if test_start_point == null:
|
||||||
|
_set_status("Es ist kein Test-Startpunkt gesetzt — klicke zuerst mit dem Test-Start-Werkzeug (T) in die Karte.", true)
|
||||||
|
return
|
||||||
|
test_start_point = null
|
||||||
|
TestStartStore.clear_point(current_path)
|
||||||
|
test_start_tool.call("refresh_from_doc")
|
||||||
|
_sync_test_start_panel()
|
||||||
|
_set_status("Test-Start gelöscht — F5 startet wieder am ersten Team-Spawnpunkt (sonst Kartenmitte).")
|
||||||
|
|
||||||
|
|
||||||
|
## Nach Neu/Laden/Speichern (Pfadwechsel → anderer Sidecar-Schlüssel): den
|
||||||
|
## gespeicherten Punkt für DIESE Map wiederherstellen. Eine Map ohne Punkt
|
||||||
|
## verhält sich exakt wie bisher.
|
||||||
|
func _test_start_reload() -> void:
|
||||||
|
test_start_point = TestStartStore.load_point(current_path)
|
||||||
|
test_start_tool.call("refresh_from_doc")
|
||||||
|
_sync_test_start_panel()
|
||||||
|
|
||||||
|
|
||||||
func _selected_terrain_mode() -> int:
|
func _selected_terrain_mode() -> int:
|
||||||
if terrain_mode_lower.button_pressed:
|
if terrain_mode_lower.button_pressed:
|
||||||
return TerrainField.Mode.LOWER
|
return TerrainField.Mode.LOWER
|
||||||
|
|
|
||||||
1
tests/probe_m3_partner_render.gd.uid
Normal file
1
tests/probe_m3_partner_render.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dk6pg5j82wv4n
|
||||||
141
tests/probe_t1b_render.gd
Normal file
141
tests/probe_t1b_render.gd
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
extends SceneTree
|
||||||
|
## WEGWERF-Renderprobe (Karte t_96e3c4ee, Test-Startpunkt per Klick) —
|
||||||
|
## (a) Editor mit sichtbarem Marker am gesetzten Test-Start
|
||||||
|
## (b) F5 startet den Testlauf GENAU dort (HUD + Startposition)
|
||||||
|
## (c) T im Testlauf versetzt zum Blickpunkt (+ setzt den Test-Start neu)
|
||||||
|
## Ausführen:
|
||||||
|
## xvfb-run -a godot4 --path . --rendering-driver opengl3 -s res://tests/probe_t1b_render.gd
|
||||||
|
## Wegwerf-Probe (nicht im Repo-Commit), Beweise nach /opt/data/scopedvr/work/t96e3_beweise/.
|
||||||
|
|
||||||
|
var main: Control
|
||||||
|
var subvp: SubViewport
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
func _shot(name: String) -> void:
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
# ROOT-Viewport = das ganze Fenster (Editor-UI + 3D-Ansicht + HUD) —
|
||||||
|
# exakt das, was Sly auf dem Desktop sieht.
|
||||||
|
var img := root.get_texture().get_image()
|
||||||
|
img.save_png(_png(name))
|
||||||
|
print("PROBE_PNG=%s (%dx%d)" % [_png(name), img.get_width(), img.get_height()])
|
||||||
|
|
||||||
|
func _png(name: String) -> String:
|
||||||
|
return "/opt/data/scopedvr/work/t96e3_beweise/" + name
|
||||||
|
|
||||||
|
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/t96e3_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-Map: hügeliges Ramp-Terrain, V11-konforme Spawns, Objekte zum Ansehen.
|
||||||
|
var doc: Dictionary = SvmapDocument.create_new("T1b-Probe", 100.0, "mittag")
|
||||||
|
var terrain: Dictionary = doc.get("terrain", {})
|
||||||
|
var res := int(terrain.get("resolution", 0))
|
||||||
|
var heights := PackedFloat32Array()
|
||||||
|
heights.resize(res * res)
|
||||||
|
for zi in res:
|
||||||
|
for xi in res:
|
||||||
|
heights[zi * res + xi] = 2.0 + float(xi) * 0.06
|
||||||
|
terrain["min_height_m"] = 2.0
|
||||||
|
terrain["max_height_m"] = 2.0 + float(res - 1) * 0.06
|
||||||
|
terrain["heightmap_r16_b64"] = TerrainField.encode(heights, 2.0, 2.0 + float(res - 1) * 0.06)
|
||||||
|
doc["objects"] = [
|
||||||
|
{"id": "crate_wood", "pos": [26.0, 0.0, -30.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||||
|
{"id": "crate_wood", "pos": [14.0, 0.0, -24.0], "scale": 1.0, "rot_y_deg": 30.0},
|
||||||
|
{"id": "watchtower_wood", "pos": [-30.0, 0.0, 20.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||||
|
]
|
||||||
|
doc["spawn_zones"] = {
|
||||||
|
"0": {"points": [
|
||||||
|
{"id": "A1", "pos": [-20.0, 0.0, 0.0]}, {"id": "A2", "pos": [-24.0, 0.0, 2.0]},
|
||||||
|
{"id": "A3", "pos": [-22.0, 0.0, -3.0]}, {"id": "A4", "pos": [-26.0, 0.0, -2.0]},
|
||||||
|
]},
|
||||||
|
"1": {"points": [
|
||||||
|
{"id": "B1", "pos": [20.0, 0.0, 0.0]}, {"id": "B2", "pos": [24.0, 0.0, 2.0]},
|
||||||
|
{"id": "B3", "pos": [22.0, 0.0, -3.0]}, {"id": "B4", "pos": [26.0, 0.0, -2.0]},
|
||||||
|
]},
|
||||||
|
}
|
||||||
|
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
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
# --- (a) Test-Start setzen (Demo-Klick) + Editor-Ansicht auf den Marker ---
|
||||||
|
var tool: Node = main.get("test_start_tool")
|
||||||
|
tool.call("demo_set", 20.0, -30.0)
|
||||||
|
var marker: Node3D = tool.get("_marker")
|
||||||
|
var h: float = SvmapDocument.terrain_height_at(doc, 20.0, -30.0)
|
||||||
|
print("ZAHLEN (a) Marker: pos=", marker.position, " sichtbar=", marker.visible,
|
||||||
|
" | Terrainhöhe dort=", h)
|
||||||
|
print("ZAHLEN (a) Status: ", main.status_label.text)
|
||||||
|
main.call("_demo_camera", Vector3(20.0, h, -30.0) + Vector3(9.0, 7.0, 9.0), 16.0, 30.0, -45.0)
|
||||||
|
await _shot("t1b_a_editor_marker.png")
|
||||||
|
|
||||||
|
# --- (b) F5: Testlauf startet GENAU am Test-Start --------------------------
|
||||||
|
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 (b) Start: body.position=", body.position,
|
||||||
|
" | erwartet x=20 z=-30 Höhe=", body.call("terrain_height", 20.0, -30.0))
|
||||||
|
print("ZAHLEN (b) Marker sichtbar im Spiel=", marker.visible)
|
||||||
|
print("ZAHLEN (b) Status: ", main.status_label.text)
|
||||||
|
await _shot("t1b_b_test_start_dort.png")
|
||||||
|
|
||||||
|
# --- (c) T im Testlauf: Blick anheben → Teleport zum Blickpunkt ------------
|
||||||
|
body.set("yaw_deg", 20.0)
|
||||||
|
body.set("pitch_deg", -25.0)
|
||||||
|
body.call("_apply_look")
|
||||||
|
var eye: Vector3 = body.position + Vector3(0.0, 1.7, 0.0)
|
||||||
|
var cam: Camera3D = body.get("look_cam")
|
||||||
|
var dir: Vector3 = cam.global_transform.basis * Vector3(0.0, 0.0, -1.0)
|
||||||
|
print("ZAHLEN (c) vor T: pos=", body.position, " blick_dir=", dir)
|
||||||
|
main._unhandled_key_input(_press_key(KEY_T))
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
print("ZAHLEN (c) nach T: pos=", body.position,
|
||||||
|
" | main.test_start_point=", main.get("test_start_point"))
|
||||||
|
print("ZAHLEN (c) HUD: ", (pt.get("hud_msg") as Label).text)
|
||||||
|
await _shot("t1b_c_teleport.png")
|
||||||
|
|
||||||
|
# --- (d) ESC: zurück, Marker zeigt auf die neue Stelle ---------------------
|
||||||
|
main._unhandled_key_input(_press_key(KEY_ESCAPE))
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
print("ZAHLEN (d) playtest=null: ", main.get("playtest") == null,
|
||||||
|
" | Marker pos=", marker.position, " sichtbar=", marker.visible)
|
||||||
|
await _shot("t1b_d_editor_marker_neu.png")
|
||||||
|
print("PROBE_FERTIG")
|
||||||
|
quit(0)
|
||||||
1
tests/probe_t1b_render.gd.uid
Normal file
1
tests/probe_t1b_render.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://eg5vli2gshth
|
||||||
383
tests/test_playtest_spawn.gd
Normal file
383
tests/test_playtest_spawn.gd
Normal file
|
|
@ -0,0 +1,383 @@
|
||||||
|
extends SceneTree
|
||||||
|
## Editor-Test (headless): Karte t_96e3c4ee — Test-Startpunkt per Klick
|
||||||
|
## („hinklicken, wo man beim Testen spawnt", statt nur Team-Spawnpunkte).
|
||||||
|
## Ausführen: godot4 --headless --path . --script res://tests/test_playtest_spawn.gd
|
||||||
|
## Exit 0 = alles grün.
|
||||||
|
##
|
||||||
|
## Deckt die Karten-Pflichten ab:
|
||||||
|
## 1. Setzen/Löschen ändert die .svmap NICHT (Byte-Vergleich) + kein Undo.
|
||||||
|
## 2. Höhe = Terrainhöhe an der Stelle (Toleranz 1 cm).
|
||||||
|
## 3. Fallback-Reihenfolge je einmal belegt (Test-Start → Team-Spawn → Mitte).
|
||||||
|
## 4. Nach „Neustart" wiederhergestellt (Sidecar laden/leeren).
|
||||||
|
## 5. T im Testlauf versetzt genau dorthin, Augenhöhe 1,70 m, Kollision
|
||||||
|
## beachtet (blocked = ok=false, Position unverändert; Kisten-Dach = oben).
|
||||||
|
## 6. Marker existiert nur bei gesetztem Punkt.
|
||||||
|
## 7. Test-Start taucht in KEINER Spawn-Zone/Analyse auf.
|
||||||
|
|
||||||
|
var failures := 0
|
||||||
|
var checks := 0
|
||||||
|
var main: Control
|
||||||
|
var subvp: SubViewport
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run_all.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_all() -> void:
|
||||||
|
# Sauberer Sidecar-Start (Suite darf von früheren Läufen nichts erben).
|
||||||
|
TestStartStore.clear_point("")
|
||||||
|
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
|
||||||
|
main.call("_test_start_reload") # frischer Zustand nach dem Key-Clear
|
||||||
|
|
||||||
|
var container := _find_viewport_container(main)
|
||||||
|
if container != null:
|
||||||
|
subvp = container.get_child(0) as SubViewport
|
||||||
|
subvp.size = Vector2i(800, 600) # deterministisches Fadenkreuz
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
# Test-Map: hügeliges Terrain (Höhe steigt mit x), Team-0-Spawn, Objekt.
|
||||||
|
var doc: Dictionary = SvmapDocument.create_new("Test-Start-Karte", 100.0, "mittag")
|
||||||
|
var terrain: Dictionary = doc.get("terrain", {})
|
||||||
|
var res := int(terrain.get("resolution", 0))
|
||||||
|
var heights := PackedFloat32Array()
|
||||||
|
heights.resize(res * res)
|
||||||
|
for zi in res:
|
||||||
|
for xi in res:
|
||||||
|
heights[zi * res + xi] = 2.0 + float(xi) * 0.1 # 2.0 … +Ramp
|
||||||
|
terrain["min_height_m"] = 2.0
|
||||||
|
terrain["max_height_m"] = 2.0 + float(res - 1) * 0.1
|
||||||
|
terrain["heightmap_r16_b64"] = TerrainField.encode(heights, 2.0, 2.0 + float(res - 1) * 0.1)
|
||||||
|
doc["objects"] = [
|
||||||
|
{"id": "crate_wood", "pos": [10.0, 0.0, 0.0], "scale": 1.0, "rot_y_deg": 0.0},
|
||||||
|
]
|
||||||
|
# V11-konform: 4 Punkte pro Team (sonst lehnt der Validator das Speichern ab).
|
||||||
|
doc["spawn_zones"] = {
|
||||||
|
"0": {"points": [
|
||||||
|
{"id": "A1", "pos": [-20.0, 0.0, 0.0]}, {"id": "A2", "pos": [-24.0, 0.0, 2.0]},
|
||||||
|
{"id": "A3", "pos": [-22.0, 0.0, -3.0]}, {"id": "A4", "pos": [-26.0, 0.0, -2.0]},
|
||||||
|
]},
|
||||||
|
"1": {"points": [
|
||||||
|
{"id": "B1", "pos": [20.0, 0.0, 0.0]}, {"id": "B2", "pos": [24.0, 0.0, 2.0]},
|
||||||
|
{"id": "B3", "pos": [22.0, 0.0, -3.0]}, {"id": "B4", "pos": [26.0, 0.0, -2.0]},
|
||||||
|
]},
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
main.call("_test_start_reload")
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
_test_svmap_unchanged()
|
||||||
|
_test_height_snaps_to_terrain()
|
||||||
|
_test_fallback_order()
|
||||||
|
_test_sidecar_roundtrip()
|
||||||
|
_test_marker_visibility()
|
||||||
|
_test_t_key_toggles_tool()
|
||||||
|
_test_not_a_game_spawn()
|
||||||
|
await _test_playtest_teleport()
|
||||||
|
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
TestStartStore.clear_point("") # Suite hinterlässt keinen Müll
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
func _tool() -> Node:
|
||||||
|
return main.get("test_start_tool")
|
||||||
|
|
||||||
|
|
||||||
|
func _marker() -> Node3D:
|
||||||
|
var t := _tool()
|
||||||
|
if t == null:
|
||||||
|
return null
|
||||||
|
return t.get("_marker")
|
||||||
|
|
||||||
|
|
||||||
|
## --- 1: Setzen/Löschen ändert die .svmap nicht ------------------------------
|
||||||
|
func _test_svmap_unchanged() -> void:
|
||||||
|
print("Testgruppe: Setzen/Löschen ändert die .svmap NICHT (Byte-Vergleich)")
|
||||||
|
var path := "user://test_playtest_spawn_a.svmap"
|
||||||
|
var r1 := SvmapIO.save_to_file(path, main.get("doc"))
|
||||||
|
_check(bool(r1.get("ok")), "Map vor dem Setzen speicherbar (Validator ok)")
|
||||||
|
var bytes_before := FileAccess.get_file_as_bytes(path)
|
||||||
|
var doc_before := str(main.get("doc"))
|
||||||
|
var undo_before: bool = main.get("history").call("can_undo")
|
||||||
|
|
||||||
|
_tool().call("demo_set", 20.0, -30.0)
|
||||||
|
_check(main.get("test_start_point") != null, "Test-Start gesetzt (main.test_start_point)")
|
||||||
|
var r2 := SvmapIO.save_to_file(path, main.get("doc"))
|
||||||
|
_check(bool(r2.get("ok")), "Map nach dem Setzen speicherbar")
|
||||||
|
var bytes_after_set := FileAccess.get_file_as_bytes(path)
|
||||||
|
_check(bytes_after_set == bytes_before, "Setzen: .svmap byte-identisch")
|
||||||
|
_check(str(main.get("doc")) == doc_before, "Setzen: Dokument unverändert")
|
||||||
|
_check(not main.get("history").call("can_undo"), "Setzen: KEIN Undo-Eintrag")
|
||||||
|
|
||||||
|
main.call("_test_start_delete")
|
||||||
|
_check(main.get("test_start_point") == null, "Test-Start gelöscht")
|
||||||
|
var bytes_after_del := FileAccess.get_file_as_bytes(path)
|
||||||
|
_check(bytes_after_del == bytes_before, "Löschen: .svmap byte-identisch")
|
||||||
|
_check(str(main.get("doc")) == doc_before, "Löschen: Dokument unverändert")
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||||
|
|
||||||
|
|
||||||
|
## --- 2: Höhe = Terrainhöhe an der Stelle (Toleranz 1 cm) --------------------
|
||||||
|
func _test_height_snaps_to_terrain() -> void:
|
||||||
|
print("Testgruppe: Höhe = Terrainhöhe an der Stelle (Toleranz 1 cm)")
|
||||||
|
var doc: Dictionary = main.get("doc")
|
||||||
|
_tool().call("demo_set", 30.0, 5.0)
|
||||||
|
var expected := SvmapDocument.terrain_height_at(doc, 30.0, 5.0)
|
||||||
|
var marker := _marker()
|
||||||
|
_check(marker != null and marker.visible, "Marker sichtbar nach dem Setzen")
|
||||||
|
_check(absf(marker.position.y - expected) <= 0.01,
|
||||||
|
"Marker-Höhe == Terrainhöhe (%.3f == %.3f, Δ %.4f m)" % [
|
||||||
|
marker.position.y, expected, absf(marker.position.y - expected)])
|
||||||
|
_check(absf(marker.position.x - 30.0) < 0.001 and absf(marker.position.z - 5.0) < 0.001,
|
||||||
|
"Marker x/z exakt am Klickpunkt")
|
||||||
|
# Klick auf ein Objekt: Punkt auf dem Boden DARUNTER (gleiche Höhe-Regel).
|
||||||
|
_tool().call("demo_set", 10.0, 0.0, "auf dem Boden unter dem Objekt")
|
||||||
|
var expected_obj := SvmapDocument.terrain_height_at(doc, 10.0, 0.0)
|
||||||
|
_check(absf(marker.position.y - expected_obj) <= 0.01,
|
||||||
|
"Objekt-Treffer: Marker auf dem Boden DARUNTER (%.3f == %.3f)" % [
|
||||||
|
marker.position.y, expected_obj])
|
||||||
|
var status: String = main.status_label.text
|
||||||
|
_check(status.contains("Boden darunter") or status.contains("unter dem Objekt"),
|
||||||
|
"Statuszeile meldet Klartext „unter dem Objekt“: '%s'" % status)
|
||||||
|
main.call("_test_start_delete")
|
||||||
|
|
||||||
|
|
||||||
|
## --- 3: Fallback-Reihenfolge -------------------------------------------------
|
||||||
|
func _test_fallback_order() -> void:
|
||||||
|
print("Testgruppe: Fallback-Reihenfolge (Test-Start → Team-Spawn → Mitte)")
|
||||||
|
var doc: Dictionary = main.get("doc")
|
||||||
|
# a) Test-Start gewinnt.
|
||||||
|
var r: Dictionary = TestStart.resolve_start(doc, Vector3(33.0, 0.0, -44.0))
|
||||||
|
_check(str(r.get("quelle")) == "test", "Fallback 1: gesetzter Test-Start gewinnt")
|
||||||
|
_check(absf((r.get("pos") as Vector3).x - 33.0) < 0.001
|
||||||
|
and absf((r.get("pos") as Vector3).z + 44.0) < 0.001,
|
||||||
|
"Fallback 1: Position == Test-Start (x/z)")
|
||||||
|
_check(absf((r.get("pos") as Vector3).y - SvmapDocument.terrain_height_at(doc, 33.0, -44.0)) <= 0.01,
|
||||||
|
"Fallback 1: Höhe automatisch vom Terrain")
|
||||||
|
# b) Kein Test-Start → erster Team-Spawnpunkt (Team Blau = „0“).
|
||||||
|
r = TestStart.resolve_start(doc, null)
|
||||||
|
_check(str(r.get("quelle")) == "team", "Fallback 2: erster Team-Spawnpunkt")
|
||||||
|
_check(absf((r.get("pos") as Vector3).x + 20.0) < 0.001,
|
||||||
|
"Fallback 2: Position == Team-Blau-Spawn A1 (-20, 0)")
|
||||||
|
# c) Weder noch → Kartenmitte.
|
||||||
|
var ohne: Dictionary = doc.duplicate(true)
|
||||||
|
ohne["spawn_zones"] = {}
|
||||||
|
r = TestStart.resolve_start(ohne, null)
|
||||||
|
_check(str(r.get("quelle")) == "mitte", "Fallback 3: Kartenmitte")
|
||||||
|
var bmin: Array = doc.get("bounds", {}).get("min", [])
|
||||||
|
var bmax: Array = doc.get("bounds", {}).get("max", [])
|
||||||
|
_check(absf((r.get("pos") as Vector3).x - (float(bmin[0]) + float(bmax[0])) * 0.5) < 0.001,
|
||||||
|
"Fallback 3: Mitte == Bounds-Mitte")
|
||||||
|
# Klartext-Hinweise je Stufe.
|
||||||
|
_check(str(r.get("status")).length() > 0 and str(TestStart.resolve_start(doc, Vector3.ZERO)["status"]).length() > 0,
|
||||||
|
"Jede Fallback-Stufe liefert einen Klartext-Hinweis")
|
||||||
|
|
||||||
|
|
||||||
|
## --- 4: Nach Neustart wiederhergestellt (Sidecar) ----------------------------
|
||||||
|
func _test_sidecar_roundtrip() -> void:
|
||||||
|
print("Testgruppe: Sidecar — nach Neustart wiederhergestellt")
|
||||||
|
var pfad := "/tmp/scopedvr_test_karte.svmap"
|
||||||
|
TestStartStore.save_point(pfad, Vector3(12.5, 0.0, -7.25))
|
||||||
|
var loaded: Variant = TestStartStore.load_point(pfad)
|
||||||
|
_check(loaded != null, "Sidecar: Punkt gespeichert + lesbar")
|
||||||
|
if loaded != null:
|
||||||
|
var v := loaded as Vector3
|
||||||
|
_check(absf(v.x - 12.5) < 0.001 and absf(v.z + 7.25) < 0.001,
|
||||||
|
"Sidecar: x/z exakt (%.2f, %.2f)" % [v.x, v.z])
|
||||||
|
# editor-Flow: setzen → main verliert den Zustand („Neustart") → reload holt ihn zurück.
|
||||||
|
_tool().call("demo_set", 3.0, 4.0)
|
||||||
|
main.set("test_start_point", null)
|
||||||
|
main.call("_test_start_reload")
|
||||||
|
var p: Variant = main.get("test_start_point")
|
||||||
|
_check(p != null, "Neustart-Simulation: Punkt aus dem Sidecar wieder da")
|
||||||
|
if p != null:
|
||||||
|
var pv := p as Vector3
|
||||||
|
_check(absf(pv.x - 3.0) < 0.001 and absf(pv.z - 4.0) < 0.001,
|
||||||
|
"Neustart-Simulation: gleiche Position (%.1f, %.1f)" % [pv.x, pv.z])
|
||||||
|
main.call("_test_start_delete")
|
||||||
|
_check(TestStartStore.load_point("") == null, "Löschen entfernt den Sidecar-Eintrag")
|
||||||
|
|
||||||
|
|
||||||
|
## --- 6: Marker existiert nur bei gesetztem Punkt ------------------------------
|
||||||
|
func _test_marker_visibility() -> void:
|
||||||
|
print("Testgruppe: Marker existiert nur bei gesetztem Punkt")
|
||||||
|
var marker := _marker()
|
||||||
|
_check(marker != null and not marker.visible, "Ohne Punkt: Marker unsichtbar")
|
||||||
|
_tool().call("demo_set", -5.0, 8.0)
|
||||||
|
_check(marker.visible, "Mit Punkt: Marker sichtbar")
|
||||||
|
main.call("_test_start_delete")
|
||||||
|
_check(not marker.visible, "Nach Löschen: Marker wieder unsichtbar")
|
||||||
|
|
||||||
|
|
||||||
|
## --- 6b: Taste T schaltet das Werkzeug im Editor um ---------------------------
|
||||||
|
func _test_t_key_toggles_tool() -> void:
|
||||||
|
print("Testgruppe: Taste T schaltet das Test-Start-Werkzeug um")
|
||||||
|
_check(str(main.get("tool_mode")) == "objects", "Start im Objekt-Werkzeug (Vorbedingung)")
|
||||||
|
main._unhandled_key_input(_press_key(KEY_T))
|
||||||
|
_check(str(main.get("tool_mode")) == "teststart", "T: Test-Start-Werkzeug an")
|
||||||
|
var btn: Button = main.get("test_start_btn")
|
||||||
|
_check(btn != null and btn.button_pressed, "Toolbar-Knopf leuchtet mit")
|
||||||
|
var panel: Control = main.get("test_start_panel")
|
||||||
|
_check(panel != null and panel.visible, "Panel (Info + Löschen) sichtbar")
|
||||||
|
main._unhandled_key_input(_press_key(KEY_T))
|
||||||
|
_check(str(main.get("tool_mode")) == "objects", "T nochmal: zurück zum Objekt-Werkzeug")
|
||||||
|
_check(not panel.visible, "Panel wieder zu")
|
||||||
|
# ESC im Werkzeug beendet es, der Punkt bleibt.
|
||||||
|
main._unhandled_key_input(_press_key(KEY_T))
|
||||||
|
_tool().call("demo_set", 1.0, 2.0)
|
||||||
|
main._unhandled_key_input(_press_key(KEY_ESCAPE))
|
||||||
|
_check(str(main.get("tool_mode")) == "objects", "ESC: Werkzeug beendet")
|
||||||
|
_check(main.get("test_start_point") != null, "ESC: gesetzter Test-Start bleibt")
|
||||||
|
main.call("_test_start_delete")
|
||||||
|
|
||||||
|
|
||||||
|
## --- 7: Test-Start taucht in keiner Spawn-Zone/Analyse auf --------------------
|
||||||
|
func _test_not_a_game_spawn() -> void:
|
||||||
|
print("Testgruppe: Kein Spiel-Spawnpunkt (Spawn-Zonen/Analyse unberührt)")
|
||||||
|
var doc: Dictionary = main.get("doc")
|
||||||
|
var zones_before := str(doc.get("spawn_zones", {}))
|
||||||
|
var check_before := SpawnOps.ready_report(doc, main.get("manifest"))
|
||||||
|
_tool().call("demo_set", 44.0, 44.0)
|
||||||
|
_check(str(doc.get("spawn_zones", {})) == zones_before,
|
||||||
|
"Setzen: spawn_zones im Dokument byte-identisch")
|
||||||
|
var check_after := SpawnOps.ready_report(doc, main.get("manifest"))
|
||||||
|
_check(str(check_after) == str(check_before), "Setzen: Map-Check-Ergebnis unverändert")
|
||||||
|
var live_before := SpawnOps.live_status(doc, main.get("manifest"))
|
||||||
|
_tool().call("demo_set", -44.0, 33.0)
|
||||||
|
_check(SpawnOps.live_status(doc, main.get("manifest")) == live_before,
|
||||||
|
"Weiteres Setzen: Live-Spawn-Status unverändert")
|
||||||
|
main.call("_test_start_delete")
|
||||||
|
_check(str(doc.get("spawn_zones", {})) == zones_before,
|
||||||
|
"Löschen: spawn_zones im Dokument byte-identisch")
|
||||||
|
|
||||||
|
|
||||||
|
## --- 5: T im Testlauf versetzt genau dorthin (Augenhöhe, Kollision) ----------
|
||||||
|
func _test_playtest_teleport() -> void:
|
||||||
|
print("Testgruppe: T im Testlauf — versetzt genau dorthin, Augenhöhe, Kollision")
|
||||||
|
main.set("test_start_point", null)
|
||||||
|
main.call("_test_start_reload")
|
||||||
|
main.call("_on_test_run")
|
||||||
|
await process_frame
|
||||||
|
var pt: Node = main.get("playtest")
|
||||||
|
_check(pt != null and bool(pt.get("active")), "Testlauf läuft (Vorbedingung)")
|
||||||
|
if pt == null:
|
||||||
|
return
|
||||||
|
var body: Node3D = pt.get("body")
|
||||||
|
var doc: Dictionary = main.get("doc")
|
||||||
|
|
||||||
|
# a) Fadenkreuz-Treffer: Blick 45° nach vorn-unten. Das Ramp-Terrain hängt
|
||||||
|
# nur von x ab — der Strahl (yaw 0) bleibt auf konstantem x, also ist
|
||||||
|
# die Ziel-Höhe analytisch exakt.
|
||||||
|
body.set("yaw_deg", 0.0) # Blick nach -Z
|
||||||
|
body.set("pitch_deg", -45.0)
|
||||||
|
body.call("_apply_look") # yaw/pitch erst durch _apply_look auf die Kamera
|
||||||
|
var eye: Vector3 = body.position + Vector3(0.0, float(body.get("eye_height")), 0.0)
|
||||||
|
var cam: Camera3D = body.get("look_cam")
|
||||||
|
var dir: Vector3 = cam.global_transform.basis * Vector3(0.0, 0.0, -1.0)
|
||||||
|
_check(absf(dir.y + 0.7071) < 0.01, "Kamera-Blick zeigt 45° nach unten (dir.y ≈ -0.707)")
|
||||||
|
var h0: float = body.call("terrain_height", eye.x, eye.z)
|
||||||
|
var th := absf((eye.y - h0) / dir.y)
|
||||||
|
var expected := eye + dir * th
|
||||||
|
main._unhandled_key_input(_press_key(KEY_T)) # ECHTER T-Druck (main-Verdrahtung)
|
||||||
|
await process_frame
|
||||||
|
var np := body.position
|
||||||
|
_check(absf(np.x - expected.x) < 0.15 and absf(np.z - expected.z) < 0.15,
|
||||||
|
"Teleport-Ziel == Fadenkreuz-Punkt (%.2f,%.2f ≈ %.2f,%.2f)" % [
|
||||||
|
np.x, np.z, expected.x, expected.z])
|
||||||
|
var ground_here: float = body.call("terrain_height", np.x, np.z)
|
||||||
|
_check(absf(np.y - ground_here) <= 0.01,
|
||||||
|
"Fusspunkt auf Terrainhöhe (Δ %.4f m)" % absf(np.y - ground_here))
|
||||||
|
_check(absf(float(cam.position.y) - 1.7) < 0.001, "Augenhöhe 1,70 m (look_cam-Offset)")
|
||||||
|
var stored: Variant = main.get("test_start_point")
|
||||||
|
_check(stored != null and absf((stored as Vector3).x - np.x) < 0.01
|
||||||
|
and absf((stored as Vector3).z - np.z) < 0.01,
|
||||||
|
"T hat den Test-Start AUF den Fadenkreuz-Punkt gesetzt")
|
||||||
|
var msg: String = (pt.get("hud_msg") as Label).text
|
||||||
|
_check(msg.contains("Test-Start"), "HUD meldet Klartext: '%s'" % msg)
|
||||||
|
|
||||||
|
# b) Kollision: hohe Wand vor dem Spieler, flacher Blick (−10°) zielt HINTER
|
||||||
|
# die Wand ins Terrain — der Wand-AABB-Treffer kommt zuerst → ABGELEHNT.
|
||||||
|
# (Wand ragt weit über die Augenhöhe am Ramp-Terrain: Augen ≈ 7,5 m dort.)
|
||||||
|
var vor := Vector3(body.position)
|
||||||
|
body.obstacles.append(AABB(Vector3(vor.x - 2.0, -5.0, vor.z - 4.0), Vector3(4.0, 20.0, 0.6)))
|
||||||
|
body.set("yaw_deg", 0.0)
|
||||||
|
body.set("pitch_deg", -10.0)
|
||||||
|
body.call("_apply_look")
|
||||||
|
main._unhandled_key_input(_press_key(KEY_T))
|
||||||
|
await process_frame
|
||||||
|
_check(body.position.distance_to(vor) < 0.001, "Im Objekt: Teleport abgelehnt, Position unverändert")
|
||||||
|
var blocked_msg: String = (pt.get("hud_msg") as Label).text
|
||||||
|
_check(blocked_msg.contains("nicht landen") or blocked_msg.contains("Weg"),
|
||||||
|
"Abgelehnt: Klartext im HUD: '%s'" % blocked_msg)
|
||||||
|
body.obstacles.pop_back()
|
||||||
|
|
||||||
|
# c) Kisten-Dach: begehbare Block-Decke → oben drauf landen (Boden DARONTER,
|
||||||
|
# nie im Objekt). Strahl von oben senkrecht auf die Kiste.
|
||||||
|
var crate := AABB(Vector3(np.x + 5.0, np.y, np.z), Vector3(1.0, 1.0, 1.0))
|
||||||
|
body.obstacles.append(crate)
|
||||||
|
var ccenter := crate.get_center()
|
||||||
|
var roof: Dictionary = body.call("teleport_to_ground",
|
||||||
|
Vector3(ccenter.x, crate.end.y + 2.0, ccenter.z), Vector3(0.0, -1.0, 0.0))
|
||||||
|
_check(bool(roof.get("ok")) and absf(float(roof.get("ground")) - crate.end.y) < 0.01,
|
||||||
|
"Kisten-Dach: auf der Decke gelandet (ground %.2f == %.2f)" % [
|
||||||
|
float(roof.get("ground")), crate.end.y])
|
||||||
|
body.obstacles.pop_back()
|
||||||
|
|
||||||
|
# d) ESC: zurück, Editor-Zustand intakt (Punkt bleibt gesetzt).
|
||||||
|
var doc_before := str(doc)
|
||||||
|
pt.call("handle_escape")
|
||||||
|
await process_frame
|
||||||
|
_check(main.get("playtest") == null, "ESC: zurück im Editor")
|
||||||
|
_check(str(main.get("doc")) == doc_before, "ESC: Dokument byte-identisch")
|
||||||
|
_check(main.get("test_start_point") != null, "ESC: gesetzter Test-Start bleibt")
|
||||||
|
main.call("_test_start_delete")
|
||||||
|
|
||||||
|
|
||||||
|
func _press_key(code: Key) -> InputEventKey:
|
||||||
|
var key := InputEventKey.new()
|
||||||
|
key.keycode = code
|
||||||
|
key.physical_keycode = code
|
||||||
|
key.pressed = true
|
||||||
|
return key
|
||||||
1
tests/test_playtest_spawn.gd.uid
Normal file
1
tests/test_playtest_spawn.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c3eq6pgogm8ns
|
||||||
Loading…
Reference in a new issue