Sly 25.10.: „weich“ soll ein Textur-Verlauf am Pinselrand sein, nicht fleckig; gerade Strichkanten wirkten weiter hart (1-m-Übergang). - Format (optional, alte Karten gültig): materials_b_r8_b64 (zweites Material je Punkt) + materials_w_r8_b64 (Anteil 0–255); ohne Mischung fallen die Felder weg. V15 prüft Länge/IDs/Paarigkeit. - TerrainField.apply_material_brush_mix: innen voll, außen Smoothstep-Anteil; nur verstärken, nie abschwächen; Gras drübermalen mischt zurück. - Shader: Top-2 aus 4 Nachbarpunkten × (a, b) mit Anteilen → nur noch 2 Albedo-Fetches; Warp bleibt. - Pinsel/Live-Vorschau, Spiegeln, Symmetrie-Analyse/-Anwenden, Resize, Anheben, Undo (terrain-Snapshot) tragen die Misch-Schicht mit. - Streu-Rand (vorheriger Commit) entfernt. - Tests: test_mal_fein 30/30 (Verlauf fällt weich, nie abschwächen, zurückmischen, Kodierung), Editor-Suite unverändert grün bis auf die bekannten Umgebungs-Tests; Kette Editor → Server → Client per run_map_download_e2e 15/15 mit gemischter Karte. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
807 lines
No EOL
34 KiB
GDScript
807 lines
No EOL
34 KiB
GDScript
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://theme/editor_theme.tres"
|
||
const OUTLINE_SHADER := "res://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 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 # Kontur-Mesh um das gewählte Bauteil
|
||
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 = []
|
||
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
|
||
# 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)
|
||
_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)
|
||
|
||
|
||
## 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()
|
||
var panorama := false
|
||
if sky_override == "prozedural":
|
||
panorama = false
|
||
elif sky_override == "sky_panorama":
|
||
panorama = true
|
||
else:
|
||
panorama = bool(preset.get("sky_panorama", false))
|
||
if panorama:
|
||
var pfad := panorama_sky_pfad()
|
||
if not pfad.is_empty() and FileAccess.file_exists(pfad):
|
||
var img := Image.load_from_file(pfad)
|
||
if img != null:
|
||
if img.is_compressed():
|
||
img.decompress()
|
||
img.generate_mipmaps()
|
||
var pano_mat := PanoramaSkyMaterial.new()
|
||
pano_mat.panorama = ImageTexture.create_from_image(img)
|
||
pano_mat.energy_multiplier = float(preset.get("panorama_energy", 1.0))
|
||
sky.sky_material = pano_mat
|
||
else:
|
||
push_warning("MAPBUILDER: Panorama-Textur nicht ladbar (%s) — Fallback ProceduralSky" % pfad)
|
||
else:
|
||
push_warning("MAPBUILDER: weather_sky=sky_panorama, aber Pack-Skybox fehlt (%s) — Fallback ProceduralSky" % pfad)
|
||
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 = preset.get("panorama_fog_color", preset["fog_color"]) 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 = 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. 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)
|
||
|
||
|
||
## 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
|
||
|
||
|
||
# ------------------------------------------------------------ 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 (U6, t_639135dd): dezente Außenhüllen-Kontur um das
|
||
## Bauteil-Mesh — gleiche Technik wie bei den Objekten (selection_outline-
|
||
## Shader, Theme-Farbe). Keine halbtransparente Vollbox mehr, die das Teil
|
||
## verdeckt.
|
||
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 mesh := BuildOps.piece_mesh(list[idx])
|
||
if mesh == null:
|
||
return
|
||
# Kontur in GLEICHER Transform wie das echte Bauteil-Mesh (aabb ist
|
||
# welt-transformiert — nur als Größen-Guard; das Mesh selbst sitzt am
|
||
# Teil-Anker mit rot).
|
||
var kontur := MeshInstance3D.new()
|
||
kontur.mesh = mesh
|
||
var pos: Array = list[idx].get("pos", [0.0, 0.0, 0.0])
|
||
kontur.position = Vector3(float(pos[0]), float(pos[1]), float(pos[2]))
|
||
kontur.rotation.y = deg_to_rad(float(list[idx].get("rot", 0.0)))
|
||
kontur.material_override = _outline_mat_sel
|
||
add_child(kontur)
|
||
_gebaeude_sel = kontur
|
||
|
||
|
||
# ------------------------------------------------------ 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))) |