scopedvr-map-editor/src/viewport/map_view_builder.gd

404 lines
No EOL
16 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends Node3D
## Baut die visuelle Repräsentation der Map im Editor-Viewport:
## Terrain-Höhenfeld-Mesh + Maßstabsgitter + Himmel/Licht
## + Platzhalter-Meshes für Bibliotheks-Objekte + Spawn-Marker mit Beschriftung.
## ##
## Objekt-Marker werden in object_markers gemerkt (Meta: obj_index,
## pick_radius) — Auswahl/Werkzeuge (interaction.gd) greifen darauf zu,
## statt die Szene selbst zu durchsuchen.
const GRID_COLOR := Color(0.35, 0.45, 0.55, 0.6)
const AXIS_COLOR := Color(0.9, 0.7, 0.2, 0.9)
const OBJECT_COLOR := Color(0.55, 0.75, 0.95)
const SELECT_COLOR := Color(1.0, 0.6, 0.15) # Auswahl klar orange
const TEAM_COLORS := [Color(0.3, 0.6, 1.0), Color(1.0, 0.35, 0.3)] # Blau / Rot
# Wetter → Licht (grobe Stimmung; Feintuning kommt mit dem Wetter-Konsum)
const WEATHER_LIGHT := {
"mittag": {"energy": 1.2, "color": Color(1.0, 0.98, 0.9), "ambient": 0.45, "sky_top": Color(0.28, 0.5, 0.85)},
"abend": {"energy": 0.9, "color": Color(1.0, 0.75, 0.5), "ambient": 0.3, "sky_top": Color(0.35, 0.3, 0.55)},
"regen": {"energy": 0.6, "color": Color(0.8, 0.85, 0.9), "ambient": 0.35, "sky_top": Color(0.45, 0.48, 0.52)},
"nacht": {"energy": 0.25, "color": Color(0.6, 0.7, 1.0), "ambient": 0.12, "sky_top": Color(0.03, 0.05, 0.12)},
}
var selected_index := -1 # aktuelles Auswahl-Objekt (orange gefärbt)
var selected_indices: Array = [] # komplette Auswahl (Karte t_f6f29d86 — Mehrfachauswahl)
var object_markers: Array = [] # Node3D je Objekt (gleiche Reihenfolge wie doc.objects)
var spawn_markers: Array = [] # Node3D je Spawn-Point (Meta: spawn_team/spawn_id/spawn_radius)
var terrain_view # TerrainMesh-Ansicht (script-erweitert, Variant)
var gebaeude_markers: Array = [] # MeshInstance3D je Bauteil (Karte t_97ea59c5)
var _gebaeude_baked: Dictionary = {} # faces/offsets für Ray-Pick (Bauen-Werkzeug)
var _gebaeude_sel: MeshInstance3D # orangener AABB-Rahmen um das gewählte Teil
var _doc_ref: Dictionary = {} # letztes rebuild-Doc (Auswahl-Markierung)
var _base_mat: StandardMaterial3D
var _select_mat: StandardMaterial3D
func _ready() -> void:
rebuild({}, {})
## Komplette Szene aus dem Map-Dict neu aufbauen. manifest = Bibliotheks-Registry.
func rebuild(data: Dictionary, manifest: Dictionary) -> void:
for child in get_children():
child.queue_free()
object_markers = []
spawn_markers = []
gebaeude_markers = []
_gebaeude_baked = {}
_gebaeude_sel = null
_doc_ref = data
_base_mat = StandardMaterial3D.new()
_base_mat.albedo_color = OBJECT_COLOR
_base_mat.roughness = 0.8
_select_mat = StandardMaterial3D.new()
_select_mat.albedo_color = SELECT_COLOR
_select_mat.emission_enabled = true
_select_mat.emission = SELECT_COLOR * 0.35
_select_mat.roughness = 0.7
if data.is_empty():
return
var terrain: Dictionary = data.get("terrain", {})
var size_x := float(terrain.get("size_x_m", 1024.0))
var size_z := float(terrain.get("size_z_m", 1024.0))
var ground_y := float(terrain.get("min_height_m", 0.0))
var weather := str(data.get("weather", "mittag"))
_build_environment(weather)
_build_terrain(data)
_build_grid(size_x, size_z, ground_y)
_build_objects(data.get("objects", []), manifest)
_rebuild_spawn_markers(data)
_build_gebaeude(data)
## Terrain als echtes Höhenfeld-Mesh statt flacher Ebene (Karte t_9aba04f1):
## der Terrain-Pinsel schreibt die Vertices live neu (terrain_view.update_terrain).
func _build_terrain(data: Dictionary) -> void:
terrain_view = Node3D.new()
terrain_view.set_script(load("res://src/viewport/terrain_mesh.gd"))
add_child(terrain_view)
terrain_view.call("rebuild_from_doc", data)
## Live-Update während eines Pinsel-Strichs (aus terrain_tool.gd): nur die
## Vertices werden neu geschrieben — Grid/Objekte/Marker bleiben unangetastet.
## mats (optional) = Material-Schicht (Karte t_603ec1a8, Malen-Modus) —
## leer = Farben nur aus der Höhe (Legacy).
func update_terrain(heights: PackedFloat32Array, mats: PackedByteArray = PackedByteArray()) -> void:
if terrain_view != null and is_instance_valid(terrain_view):
terrain_view.call("update_heights", heights, 0, 0.0, 0.0, mats)
## Nur das Terrain neu aufbauen (nach Strich-Commit/Undo) — Objekt-Marker und
## Auswahl bleiben bestehen, kein Flackern der ganzen Szene.
func rebuild_terrain_from_doc(data: Dictionary) -> void:
if terrain_view != null and is_instance_valid(terrain_view):
terrain_view.call("rebuild_from_doc", data)
func _build_environment(weather: String) -> void:
var preset: Dictionary = WEATHER_LIGHT.get(weather, WEATHER_LIGHT["mittag"])
var env := Environment.new()
var sky := Sky.new()
var sky_mat := ProceduralSkyMaterial.new()
sky_mat.sky_top_color = preset["sky_top"]
sky_mat.sky_horizon_color = Color(0.6, 0.65, 0.7)
sky_mat.ground_bottom_color = Color(0.1, 0.1, 0.12)
sky_mat.ground_horizon_color = Color(0.4, 0.42, 0.45)
sky.sky_material = sky_mat
env.background_mode = Environment.BG_SKY
env.sky = sky
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color(1, 1, 1)
env.ambient_light_energy = preset["ambient"]
var we := WorldEnvironment.new()
we.environment = env
add_child(we)
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-50.0, 30.0, 0.0)
sun.light_energy = preset["energy"]
sun.light_color = preset["color"]
sun.shadow_enabled = true
add_child(sun)
## Maßstabsgitter: Linien alle 100 m, gelbe Mittelachsen (X rot? — Z-Achse
## blau wäre Godot-Konvention; hier neutral gelb für gute Sichtbarkeit).
func _build_grid(size_x: float, size_z: float, ground_y: float) -> void:
var mesh := ImmediateMesh.new()
mesh.surface_begin(Mesh.PRIMITIVE_LINES)
var step := _grid_step(size_x)
var half_x := size_x / 2.0
var half_z := size_z / 2.0
var y := ground_y + 0.05
var x := -half_x
while x <= half_x + 0.01:
var major: bool = absf(fmod(x, step * 5.0)) < 0.01 or absf(x) < 0.01
_add_line(mesh, Vector3(x, y, -half_z), Vector3(x, y, half_z),
AXIS_COLOR if absf(x) < 0.01 else (GRID_COLOR if not major else GRID_COLOR))
x += step
var z := -half_z
while z <= half_z + 0.01:
_add_line(mesh, Vector3(-half_x, y, z), Vector3(half_x, y, z),
AXIS_COLOR if absf(z) < 0.01 else GRID_COLOR)
z += step
mesh.surface_end()
var mi := MeshInstance3D.new()
mi.mesh = mesh
var mat := StandardMaterial3D.new()
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.vertex_color_use_as_albedo = true
mi.material_override = mat
add_child(mi)
func _grid_step(size_m: float) -> float:
# Ziel: ~10–20 Linien pro Achse, runde Werte.
var raw := size_m / 16.0
var candidates := [10.0, 25.0, 50.0, 100.0, 250.0, 500.0]
for c in candidates:
if raw <= float(c):
return float(c)
return 1000.0
func _add_line(mesh: ImmediateMesh, from: Vector3, to: Vector3, color: Color) -> void:
mesh.surface_set_color(color)
mesh.surface_add_vertex(from)
mesh.surface_set_color(color)
mesh.surface_add_vertex(to)
## Platzhalter je Objekt: Form aus dem Bibliotheks-Manifest (wie der Server-
## Kollider), Position = Objekt-Fußpunkt. Marker-Root bekommt Metas für
## Auswahl/Anfassen: obj_index, pick_radius (Kugel-Approximation), half_height.
func _build_objects(objects: Array, manifest: Dictionary) -> void:
var lib: Dictionary = manifest.get("objects", {})
for i in objects.size():
var obj = objects[i]
if not (obj is Dictionary):
object_markers.append(null)
continue
var oid := str(obj.get("id", ""))
if not lib.has(oid):
object_markers.append(null)
continue # Validierung fängt das — defensiv überspringen.
var entry: Dictionary = lib[oid]
var col: Dictionary = entry.get("collision", {})
var scale := float(obj.get("scale", 1.0))
var mesh := MeshShapes.make_placeholder(str(col.get("shape", "box")), col, scale)
var pos: Array = obj.get("pos", [0.0, 0.0, 0.0])
var marker := Node3D.new()
marker.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
marker.rotation.y = deg_to_rad(float(obj.get("rot_y_deg", 0.0)))
marker.set_meta("obj_index", i)
var mi := MeshInstance3D.new()
mi.mesh = mesh
mi.position.y = MeshShapes.half_height(mesh)
mi.material_override = _select_mat if _is_selected(i) else _base_mat
marker.add_child(mi)
var half := (mesh.get_aabb().size * 0.5) as Vector3
marker.set_meta("pick_radius", half.length() + maxf(0.6, half.y * 0.25))
marker.set_meta("half_height", MeshShapes.center_height(mesh))
object_markers.append(marker)
add_child(marker)
## Auswahl nachträglich umfärben (ohne ganzen Rebuild) — z. B. nach Drag.
## Einzel-Variante bleibt für Rückwärtskompatibilität bestehen.
func set_selected(idx: int) -> void:
selected_index = idx
set_selected_multi([idx] if idx >= 0 else [])
## Mehrfachauswahl (Karte t_f6f29d86): ALLE markierten Objekte färben sich
## orange — Gruppen/Mehrfachauswahl sind auf einen Blick erkennbar.
func set_selected_multi(indices: Array) -> void:
selected_indices = indices.duplicate()
selected_index = indices.back() if not indices.is_empty() else -1
for marker in object_markers:
if marker == null or not is_instance_valid(marker):
continue
var mi: MeshInstance3D = marker.get_child(0)
mi.material_override = _select_mat if _is_selected(int(marker.get_meta("obj_index", -1))) else _base_mat
func _is_selected(idx: int) -> bool:
return selected_indices.has(idx)
## Spawn-Zonen (Karte t_c4162c5d): JEDE Zone ist ein flacher, teamfarbiger
## Kreis auf dem Boden (Radius = design_notes.spawn_radius_m, Editor-eigen)
## + Label3D mit der Punkt-ID. Marker-Root bekommt Metas für das Spawn-Tool
## (spawn_team, spawn_id, spawn_radius → Anfassen/Regel).
func _rebuild_spawn_markers(data: Dictionary) -> void:
for marker in spawn_markers:
if marker != null and is_instance_valid(marker):
marker.queue_free()
spawn_markers = []
var zones = data.get("spawn_zones", {})
if not (zones is Dictionary):
return
for team_key in zones.keys():
if not (zones[team_key] is Dictionary):
continue
var team := int(team_key)
var zone: Dictionary = zones[team_key]
var points = zone.get("points", [])
if not (points is Array):
continue
for p in points:
if not (p is Dictionary):
continue
var pid: String = str(p.get("id", "?"))
var pos: Array = p.get("pos", [0.0, 0.0, 0.0])
var marker := _spawn_marker(
Vector3(float(pos[0]), float(pos[1]), float(pos[2])),
team, pid, SpawnOps.zone_radius(data, pid))
spawn_markers.append(marker)
add_child(marker)
## Ein Spawn-Marker bauen (flacher Kreis + Label). radius = angezeigter Zonen-Radius.
func _spawn_marker(pos: Vector3, team: int, pid: String, radius: float) -> Node3D:
var marker := Node3D.new()
marker.position = pos
marker.set_meta("spawn_team", team)
marker.set_meta("spawn_id", pid)
marker.set_meta("spawn_radius", radius)
marker.set_meta("spawn_pick_radius", maxf(radius + 2.0, 4.0))
marker.set_meta("spawn_selected", false)
marker.add_child(_spawn_disc(radius, team))
var label := Label3D.new()
label.text = pid
label.position.y = 2.5
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
label.modulate = TEAM_COLORS[team] if team < TEAM_COLORS.size() else Color.WHITE
label.outline_size = 12
label.font_size = 160
label.pixel_size = 0.01
marker.add_child(label)
return marker
## Kreisfläche EINES Markers (neu) bauen — Werkzeuge rufen nach Radius-Änderung
## style_spawn_marker auf.
func _spawn_disc(radius: float, team: int, selected := false) -> MeshInstance3D:
var mi := MeshInstance3D.new()
var cyl := CylinderMesh.new()
cyl.top_radius = radius
cyl.bottom_radius = radius
cyl.height = 0.25
mi.mesh = cyl
mi.position.y = 0.12
var col: Color = TEAM_COLORS[team] if team >= 0 and team < TEAM_COLORS.size() else Color.WHITE
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(col.r, col.g, col.b, 0.55 if not selected else 0.8)
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
if selected:
mat.emission_enabled = true
mat.emission = col * 0.6
mi.material_override = mat
return mi
## Radius/Anfass-Optik eines Spawn-Markers aktualisieren OHNE Gesamtrebuild
## (Radius-Regel gedreht, Zone ausgewählt/abgewählt).
func style_spawn_marker(marker: Node3D, radius: float, selected: bool) -> void:
if marker == null or not is_instance_valid(marker):
return
var team := int(marker.get_meta("spawn_team", -1))
var pid := str(marker.get_meta("spawn_id", ""))
marker.set_meta("spawn_radius", radius)
marker.set_meta("spawn_pick_radius", maxf(radius + 2.0, 4.0))
marker.set_meta("spawn_selected", selected)
if marker.get_child_count() > 0 and marker.get_child(0) is MeshInstance3D:
(marker.get_child(0) as MeshInstance3D).queue_free()
var disc := _spawn_disc(radius, team, selected)
marker.add_child(disc)
marker.move_child(disc, 0)
## Auswahl-Highlight über ALLE Spawn-Marker (Werkzeug ruft bei Änderung).
func set_spawn_selected(team: int, pid: String) -> void:
for marker in spawn_markers:
if marker == null or not is_instance_valid(marker):
continue
var is_sel := int(marker.get_meta("spawn_team", -1)) == team \
and str(marker.get_meta("spawn_id", "")) == pid
if bool(marker.get_meta("spawn_selected", false)) == is_sel:
continue
style_spawn_marker(marker, float(marker.get_meta("spawn_radius", 6.0)), is_sel)
# Label sofort umfärben (auch ohne Disc-Neubau sichtbar).
var col: Color = TEAM_COLORS[clampi(team, 0, 1)] if team >= 0 else Color.WHITE
if marker.get_child_count() > 1 and marker.get_child(1) is Label3D:
var label := marker.get_child(1) as Label3D
label.modulate = Color(1.0, 0.85, 0.2) if is_sel else col
# ------------------------------------------------------------ Bauteile ------
# (Karte t_97ea59c5 — Modular-Builder im App-Skelett; Meshes kommen aus der
# CC0-Plugin-Runtime-Geometrie, Materialien = eigene flache Fallback-Farben.)
## Bauteile aus dem doc backen und als MeshInstances aufbauen — plus die
## Welt-Flächen cachen, damit das Bauen-Werkzeug präzise auf Wände/Dächer
## klicken (stapeln) und Teile auswählen kann.
func _build_gebaeude(data: Dictionary) -> void:
_gebaeude_baked = BuildOps.bake_faces(data)
var mats := {}
var list: Array = BuildOps.pieces(data)
for i in list.size():
var teil: Dictionary = list[i]
var mesh := BuildOps.piece_mesh(teil)
if mesh == null:
continue
var pos: Array = teil.get("pos", [0.0, 0.0, 0.0])
var mi := MeshInstance3D.new()
mi.mesh = mesh
mi.material_override = BuildOps.piece_material(str(teil.get("id", "")), mats)
mi.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
mi.rotation.y = deg_to_rad(float(teil.get("rot", 0.0)))
add_child(mi)
gebaeude_markers.append(mi)
## Gebackene Welt-Flächen aller Bauteile (für das Bauen-Werkzeug: Klicken/
## Stapeln/Auswählen). Leeres faces = noch nichts gebaut.
func gebaeude_baked() -> Dictionary:
if _gebaeude_baked.is_empty():
return {"faces": PackedVector3Array(), "offsets": []}
return _gebaeude_baked
## Auswahl-Markierung: orangener, halbtransparenter Rahmen um das Bauteil.
func set_gebaeude_selected(idx: int) -> void:
if _gebaeude_sel != null and is_instance_valid(_gebaeude_sel):
_gebaeude_sel.queue_free()
_gebaeude_sel = null
var list: Array = BuildOps.pieces(_doc_ref)
if idx < 0 or idx >= list.size():
return
var aabb := BuildOps.piece_aabb(list[idx])
if aabb.size.length() < 0.001:
return
var box := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = aabb.size + Vector3(0.06, 0.06, 0.06)
box.mesh = bm
box.position = aabb.get_center()
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(SELECT_COLOR.r, SELECT_COLOR.g, SELECT_COLOR.b, 0.35)
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.emission_enabled = true
mat.emission = SELECT_COLOR * 0.3
box.material_override = mat
add_child(box)
_gebaeude_sel = box