253 lines
8.5 KiB
GDScript
253 lines
8.5 KiB
GDScript
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
|