T1b-Wiederanbindung (Operator): Test-Startpunkt zurueck nach dem U1-Umbau

Sly-Befund: der Knopf 'Test-Start' fehlte; tests/test_playtest_spawn.gd brach
mit "Nonexistent function '_test_start_reload'" ab und hing.

- Werkzeug-Modus 'teststart' wieder in _set_tool_mode (+ enter/leave/sync)
- ShortcutPolicy.esc_action: exit_teststart inkl. Test
- Taste T: Editor = Werkzeug umschalten, Lauf-Modus = Punkt setzen + Teleport
  aufs Fadenkreuz (playtest.teleport_crosshair)
- Entf im Werkzeug loescht den Punkt; Klartext-Hinweis des Werkzeugs wieder in
  der Statuszeile ('auf dem Boden unter dem Objekt')
- Leisten-Knopf '🎯 Test-Start' als Umschalter (registry.gd ACTIONS)
- neues Panel-Modul src/ui/panels/test_start_panel.gd + Registry-Eintrag
- Haken _test_start_apply/_delete/_reload + test_start_point/test_start_panel/
  test_start_btn stehen fest auf main (Vertrag der Pruefsuite)
- _test_start_reload() wieder nach Neu/Laden/Speichern; playtest.enter() bekommt
  den aufgeloesten Startpunkt (TestStart.resolve_start)

Belegt: test_playtest_spawn 56/56, test_shortcuts 204, gui_smoke 92, ui_layout 71,
Import 0 Fehler.
This commit is contained in:
scopedvr-operator 2026-10-20 09:37:05 +00:00 committed by Hermes (scopedvr-dev)
parent 40449932b9
commit 0f86472cfa
8 changed files with 220 additions and 6 deletions

View file

@ -9,7 +9,7 @@ class_name ShortcutPolicy
## 2. Ghost-Preview (Platzieren) abbrechen
## 3. Auswahl aufheben
## Rückgabe-Werte:
## "exit_terrain" | "exit_spawns" | "exit_bauen" | "cancel_grab"
## "exit_terrain" | "exit_spawns" | "exit_bauen" | "exit_teststart" | "cancel_grab"
## "cancel_placement" | "clear_selection" | "none"
## grabbing (Karte t_55257792): G-Greifen läuft — Esc rollt die Positionen
## zurück (Blender-Semantik). Vorrang NACH Werkzeug-Exit (im Terrain-/Bauen-
@ -22,6 +22,10 @@ static func esc_action(tool_mode: String, placing: bool, has_selection: bool,
return "exit_spawns"
if tool_mode == "bauen":
return "exit_bauen"
if tool_mode == "teststart":
# Karte t_96e3c4ee (Wiederanbindung 20.10.): Esc verlässt das
# Test-Start-Werkzeug — der gesetzte Punkt BLEIBT bestehen.
return "exit_teststart"
if grabbing:
return "cancel_grab"
if placing:

View file

@ -123,6 +123,15 @@ var _grid_on := false # Nutzer-Wahl für das Maßstabsgitter
# Terrain-Werkzeug (F1) — Sly-Befund 17.10.: dauerhaft sichtbar störte.
# Effektiv = _contours_fix AN ODER tool_mode == "terrain".
var _contours_on := false
# --- Test-Start (Karte t_96e3c4ee / T1b) -------------------------------------
# main hält den ZUSTAND und die Haken (Vertrag mit tests/test_playtest_spawn.gd);
# das Panel-Modul test_start_panel.gd zeigt ihn nur an (U1-Muster).
var test_start_point: Variant = null # Vector3 (nur x/z zählt) oder null
var test_start_tool # src/editor/playtest/test_start_tool.gd
var test_start_panel: Control # src/ui/panels/test_start_panel.gd (von der Prüfsuite erwartet)
var test_start_btn: Button # „🎯 Test-Start" in der Leiste (Umschalter)
var _test_start_active := false
var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder
# Rahmen-Ausziehen (Rubberband) — Overlay über dem 3D-Viewport
@ -567,6 +576,14 @@ func _build_center_viewport() -> SubViewportContainer:
world.add_child(build_tool)
build_tool.setup(self, camera, view_builder, interaction)
# Test-Start-Werkzeug (T1b, Wiederanbindung 20.10.): Marker in der Karte,
# Klick setzt den Startpunkt. Lebt — wie die übrigen Werkzeuge — in main,
# gelesen wird es von test_start_tool.gd über main.test_start_point.
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)
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"
@ -1055,14 +1072,17 @@ func _start_playtest() -> void:
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.")
# Karte t_96e3c4ee: Fallback-Reihenfolge mit Klartext-Hinweis — gesetzter
# 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 — ESC zurück zum Editor. (%s)" % str(resolved["status"]))
## 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]:
for t in [camera, interaction, terrain_tool, spawn_tool, build_tool, test_start_tool]:
if t == null:
continue
t.set_process_unhandled_input(not on)
@ -1097,6 +1117,7 @@ func _on_new_map_confirmed() -> void:
history.begin(doc)
_clear_edit_state()
_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!" % [
new_dialog.get_map_name(), new_dialog.get_size_m(), new_dialog.get_size_m(), new_dialog.get_weather()])
@ -1113,6 +1134,7 @@ func _on_open_selected(path: String) -> void:
history.begin(doc)
_clear_edit_state()
_refresh_all()
_test_start_reload() # Karte t_96e3c4ee: Sidecar der geladenen Map lesen
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)])
@ -1150,6 +1172,7 @@ func _save_to(path: String) -> void:
return
current_path = path
dirty = false
_test_start_reload() # Karte t_96e3c4ee: Punkt wandert beim Speichern mit
_refresh_file_label()
_set_status("Map gespeichert: %s" % path)
_refresh_tree()
@ -1306,8 +1329,21 @@ func _unhandled_key_input(event: InputEvent) -> void:
if key == KEY_ESCAPE:
playtest.handle_escape()
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).
var tp: Dictionary = playtest.call("teleport_crosshair")
if bool(tp.get("ok")):
_test_start_apply(float(tp.get("x")), float(tp.get("z")), str(tp.get("hinweis")))
get_viewport().set_input_as_handled()
return
match key:
KEY_T:
# Karte t_96e3c4ee: T schaltet das Test-Start-Werkzeug um (Editor).
if not ctrl:
_set_tool_mode("teststart" if tool_mode != "teststart" else "objects")
get_viewport().set_input_as_handled()
KEY_Z:
if ctrl:
if event.shift_pressed:
@ -1437,6 +1473,10 @@ func _unhandled_key_input(event: InputEvent) -> void:
"exit_bauen":
_set_tool_mode("objects")
_set_status("Bauen-Modus beendet — zurück zum Objekt-Werkzeug.")
"exit_teststart":
# Der gesetzte Test-Start bleibt bestehen (nur Werkzeug aus).
_set_tool_mode("objects")
_set_status("Test-Start-Werkzeug beendet — zurück zum Objekt-Werkzeug.")
"cancel_grab":
interaction.grab_cancel()
"cancel_placement":
@ -1455,6 +1495,9 @@ func _unhandled_key_input(event: InputEvent) -> void:
spawn_tool.call("delete_selected")
elif tool_mode == "bauen":
build_tool.call("delete_selected")
elif tool_mode == "teststart":
# Karte t_96e3c4ee: Entf im Test-Start-Werkzeug = Punkt löschen.
_test_start_delete()
else:
interaction.delete_selected()
get_viewport().set_input_as_handled()
@ -1615,24 +1658,34 @@ func _set_tool_mode(mode: String) -> void:
_sync_terrain_button()
_sync_spawn_button()
_sync_build_button()
_sync_test_start_button()
_update_tool_label()
return
if mode == "terrain":
_leave_spawn_mode()
_leave_build_mode()
_leave_test_start_mode()
_enter_terrain_mode()
elif mode == "spawns":
_leave_terrain_mode()
_leave_build_mode()
_leave_test_start_mode()
_enter_spawn_mode()
elif mode == "bauen":
_leave_terrain_mode()
_leave_spawn_mode()
_leave_test_start_mode()
_enter_build_mode()
elif mode == "teststart":
_leave_terrain_mode()
_leave_spawn_mode()
_leave_build_mode()
_enter_test_start_mode()
else:
_leave_terrain_mode()
_leave_spawn_mode()
_leave_build_mode()
_leave_test_start_mode()
_enter_objects_mode()
_update_tool_label()
# Karte t_89c0ec95: Werkzeugwechsel = Höhenlinien-Umschalter — nach dem
@ -2477,4 +2530,83 @@ func _start_bauen_demo(args: PackedStringArray) -> void:
print("BAUEN_DEMO_OK: 3 Screenshots in %s (%d Bauteile, Map OK)" % [
out_dir, BuildOps.piece_count(doc)])
if args.has("--bauen-demo-quit"):
get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind
get_tree().quit() # Batch-Smoke (xvfb): automatisch beenden, sobald die Shots da sind
## --- Test-Start: Haken (T1b-Vertrag, Operator-Wiederanbindung 20.10.) --------
## Diese drei Namen erwartet tests/test_playtest_spawn.gd (56 checks) auf main.
## Sie waren beim U1-Umbau verschwunden (Suite brach mit „Nonexistent function
## '_test_start_reload'" ab und hing) — deshalb stehen sie hier bewusst fest.
## Der sichtbare Teil (Info + Löschen) lebt in src/ui/panels/test_start_panel.gd,
## der Zustand hier — genau wie vor dem Umbau.
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)
if test_start_tool != null:
test_start_tool.call("refresh_from_doc")
_refresh_test_start_panel()
# Klartext-Hinweis des Werkzeugs (z.B. „auf dem Boden unter dem Objekt")
# mitnehmen — die Prüfsuite liest ihn aus der Statuszeile.
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])
func _test_start_delete() -> void:
if test_start_point == null:
return
test_start_point = null
TestStartStore.clear_point(current_path)
if test_start_tool != null:
test_start_tool.call("refresh_from_doc")
_refresh_test_start_panel()
func _test_start_reload() -> void:
test_start_point = TestStartStore.load_point(current_path)
if test_start_tool != null:
test_start_tool.call("refresh_from_doc")
_refresh_test_start_panel()
func _refresh_test_start_panel() -> void:
if test_start_panel != null and is_instance_valid(test_start_panel):
test_start_panel.call("refresh_point", test_start_point)
## --- Test-Start: Werkzeug-Modus (wie Terrain/Spawns/Bauen) --------------------
## Die Prüfsuite erwartet tool_mode == "teststart", einen leuchtenden Leisten-
## Knopf (`test_start_btn`) und das erst dann sichtbare Panel (`test_start_panel`).
func _enter_test_start_mode() -> void:
interaction.call("cancel_placement")
tool_mode = "teststart"
if test_start_tool != null:
test_start_tool.call("activate")
_sync_test_start_button()
_sync_test_start_panel_visibility()
func _leave_test_start_mode() -> void:
if test_start_tool != null:
test_start_tool.call("deactivate")
# Direkt aus-/einschalten statt über tool_mode: _set_tool_mode ruft die
# _leave_*-Funktionen BEVOR der Zielmodus gesetzt ist — ein Abgleich gegen
# tool_mode ließe das Panel offen (Befund aus test_playtest_spawn).
if test_start_panel != null:
test_start_panel.visible = false
if test_start_btn != null:
test_start_btn.button_pressed = false
func _sync_test_start_button() -> void:
if test_start_btn != null:
test_start_btn.button_pressed = (tool_mode == "teststart")
func _sync_test_start_panel_visibility() -> void:
if test_start_panel != null:
test_start_panel.visible = (tool_mode == "teststart")
## Leisten-Knopf „🎯 Test-Start" (registry.gd ACTIONS) — schaltet das Werkzeug um.
func _on_test_start_tool_pressed() -> void:
_set_tool_mode("objects" if tool_mode == "teststart" else "teststart")

View file

@ -36,6 +36,8 @@ const PANELS: Array = [
"skript": "res://src/ui/panels/terrain_panel.gd"},
{"id": "check", "titel": "Prüfen", "gruppe": "rechts", "order": 50,
"skript": "res://src/ui/panels/check_panel.gd"},
{"id": "teststart", "titel": "Test-Start", "gruppe": "rechts", "order": 60,
"skript": "res://src/ui/panels/test_start_panel.gd"},
# --- links: Vorbereitet für U2 (Szenen-Baum); Bibliothek ist UNTEN ---
{"id": "szene", "titel": "Map-Inhalt", "gruppe": "links", "order": 10,
"skript": "res://src/ui/panels/scene_tree_panel.gd"},
@ -56,6 +58,10 @@ const ACTIONS: Array = [
"order": 10, "action": "_on_check_map"},
{"id": "karte_testen", "titel": "▶ Karte testen", "gruppe": "toolbar",
"order": 20, "action": "_on_test_run"},
# Wiederanbindung 20.10. (T1b, Karte t_96e3c4ee): der Test-Start-Knopf war
# beim U1-Umbau aus der Leiste verschwunden.
{"id": "teststart", "titel": "🎯 Test-Start", "gruppe": "toolbar",
"order": 30, "action": "_on_test_start_tool_pressed"},
]

View file

@ -0,0 +1,63 @@
extends "res://src/ui/panels/editor_panel.gd"
## Test-Start-Bereich (rechte Spalte — Karte t_96e3c4ee / T1b, Wiederanbindung
## durch den Operator am 20.10.).
##
## Aufteilung: der ZUSTAND lebt in main.gd (`test_start_point`) samt den Haken
## `_test_start_apply` / `_test_start_delete` / `_test_start_reload`, weil die
## Prüfsuite tests/test_playtest_spawn.gd (56 checks) genau diese drei Namen auf
## main erwartet — das ist der T1b-Vertrag. Beim U1-Umbau waren sie verschwunden,
## die Suite brach mit „Nonexistent function '_test_start_reload'" ab.
## Dieses Modul zeigt den Zustand nur an (Info-Text + Löschen-Knopf) und meldet
## sich datengetrieben über registry.gd an — main.gd verdrahtet nichts fest.
var info: Label
var delete_btn: Button
var info_hint: Label
static func spec() -> Dictionary:
return {"titel": "Test-Start", "gruppe": "rechts", "order": 60}
func panel_title() -> String:
return "Test-Start (F5)"
func build() -> void:
add_child(_header_label("Startpunkt für „▶ Karte testen“"))
info = Label.new()
info.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(info)
delete_btn = Button.new()
delete_btn.text = "🗑 Test-Start löschen"
delete_btn.custom_minimum_size = UIScale.v2(0, 40)
delete_btn.tooltip_text = "Test-Startpunkt entfernen — F5 startet dann wieder am ersten Team-Spawnpunkt."
delete_btn.pressed.connect(func() -> void: main.call("_test_start_delete"))
add_child(delete_btn)
add_child(_separator())
info_hint = Label.new()
info_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
info_hint.text = "In der Leiste „🎯 Test-Start“ drücken (oder Taste T), dann in die Karte klicken. Die Höhe kommt automatisch vom Terrain. Der Punkt liegt in einer Nebendatei zur .svmap — die Kartendatei selbst bleibt unverändert."
add_child(info_hint)
# main holt sich das Modul hier ab (main._refresh_test_start_panel) — die
# Prüfsuite liest es als main.test_start_panel. Sichtbar erst, wenn das
# Werkzeug aktiv ist (wie vor dem U1-Umbau).
main.set("test_start_panel", self)
visible = false
refresh_point(main.get("test_start_point"))
## Wird von main._refresh_test_start_panel() gerufen. point = Vector3 oder null.
func refresh_point(point: Variant) -> void:
if info == null or delete_btn == null:
return
if point == null:
info.text = "Kein Test-Start gesetzt.\nF5 startet dann am ersten Team-Blau-Spawnpunkt (ohne Spawnpunkte: Kartenmitte)."
delete_btn.disabled = true
else:
var p: Vector3 = point
info.text = "Test-Start: x=%.0f z=%.0f\n(Höhe kommt automatisch vom Terrain)" % [p.x, p.z]
delete_btn.disabled = false

View file

@ -0,0 +1 @@
uid://brik7mu7wv0el

View file

@ -131,6 +131,11 @@ func build() -> void:
btn.custom_minimum_size = UIScale.v2(190, 40)
btn.tooltip_text = "Startet die Map SOFORT als Spiel zum Durchlaufen — eigenes Fenster, der Editor bleibt offen. ESC im Spiel = zurück zum Editor."
btn.pressed.connect(main.call.bind(str(entry["action"])))
# Test-Start ist ein Umschalter: main liest/setzt seinen Zustand
# (Prüfsuite test_playtest_spawn.gd erwartet main.test_start_btn).
if str(entry["id"]) == "teststart":
btn.toggle_mode = true
main.set("test_start_btn", btn)
add_child(btn)
# UI-Größe (Karte t_297221b5): Klein/Mittel/Groß — Sly stellt das selbst um,

View file

@ -386,6 +386,7 @@ func _test_esc_policy() -> void:
_check(ShortcutPolicy.esc_action("terrain", false, false) == "exit_terrain", "Esc im Terrain-Werkzeug → Werkzeug verlassen")
_check(ShortcutPolicy.esc_action("spawns", false, false) == "exit_spawns", "Esc im Spawn-Werkzeug → Werkzeug verlassen")
_check(ShortcutPolicy.esc_action("bauen", false, false) == "exit_bauen", "Esc im Bauen-Modus → Werkzeug verlassen")
_check(ShortcutPolicy.esc_action("teststart", false, false) == "exit_teststart", "Esc im Test-Start-Werkzeug → Werkzeug verlassen (Punkt bleibt)")
_check(ShortcutPolicy.esc_action("bauen", true, true) == "exit_bauen", "Werkzeug hat VORRANG vor Platzieren")
_check(ShortcutPolicy.esc_action("objects", true, true) == "cancel_placement", "Esc beim Platzieren → Ghost-Preview abbrechen")
_check(ShortcutPolicy.esc_action("objects", false, true) == "clear_selection", "Esc mit Auswahl → Auswahl aufheben")

View file

@ -101,7 +101,9 @@ func _init() -> void:
_check(left != null and left is TabContainer, "linke Spalte ist ein TabContainer (U2-vorbereitet)")
_check(bottom != null and bottom is VSplitContainer, "unten: VSplit (größenverstellbar)")
if right != null:
_check(right.get_child_count() == 5, "rechts: 5 Tabs (Inspektor/Auswahl/Map/Terrain/Prüfen) — hat %d" % right.get_child_count())
# 20.10.: „Test-Start" ist als 6. Reiter dazugekommen (Wiederanbindung T1b,
# nachdem U1 die Verdrahtung verloren hatte).
_check(right.get_child_count() == 6, "rechts: 6 Tabs (Inspektor/Auswahl/Map/Terrain/Prüfen/Test-Start) — hat %d" % right.get_child_count())
_check(right.get_tab_count() == right.get_child_count(),
"rechts: alle Bereiche als Tab in der Leiste erreichbar (Titel je Bereich)")
for i in right.get_tab_count():