extends Node3D ## Baut die visuelle Repräsentation der Map im Editor-Viewport: ## Terrain-Höhenfeld-Mesh + Maßstabsgitter + Himmel/Licht ## + Echte GLB-Meshes für Bibliotheks-Objekte (GlbMeshStore; ohne GLB = ## Platzhalter-Mesh aus dem Kollisions-Eintrag) + 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 fehlende_glbs: Array = [] # OIDs ohne echte GLB (Platzhalter-Fallback — Statuszeilen-Hinweis) 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 grid_node: MeshInstance3D # Maßstabsgitter (Toggle „⊞ Gitter“, Karte t_7ac70014) 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 = [] grid_node = null _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) # Gitter default AUS über sichtbarem Terrain (Karte t_7ac70014): das # Terrain ist die Bodendarstellung — das Gitter lag als Linien-Overlay # DARÜBER und wirkte wie ein „unfertiges“ Editor-Raster. Ohne Terrain- # Block bleibt es als Boden-Referenz sichtbar. if grid_node != null: grid_node.visible = terrain.is_empty() _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 grid_node = mi add_child(mi) ## Gitter ohne Rebuild zu-/abschalten (Toggle in der Top-Bar, Karte t_7ac70014). func set_grid_visible(visible_now: bool) -> void: if grid_node != null and is_instance_valid(grid_node): grid_node.visible = visible_now func grid_is_visible() -> bool: return grid_node != null and is_instance_valid(grid_node) and grid_node.visible 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. ECHTES GLB-Mesh vorziehen ## (Karte t_9b50b231, GlbMeshStore): vorhanden = echtes Modell mit Texturen, ## fehlend = Platzhalter + Eintrag in fehlende_glbs (Statuszeilen-Hinweis). ## Marker-Root bekommt Metas für Auswahl/Anfassen: obj_index, pick_radius ## (Kugel-Approximation), half_height, glb (true = echtes Modell — keine ## Grundfarbe drüberlegen, sonst sind die Texturen weg). func _build_objects(objects: Array, manifest: Dictionary) -> void: var lib: Dictionary = manifest.get("objects", {}) fehlende_glbs = [] 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 := GlbMeshStore.mesh_fuer(oid) var echt := mesh != null if not echt: if GlbMeshStore.fehlt(oid): fehlende_glbs.append(oid) 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 if echt: # Normalisiertes GLB liegt schon in Fußpunkt-Koordinaten # (Unterkante y = 0) → KEIN half_height-Lift; Objekt-Maßstab # über Node-Scale. AABB-Metas mal scale (Node-Scale geht nicht # in mesh.get_aabb() ein). mi.scale = Vector3.ONE * scale else: mi.position.y = MeshShapes.half_height(mesh) mi.material_override = _select_mat if _is_selected(i) else (null if echt else _base_mat) marker.add_child(mi) var half: Vector3 = (mesh.get_aabb().size * (scale if echt else 1.0) * 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) * (scale if echt else 1.0)) marker.set_meta("glb", echt) 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. ## Echte GLB-Objekte (Meta „glb") bekommen im ungewählten Zustand KEINE ## Grundfarbe drübergelegt — material_override würde die Texturen verstecken ## (Karte t_9b50b231); orange bleibt auch dort das Auswahl-Signal. 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) var echt := bool(marker.get_meta("glb", false)) mi.material_override = _select_mat if _is_selected(int(marker.get_meta("obj_index", -1))) else (null if echt 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