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 TEAM_COLORS := [Color(0.3, 0.6, 1.0), Color(1.0, 0.35, 0.3)] # Blau / Rot # Auswahl-Markierung (U6, t_639135dd): Farbe + Breite kommen aus dem Theme # (eine Quelle) — Auswahl-Kontur, schwächere Hover-Kontur, dünn-gestrichelte # Partner-Kontur. Kein Vollflächen-Orange mehr: das Grundmaterial (mit Textur) # bleibt sichtbar, die Markierung lebt als Außenhüllen-Kontur darum herum. const THEME_PATH := "res://core/theme/editor_theme.tres" const OUTLINE_SHADER := "res://core/shaders/selection_outline.gdshader" # Fallback, falls das Theme (noch) nicht geladen werden kann. const OUTLINE_FALLBACK := Color(1.0, 0.6, 0.15, 0.95) # Wetter → Licht/Himmel — 1:1-SPIEGEL aus client/scripts/world/weather_presets.gd # (Karte t_f72913d4: Editor-Vorschau zeigt exakt was der Client zeigt — dieselbe # Projekt-Konvention wie KolliderBau als 1:1-Spiegel von svmap_world, t_1da96275). # Bei Preset-Änderungen im Client HIER MITPFLEGEN. Schlüssel = Client-Keys. const WEATHER_LIGHT := { "mittag": { "sky_top": Color(0.30, 0.54, 0.86), "sky_horizon": Color(0.72, 0.83, 0.92), "ground_bottom": Color(0.20, 0.23, 0.27), "ground_horizon": Color(0.66, 0.75, 0.83), "sky_energy": 1.0, "sun_deg": Vector2(-52.0, 30.0), "sun_energy": 1.2, "sun_color": Color(1.0, 0.98, 0.92), "ambient_source": "sky", "ambient_color": Color(0.60, 0.70, 0.85), "ambient_energy": 1.0, "fog_enabled": false, "fog_color": Color(0.80, 0.85, 0.90), "fog_density": 0.0, "fog_sky_affect": 0.0, "sky_panorama": false, "panorama_fog_color": Color(0.93, 0.80, 0.66), "panorama_energy": 1.0, }, "abend": { "sky_top": Color(0.25, 0.30, 0.55), "sky_horizon": Color(0.95, 0.55, 0.28), "ground_bottom": Color(0.16, 0.13, 0.14), "ground_horizon": Color(0.80, 0.45, 0.25), "sky_energy": 1.0, "sun_deg": Vector2(-8.0, 250.0), "sun_energy": 0.9, "sun_color": Color(1.0, 0.62, 0.35), "ambient_source": "color", "ambient_color": Color(0.55, 0.40, 0.32), "ambient_energy": 0.8, "fog_enabled": true, "fog_color": Color(0.85, 0.55, 0.35), "fog_density": 0.004, "fog_sky_affect": 0.2, "sky_panorama": false, "panorama_fog_color": Color(0.95, 0.72, 0.48), "panorama_energy": 0.8, }, "regen": { "sky_top": Color(0.38, 0.41, 0.45), "sky_horizon": Color(0.55, 0.58, 0.62), "ground_bottom": Color(0.22, 0.23, 0.25), "ground_horizon": Color(0.50, 0.53, 0.57), "sky_energy": 0.9, "sun_deg": Vector2(-40.0, 120.0), "sun_energy": 0.25, "sun_color": Color(0.75, 0.78, 0.82), "ambient_source": "color", "ambient_color": Color(0.55, 0.58, 0.62), "ambient_energy": 1.0, "fog_enabled": true, "fog_color": Color(0.55, 0.58, 0.62), "fog_density": 0.015, "fog_sky_affect": 0.6, "sky_panorama": false, "panorama_fog_color": Color(0.76, 0.72, 0.68), "panorama_energy": 0.55, }, "nacht": { "sky_top": Color(0.015, 0.03, 0.08), "sky_horizon": Color(0.05, 0.08, 0.15), "ground_bottom": Color(0.01, 0.015, 0.025), "ground_horizon": Color(0.04, 0.06, 0.11), "sky_energy": 1.0, "sun_deg": Vector2(-65.0, 200.0), "sun_energy": 0.35, "sun_color": Color(0.62, 0.72, 1.0), "ambient_source": "color", "ambient_color": Color(0.10, 0.14, 0.25), "ambient_energy": 0.5, "fog_enabled": true, "fog_color": Color(0.04, 0.06, 0.11), "fog_density": 0.006, "fog_sky_affect": 0.4, "sky_panorama": false, "panorama_fog_color": Color(0.10, 0.12, 0.18), "panorama_energy": 0.10, }, } ## Pack-Panorama-Skybox (Karte t_f72913d4): die Textur liegt im Client-Repo ## (Nachbar-Klon — KEINE Duplikat-Pflicht im Editor, siehe Spec) und wird über ## einen absoluten Pfad geladen; fehlt sie → Fallback ProceduralSky. const PANORAMA_SKY_CLIENT_REL := "assets/textures/military_desert/Skybox_3.png" ## Absoluter Pfad der Pack-Skybox im Nachbar-Klon ("" = kein Klon gefunden). static func panorama_sky_pfad() -> String: var root := TestRun.find_client(ProjectSettings.globalize_path("res://"), TestRun.load_cfg()) if root.is_empty(): return "" return root.path_join(PANORAMA_SKY_CLIENT_REL) var selected_index := -1 # aktuelles Auswahl-Objekt (Kontur-Markierung) var selected_indices: Array = [] # komplette Auswahl (Karte t_f6f29d86 — Mehrfachauswahl) var hover_index := -1 # Hover-Objekt (schwächste Kontur; U6-Abstufung) 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 zone_root: Node3D = null # Conquest-Zonen bzw. Hills (Spielmodus), Kinder je Zone var spawn_markers: Array = [] # Node3D je Spawn-Point (Meta: spawn_team/spawn_id/spawn_radius) var terrain_view # TerrainMesh-Ansicht (script-erweitert, Variant) var grid_node: MeshInstance3D # Maßstabsgitter (Toggle „⊞ Gitter“, Karte t_7ac70014) var _doc_ref: Dictionary = {} # letztes rebuild-Doc (Auswahl-Markierung) var _base_mat: StandardMaterial3D var _sym_partners: Array = [] # Indizes der Zwillinge der aktuellen Auswahl # Kontur-Materialien (U6): Auswahl / Hover / Spiegel-Zwilling — Farben und # Breiten kommen aus dem Theme, der Shader ist die Außenhüllen-Technik. var _outline_mat_sel: ShaderMaterial var _outline_mat_hover: ShaderMaterial var _outline_mat_partner: ShaderMaterial 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 = [] zone_root = null grid_node = null _doc_ref = data _base_mat = StandardMaterial3D.new() _base_mat.albedo_color = OBJECT_COLOR _base_mat.roughness = 0.8 # U6 (t_639135dd): keine Vollfarben-Materialien mehr — die Markierung # (Auswahl/Hover/Partner) lebt als Außenhüllen-Kontur um das Modell, # Farben + Breiten aus dem Theme. _markierung_theme_laden() _sym_partners = [] 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")) # Skybox-Option (Karte t_f72913d4): optionales svmap-Feld `weather_sky` # ("sky_panorama"/"prozedural"/leer) — dieselbe Semantik wie im Client. var weather_sky := str(data.get("weather_sky", "")) _build_environment(weather, weather_sky) _build_terrain(data) _build_wasser(data) # Karte t_28fdef03: nach dem Terrain (Senken füllen sich) _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) rebuild_zone_markers(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://core/viewport/terrain_mesh.gd")) add_child(terrain_view) terrain_view.call("rebuild_from_doc", data) ## Wasserfläche (Karte t_28fdef03): EINE transparente Fläche auf `water_level` ## über der GANZEN Map (SvmapWasser.baue — Senken unter der Höhe füllen sich ## automatisch). OHNE Feld → kein Wasser (Legacy-Maps unverändert). Dasselbe ## Material-Verfahren wie der Client (SvmapWasser.make_material — Identitäts- ## Vertrag der Vorschau: Editor zeigt exakt, was der Quest-Client zeigt). ## Leicht ÜBER dem Terrain eingehängt (y-Offset +0.01 über Reihenfolge), aber ## die Fläche selbst liegt exakt auf der Wasserhöhe (Z-Fight-Vermeidung macht ## der Shader über die Tiefen-Textur, nicht über einen Off-Aufschlag). func _build_wasser(data: Dictionary) -> void: var level := SvmapWasser.level(data) if not is_finite(level): return # Legacy/fehlendes Feld — kein Wasser, nichts bauen. var terrain: Dictionary = data.get("terrain", {}) var wetter := str(data.get("weather", "mittag")) var wasser := SvmapWasser.baue(terrain, level, wetter) if wasser != null: add_child(wasser) ## 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 = terrain_mesh verwendet die zuletzt gesehene Schicht weiter (Cache, ## Karte t_55ed041a); nur bei NIE gesehener Schicht (Legacy-v1) gilt Gras. ## splat_rect_cells (optional, Karte t_7a0a6b79) = Zellrechteck des Mal- ## Schritts (inkl. Spiegelzellen) — terrain_mesh leitet nur die Splat-Region ## neu ab (Sub-Rect-Update, kein Voll-Rebuild pro Mal-Frame). func update_terrain(heights: PackedFloat32Array, mats: PackedByteArray = PackedByteArray(), splat_rect_cells: Rect2i = Rect2i()) -> void: if terrain_view != null and is_instance_valid(terrain_view): terrain_view.call("update_heights", heights, 0, 0.0, 0.0, mats, splat_rect_cells) ## Misch-Schicht für das nächste Live-Update (Malen mit weichem Rand, Karte 25.10.). func set_terrain_mix(mix_b: PackedByteArray, mix_w: PackedByteArray) -> void: if terrain_view != null and is_instance_valid(terrain_view): terrain_view.call("set_mix", mix_b, mix_w) ## 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) ## Himmel/Licht — 1:1-Spiegel von client autoload/weather.gd._apply_environment ## (Karte t_f72913d4): Map-Override > Preset-Key > ProceduralSky; ACES-Tonemap; ## Fog koppelt im Panorama-Fall an den Skybox-Horizont (aerial perspective). ## Kein Glow/Bloom/Volumetric — dokumentierte Absage (Mobile/Perf). func _build_environment(weather: String, sky_override: String = "") -> void: var preset: Dictionary = WEATHER_LIGHT.get(weather, WEATHER_LIGHT["mittag"]) var env := Environment.new() var sky := Sky.new() # Katalog-ID (weather_sky) > Preset-Key `sky_panorama` > ProceduralSky. var pano_id := "" if sky_override == "prozedural": pass elif sky_override != "": pano_id = sky_override elif bool(preset.get("sky_panorama", false)): pano_id = SkyboxKatalog.ID_PACK_WUESTE var panorama := false if pano_id != "": var tex := SkyboxKatalog.panorama_textur(pano_id) if tex != null: var pano_mat := PanoramaSkyMaterial.new() pano_mat.panorama = tex pano_mat.energy_multiplier = float(preset.get("panorama_energy", 1.0)) sky.sky_material = pano_mat panorama = true else: push_warning("MAPBUILDER: weather_sky=%s, aber Skybox-Textur fehlt/unbekannt — Fallback ProceduralSky" % pano_id) if sky.sky_material == null: var sky_mat := ProceduralSkyMaterial.new() sky_mat.sky_top_color = preset["sky_top"] sky_mat.sky_horizon_color = preset["sky_horizon"] sky_mat.ground_bottom_color = preset["ground_bottom"] sky_mat.ground_horizon_color = preset["ground_horizon"] sky_mat.sky_energy_multiplier = float(preset["sky_energy"]) sky.sky_material = sky_mat env.background_mode = Environment.BG_SKY env.sky = sky # Ambient: "sky" = aus dem Himmel ableiten, "color" = feste Farbe (Client-Logik). if str(preset.get("ambient_source", "color")) == "sky": env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY else: env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = preset["ambient_color"] env.ambient_light_energy = float(preset["ambient_energy"]) # Tonemap ACES — derselbe filmische Look wie der Client; Grading bewusst # NUR über Tonemap + Preset-Farben (kein Fullscreen-Adjustment-Pass). env.tonemap_mode = Environment.TONE_MAPPER_ACES env.fog_enabled = bool(preset["fog_enabled"]) env.fog_light_color = SkyboxKatalog.fog_farbe(sky_override, preset.get("panorama_fog_color", preset["fog_color"]), float(preset.get("panorama_energy", 1.0))) if panorama else preset["fog_color"] env.fog_density = float(preset["fog_density"]) env.fog_sky_affect = float(preset["fog_sky_affect"]) var we := WorldEnvironment.new() we.environment = env add_child(we) var sun := DirectionalLight3D.new() var deg: Vector2 = preset["sun_deg"] sun.rotation_degrees = Vector3(deg.x, deg.y, 0.0) sun.light_energy = float(preset["sun_energy"]) sun.light_color = preset["sun_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])) # Karte t_e166e656: 3-Achsen-Rotation — `rot` (Euler xyz) oder Fallback # rot_y_deg, identische Regel wie im Client/Server (SvmapIO.objekt_rotation). marker.rotation_degrees = SvmapIO.objekt_rotation(obj) 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) # U6 (t_639135dd): das Grundmaterial wird NIE MEHR durch die Auswahl # ersetzt — echte GLB-Objekte tragen ihre Texturen weiter (override null), # Platzhalter die Editor-Grundfarbe. Markiert wird per Kontur (unten). mi.material_override = 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) # Farbe je Objekt (Karte t_b0edb807): optionales Feld "tint" — nur # Objekte MIT Feld bekommen die Überschreibung (Rückwärtskompatibel). # Die Farbe bleibt jetzt auch bei Markierung sichtbar (U6: Auswahl = Kontur, # Grundmaterial bleibt) — kein „Orange gewinnt“ mehr. if SvmapTint.hat(obj): SvmapTint.anwenden(marker, SvmapTint.von(obj)) # Markierung (Auswahl/Hover/Partner) als Kontur-Hülle anlegen. _outline_anwenden(marker, i) ## 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 bekommen die ## Auswahl-Kontur (U6, t_639135dd) — Gruppen/Mehrfachauswahl sind auf einen ## Blick erkennbar, OHNE die Textur zu übermalen. Das Grundmaterial bleibt ## überall sichtbar: echte GLB-Objekte tragen ihre Texturen weiter (Karte ## t_9b50b231), Platzhalter die Editor-Grundfarbe — material_override wird ## von der Markierung nicht mehr angefasst. func set_selected_multi(indices: Array) -> void: selected_indices = indices.duplicate() selected_index = indices.back() if not indices.is_empty() else -1 # sym-Zwillinge (M3, t_34908425): aus dem doc ableiten — Partner der # Auswahl, die nicht selbst ausgewählt sind, bekommen die dünnere, # gestrichelte Partner-Kontur (U6-Abstufung statt dunkler Vollfarbe). _sym_partners = _sym_partners_of(indices) var objects: Array = _doc_ref.get("objects", []) for marker in object_markers: if marker == null or not is_instance_valid(marker): continue var idx := int(marker.get_meta("obj_index", -1)) _outline_anwenden(marker, idx) # Tint-Abgleich (Karte t_b0edb807): ALLE Objekte zeigen ihre doc-Farbe # (oder — nach „Zurücksetzen“ — wieder die Pack-Farbe). Der Tint bleibt # jetzt auch bei Markierung sichtbar (Auswahl = Kontur um das Modell). if idx >= 0 and idx < objects.size() and objects[idx] is Dictionary: var obj: Dictionary = objects[idx] if SvmapTint.hat(obj): SvmapTint.anwenden(marker, SvmapTint.von(obj)) else: SvmapTint.zuruecknehmen(marker) func _is_partner(idx: int) -> bool: return _sym_partners.has(idx) ## sym-Partner der Auswahl aus dem doc bestimmen (M3, t_34908425): Objekte mit ## sym-Kennung, deren Partner NICHT selbst in der Auswahl ist. Keine sym-Felder ## im doc (alte Karten, Modus Aus) → leer, Verhalten wie bisher. func _sym_partners_of(indices: Array) -> Array: var objects: Array = _doc_ref.get("objects", []) var out: Array = [] for i in indices: if not (i is int) or i < 0 or i >= objects.size() or not (objects[i] is Dictionary): continue var key := SymmetryCore.sym_of(objects[i] as Dictionary) if key.is_empty(): continue var t := SymmetryCore.find_partner_index(objects, key, i) if t >= 0 and not indices.has(t) and not out.has(t): out.append(t) return out ## Tint frisch aus dem doc auf die MARKIERTEN Objekte anwenden (Inspector- ## Änderung ohne Rebuild). U6: die Farbe bleibt auch bei Markierung sichtbar ## (Auswahl = Kontur um das Modell) — kein „Orange gewinnt“-Skip mehr. func tint_refresh(indices: Array) -> void: var objects: Array = _doc_ref.get("objects", []) for i in indices: if i < 0 or i >= object_markers.size(): continue var marker = object_markers[i] if marker == null or not is_instance_valid(marker): continue if i >= objects.size() or not (objects[i] is Dictionary): continue var obj: Dictionary = objects[i] if SvmapTint.hat(obj): SvmapTint.anwenden(marker, SvmapTint.von(obj)) else: SvmapTint.zuruecknehmen(marker) 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 = SpawnOps.zones_of(data) # bearbeiteter Spawn-Satz (Basis oder Modus) if not (zones is Dictionary): return for team_key in zones.keys(): if not (zones[team_key] is Dictionary): continue var team := 0 if str(team_key) == "all" else 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)) var yaw_col: Color = TEAM_COLORS[team] if team < TEAM_COLORS.size() else Color.WHITE marker.set_meta("yaw_color", yaw_col) marker.add_child(_yaw_arrow(SpawnOps.zone_radius(data, pid), SpawnOps.effective_yaw(data, p), SpawnOps.has_yaw(p), yaw_col)) spawn_markers.append(marker) add_child(marker) ## Zonen des Spielmodus: Conquest = gelb, King of the Hill = orange (mit Nummer der ## Wanderreihenfolge). Boden-Kreis + durchscheinender Zylinder + Name. func rebuild_zone_markers(data: Dictionary) -> void: if zone_root != null and is_instance_valid(zone_root): zone_root.queue_free() zone_root = null var mode := ModusKatalog.mode_of(data) if not ModusKatalog.has_zones(mode): return zone_root = Node3D.new() zone_root.name = "Zonen" add_child(zone_root) var col := Color(1.0, 0.8, 0.2) if mode == ModusKatalog.MODE_CONQUEST else Color(1.0, 0.45, 0.15) var zlist := ModusKatalog.zones(data, mode) for zi in zlist.size(): var cp = zlist[zi] if not (cp is Dictionary): continue var pos: Array = cp.get("pos", [0, 0, 0]) var r := float(cp.get("radius_m", ModusKatalog.DEFAULT_RADIUS)) var node := Node3D.new() node.name = "CP_%s" % str(cp.get("id", "")) node.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2])) var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.cull_mode = BaseMaterial3D.CULL_DISABLED mat.albedo_color = Color(col.r, col.g, col.b, 0.18) var cyl := MeshInstance3D.new() var cm := CylinderMesh.new() cm.top_radius = r cm.bottom_radius = r cm.height = 8.0 cm.cap_top = false cm.cap_bottom = false cyl.mesh = cm cyl.material_override = mat cyl.position.y = 4.0 node.add_child(cyl) var ring := MeshInstance3D.new() var tm := TorusMesh.new() tm.inner_radius = r - 0.2 tm.outer_radius = r + 0.2 tm.rings = 48 tm.ring_segments = 6 ring.mesh = tm var rmat := StandardMaterial3D.new() rmat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED rmat.albedo_color = col ring.material_override = rmat ring.position.y = 0.15 node.add_child(ring) var label := Label3D.new() label.text = ("⚑ %s" if mode == ModusKatalog.MODE_CONQUEST else "♛ %d · %%s" % (zi + 1)) % str(cp.get("label", cp.get("id", ""))) label.position.y = 10.0 label.billboard = BaseMaterial3D.BILLBOARD_ENABLED label.modulate = col label.outline_size = 12 label.font_size = 160 label.pixel_size = 0.01 node.add_child(label) if mode == ModusKatalog.MODE_CONQUEST: # Hills sind keine Spawns node.set_meta("zone_id", str(cp.get("id", ""))) node.set_meta("yaw_radius", r) node.add_child(_yaw_arrow(r, SpawnOps.effective_yaw(data, cp), SpawnOps.has_yaw(cp), col)) zone_root.add_child(node) ## 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 ## Pfeil der Blickrichtung: flach am Boden, zeigt lokal nach -Z, rotation.y = Blickrichtung. ## Gesetzte Richtung kräftig, Standard (zur Kartenmitte) blass grau. Ohne Tiefentest, damit ## Hügel und Objekte ihn nicht verdecken. func _yaw_arrow(radius: float, yaw_deg: float, is_set: bool, col: Color) -> MeshInstance3D: var length := yaw_arrow_length(radius) var half_w := maxf(0.25, length * 0.04) var head_w := half_w * 3.0 var head_l := minf(length * 0.4, head_w * 2.5) var neck := -(length - head_l) var verts := PackedVector3Array([ Vector3(-half_w, 0, 0), Vector3(half_w, 0, 0), Vector3(half_w, 0, neck), Vector3(-half_w, 0, 0), Vector3(half_w, 0, neck), Vector3(-half_w, 0, neck), Vector3(-head_w, 0, neck), Vector3(head_w, 0, neck), Vector3(0, 0, -length), ]) var arrays := [] arrays.resize(Mesh.ARRAY_MAX) arrays[Mesh.ARRAY_VERTEX] = verts var mesh := ArrayMesh.new() mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) var mat := StandardMaterial3D.new() mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.cull_mode = BaseMaterial3D.CULL_DISABLED mat.no_depth_test = true mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.albedo_color = col.lightened(0.35) if is_set else Color(0.75, 0.75, 0.75, 0.55) var arrow := MeshInstance3D.new() arrow.name = "YawArrow" arrow.mesh = mesh arrow.material_override = mat arrow.position.y = 0.4 arrow.rotation.y = deg_to_rad(yaw_deg) arrow.set_meta("yaw_deg", yaw_deg) arrow.set_meta("yaw_set", is_set) arrow.set_meta("yaw_length", length) return arrow static func yaw_arrow_length(radius: float) -> float: return maxf(radius, 3.0) + 2.0 func yaw_arrow_of(node: Node3D) -> MeshInstance3D: return node.get_node_or_null("YawArrow") as MeshInstance3D ## Alle Pfeile zum Anfassen: [{kind:"spawn"|"zone", team, id, node, tip:Vector3}]. func yaw_handles() -> Array: var out: Array = [] var add := func(node: Node3D, entry: Dictionary) -> void: var arrow := yaw_arrow_of(node) if arrow == null: return var yaw := deg_to_rad(float(arrow.get_meta("yaw_deg", 0.0))) var length := float(arrow.get_meta("yaw_length", 0.0)) entry["node"] = node entry["tip"] = node.global_position + Vector3(-sin(yaw), 0.0, -cos(yaw)) * length out.append(entry) for marker in spawn_markers: if marker != null and is_instance_valid(marker): add.call(marker, {"kind": "spawn", "team": int(marker.get_meta("spawn_team", -1)), "id": str(marker.get_meta("spawn_id", ""))}) if zone_root != null and is_instance_valid(zone_root): for node in zone_root.get_children(): if node.has_meta("zone_id"): add.call(node, {"kind": "zone", "team": -1, "id": str(node.get_meta("zone_id"))}) return out ## Kreisfläche EINES Markers (neu) bauen — Werkzeuge rufen nach Radius-Änderung ## style_spawn_marker auf. U6 (t_639135dd): die Fläche bleibt IMMER gleich ## dezent (Teamfarbe, halbtransparent) — die Auswahl zeigt sich als schmaler ## Kontur-Ring am Zonenrand (Theme-Farbe), nicht als aufgehellte Vollfläche. 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) mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mi.material_override = mat return mi ## Dezenter Auswahl-Ring EINES Spawn-Markers (U6): dünner Torus am Zonenrand, ## Farbe aus dem Theme. Muss NUR bei Auswahl existieren (restlos weg bei ## Abwahl) — style_spawn_marker legt ihn an/entfernt ihn. func _spawn_ring(radius: float) -> MeshInstance3D: var mi := MeshInstance3D.new() var torus := TorusMesh.new() var t := maxf(0.15, radius * 0.06) torus.inner_radius = maxf(0.01, radius - t) torus.outer_radius = radius + t mi.mesh = torus mi.position.y = 0.26 var mat := StandardMaterial3D.new() mat.albedo_color = _markierung_farbe("selection_outline") mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED 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-Kontur: alter Ring weg, bei Auswahl neuer (restloser Wechsel). var alter_ring: Node = null if marker.has_meta("spawn_ring"): alter_ring = marker.get_meta("spawn_ring") if alter_ring != null and is_instance_valid(alter_ring): alter_ring.queue_free() marker.remove_meta("spawn_ring") if selected: var ring := _spawn_ring(radius) marker.add_child(ring) marker.set_meta("spawn_ring", ring) var arrow := yaw_arrow_of(marker) if arrow != null: # Länge folgt dem Radius var yaw_deg := float(arrow.get_meta("yaw_deg", 0.0)) var yaw_set := bool(arrow.get_meta("yaw_set", false)) arrow.name = "YawArrowAlt" arrow.queue_free() marker.add_child(_yaw_arrow(radius, yaw_deg, yaw_set, marker.get_meta("yaw_color", Color.WHITE))) ## 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) — Farbe aus # dem Theme statt hartkodiertem Gelb (U6). 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 = _markierung_farbe("selection_outline") if is_sel else col # ------------------------------------------------------ U6-Markierung ------- # (Karte t_639135dd — dezente Auswahl: Kontur statt Vollorange. Eine Quelle # für Farben/Breiten ist das Theme; der Shader ist die Außenhüllen-Technik.) ## Theme-Farben + -Breiten in die drei Kontur-Materialien überführen ## (Auswahl / Hover / Partner). Fehlende Einträge → Fallback-Farbe. func _markierung_theme_laden() -> void: _outline_mat_sel = _outline_material( _markierung_farbe("selection_outline"), _markierung_breite("selection_outline_width_mm"), 0.0) _outline_mat_hover = _outline_material( _markierung_farbe("selection_outline_hover"), _markierung_breite("selection_outline_width_hover_mm"), 0.0) _outline_mat_partner = _outline_material( _markierung_farbe("selection_outline_partner"), _markierung_breite("selection_outline_width_partner_mm"), 3.5) ## Eine Markierungs-Farbe aus dem Theme lesen (fehlt sie → Fallback). func _markierung_farbe(name: String) -> Color: var theme_res: Variant = load(THEME_PATH) if theme_res is Theme: var th := theme_res as Theme if th.has_color(name, "Editor"): return th.get_color(name, "Editor") return OUTLINE_FALLBACK ## Eine Markierungs-Breite aus dem Theme lesen (Theme speichert Millimeter ## als Ganzzahl — Theme kennt keine Gleitkommakonstanten). func _markierung_breite(name: String) -> float: var theme_res: Variant = load(THEME_PATH) if theme_res is Theme: var th := theme_res as Theme if th.has_constant(name, "Editor"): return float(th.get_constant(name, "Editor")) / 1000.0 return 0.06 ## Kontur-Material für einen Zustand bauen (streifen > 0 = gestrichelt). func _outline_material(farbe: Color, breite: float, streifen: float) -> ShaderMaterial: var mat := ShaderMaterial.new() var shader: Variant = load(OUTLINE_SHADER) if shader is Shader: mat.shader = shader mat.set_shader_parameter("linien_farbe", farbe) mat.set_shader_parameter("breite_m", breite) mat.set_shader_parameter("streifen_hz", streifen) return mat ## Zustand eines Objekts (U6-Abstufung): Auswahl > Partner > Hover > nichts. func _markierung_zustand(idx: int) -> String: if _is_selected(idx): return "sel" if _is_partner(idx): return "partner" if idx >= 0 and idx == hover_index: return "hover" return "" ## Kontur-Hülle eines Objekt-Markers anlegen/umschalten/restlos entfernen. ## Die Hülle hängt als Kind UNTER dem Mesh (erbt Position + Scale — GLB- ## Skalierung bleibt korrekt) und trägt das Mesh ein zweites Mal mit dem ## Außenhüllen-Material (visuell = zweite Render-Passage um das Modell, ## das Grundmaterial mit seiner Textur bleibt unangetastet). func _outline_anwenden(marker: Node3D, idx: int) -> void: if marker == null or not is_instance_valid(marker): return var zustand := _markierung_zustand(idx) var alt: Node = null if marker.has_meta("outline"): alt = marker.get_meta("outline") if zustand.is_empty(): if alt != null and is_instance_valid(alt): alt.queue_free() if marker.has_meta("outline"): marker.remove_meta("outline") return var mat: ShaderMaterial = _outline_mat_sel if zustand == "hover": mat = _outline_mat_hover elif zustand == "partner": mat = _outline_mat_partner if alt != null and is_instance_valid(alt): (alt as MeshInstance3D).material_override = mat return var mi := marker.get_child(0) as MeshInstance3D if mi == null or mi.mesh == null: return var huelle := MeshInstance3D.new() huelle.name = "SelectionOutline" huelle.mesh = mi.mesh huelle.material_override = mat mi.add_child(huelle) marker.set_meta("outline", huelle) ## Hover-Markierung (U6-Abstufung, schwächste Kontur). idx = obj_index oder ## -1 für „kein Hover“. Der Aufrufer ist das Objekt-Werkzeug — der Anschluss ## an die Mausbewegung liegt in interaction.gd (Grenze dieser Karte) und ## kommt als Folge-Karte; die Abstufung selbst ist hier voll wirksam. func set_hover(idx: int) -> void: if hover_index == idx: return hover_index = idx for marker in object_markers: if marker == null or not is_instance_valid(marker): continue _outline_anwenden(marker, int(marker.get_meta("obj_index", -1)))