404 lines
No EOL
16 KiB
GDScript
404 lines
No EOL
16 KiB
GDScript
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 |