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: var p := MousePointer.from_input(event) if p != null and handle_pointer(p): get_viewport().set_input_as_handled() func handle_pointer(p: PointerEvent) -> bool: if not active: return false if p.kind == PointerEvent.Kind.MOVE: _update_cursor(p.position) elif p.kind == PointerEvent.Kind.PRESS: _click(p.position) return true return false 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 # 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" 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