Atmosphaere & Skybox (t_f72913d4): Map-Option „Himmel: Pack-Skybox“ (weather_sky=sky_panorama, checksum-neutral), Vorschau = 1:1-Spiegel Client-Wetter (ACES/Fog/sun_deg/Panorama via Nachbar-Klon), V17-Validator; Suite test_skybox 23/23

This commit is contained in:
Hermes (scopedvr-dev) 2026-10-20 22:10:36 +00:00
parent 35a1d80d3e
commit 0505463ace
5 changed files with 325 additions and 19 deletions

View file

@ -28,6 +28,11 @@ const SUPPORTED_FORMAT_VERSIONS: Array[int] = [1, 2]
## Material-IDs (Index = ID). 0 = Gras = Default für Maps ohne Schicht. ## Material-IDs (Index = ID). 0 = Gras = Default für Maps ohne Schicht.
const MATERIAL_IDS: Array[String] = ["gras", "sand", "fels"] const MATERIAL_IDS: Array[String] = ["gras", "sand", "fels"]
const WEATHER_PRESETS: Array[String] = ["mittag", "abend", "regen", "nacht"] const WEATHER_PRESETS: Array[String] = ["mittag", "abend", "regen", "nacht"]
## Skybox-Option (Karte t_f72913d4): Werte des optionalen Feldes `weather_sky`
## — identische Semantik wie der Client (Weather.apply_preset sky_override):
## "sky_panorama" = Pack-Panorama erzwingen, "prozedural" = ProceduralSky
## erzwingen, leer/fehlend = Preset entscheidet (in allen Presets aus).
const WEATHER_SKY_OPTIONS: Array[String] = ["sky_panorama", "prozedural"]
# Perf-/Plausibilitäts-Limits — identisch zum Server-Loader (docs/map_format.md §4). # Perf-/Plausibilitäts-Limits — identisch zum Server-Loader (docs/map_format.md §4).
const MAX_FILE_BYTES := 50 * 1024 * 1024 # V1 const MAX_FILE_BYTES := 50 * 1024 * 1024 # V1
@ -171,6 +176,15 @@ static func _validate_core(data: Dictionary, manifest: Dictionary = {}) -> Packe
if not (weather is String) or not WEATHER_PRESETS.has(weather): if not (weather is String) or not WEATHER_PRESETS.has(weather):
errors.append("V12: weather '%s' ist kein gültiges Preset (%s)." % [ errors.append("V12: weather '%s' ist kein gültiges Preset (%s)." % [
str(weather), ", ".join(WEATHER_PRESETS)]) str(weather), ", ".join(WEATHER_PRESETS)])
# V17: weather_sky (optional, Karte t_f72913d4) — leer/fehlend = Standard.
# Checksum-neutral wie tint/gebaeude: der Server kennt das Feld nicht, die
# Prüfsumme läuft über die ganze Payload — unbekannte Zusatzfelder stören
# die Server-Validierung nicht, nur WERT-Fehler fangen wir hier ab.
var weather_sky = data.get("weather_sky", "")
if weather_sky != null and str(weather_sky) != "":
if not (weather_sky is String) or not WEATHER_SKY_OPTIONS.has(weather_sky):
errors.append("V17: weather_sky '%s' ist keine gültige Option (%s)." % [
str(weather_sky), ", ".join(WEATHER_SKY_OPTIONS)])
return errors return errors

View file

@ -10,6 +10,7 @@ var prop_author: LineEdit
var prop_desc: LineEdit var prop_desc: LineEdit
var prop_version: SpinBox var prop_version: SpinBox
var prop_weather: OptionButton var prop_weather: OptionButton
var prop_sky: OptionButton
var terrain_info: Label var terrain_info: Label
var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder var _loading_ui_values := false # verhindert dirty beim Auffüllen der Felder
@ -57,6 +58,14 @@ func build() -> void:
prop_weather.add_item("Nacht (dunkel)") prop_weather.add_item("Nacht (dunkel)")
add_child(prop_weather) add_child(prop_weather)
add_child(_field_label("Himmel"))
prop_sky = OptionButton.new()
# Karte Atmosphäre/Skybox: Panorama-Skybox aus dem Pack als Map-Option.
prop_sky.tooltip_text = "Himmel: Prozedural (Standard) oder Pack-Skybox (Wüstenpanorama). „Standard“ lässt die Karte unverändert."
prop_sky.add_item("Prozedural (Standard)")
prop_sky.add_item("Pack-Skybox (Wüste)")
add_child(prop_sky)
add_child(_separator()) add_child(_separator())
add_child(_header_label("Terrain")) add_child(_header_label("Terrain"))
terrain_info = Label.new() terrain_info = Label.new()
@ -70,6 +79,7 @@ func build() -> void:
prop_desc.text_changed.connect(_on_prop_changed.bind("description")) prop_desc.text_changed.connect(_on_prop_changed.bind("description"))
prop_version.value_changed.connect(_on_version_changed) prop_version.value_changed.connect(_on_version_changed)
prop_weather.item_selected.connect(_on_weather_changed) prop_weather.item_selected.connect(_on_weather_changed)
prop_sky.item_selected.connect(_on_sky_changed)
func _on_prop_changed(text: String, field: String) -> void: func _on_prop_changed(text: String, field: String) -> void:
@ -108,6 +118,25 @@ func _on_weather_changed(index: int) -> void:
main.call("_set_status", "Wetter geändert auf „%s“." % weathers[index]) main.call("_set_status", "Wetter geändert auf „%s“." % weathers[index])
## Karte Atmosphäre/Skybox: Himmel „Pack-Skybox (Wüste)“ → doc["weather_sky"]
## = "sky_panorama" (ein Speicherfeld, checksum-neutral wie tint/gebaeude;
## Wert = der Preset-Key `sky_panorama`, den der Client beim Laden erzwingt).
## „Prozedural (Standard)“ = Index 0 → Feld WIEDER ENTFERNEN (alte Maps
## unverändert — kein "prozedural"-Wert im doc). Sofort-Neuaufbau wie Wetter.
func _on_sky_changed(index: int) -> void:
if _loading_ui_values or main.doc.is_empty():
return
if index == 1:
main.doc["weather_sky"] = "sky_panorama"
else:
main.doc.erase("weather_sky")
main.dirty = true
main.call("_refresh_file_label")
main.view_builder.call("rebuild", main.doc, main.manifest)
main.call("_sync_grid_to_builder")
main.call("_set_status", "Himmel geändert auf „%s“." % ["Prozedural (Standard)", "Pack-Skybox (Wüste)"][index])
## Felder aus dem doc auffüllen (aus main.gd _refresh_all-Anfang ausgezogen). ## Felder aus dem doc auffüllen (aus main.gd _refresh_all-Anfang ausgezogen).
func refresh_values() -> void: func refresh_values() -> void:
_loading_ui_values = true _loading_ui_values = true
@ -119,6 +148,8 @@ func refresh_values() -> void:
var weathers := ["mittag", "abend", "regen", "nacht"] var weathers := ["mittag", "abend", "regen", "nacht"]
var weather := str(main.doc.get("weather", "mittag")) var weather := str(main.doc.get("weather", "mittag"))
prop_weather.select(maxi(0, weathers.find(weather))) prop_weather.select(maxi(0, weathers.find(weather)))
# Himmel: Pack-Skybox ausgewählt ↔ doc["weather_sky"] == "sky_panorama".
prop_sky.select(1 if str(main.doc.get("weather_sky", "")) == "sky_panorama" else 0)
var terrain: Dictionary = main.doc.get("terrain", {}) var terrain: Dictionary = main.doc.get("terrain", {})
terrain_info.text = "Größe: %.0f × %.0f m\nAuflösung: %d × %d Höhenpunkte\nHöhenbereich: %.0f bis %.0f m" % [ terrain_info.text = "Größe: %.0f × %.0f m\nAuflösung: %d × %d Höhenpunkte\nHöhenbereich: %.0f bis %.0f m" % [

View file

@ -21,14 +21,70 @@ const OUTLINE_SHADER := "res://shaders/selection_outline.gdshader"
# Fallback, falls das Theme (noch) nicht geladen werden kann. # Fallback, falls das Theme (noch) nicht geladen werden kann.
const OUTLINE_FALLBACK := Color(1.0, 0.6, 0.15, 0.95) const OUTLINE_FALLBACK := Color(1.0, 0.6, 0.15, 0.95)
# Wetter → Licht (grobe Stimmung; Feintuning kommt mit dem Wetter-Konsum) # 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 := { 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)}, "mittag": {
"abend": {"energy": 0.9, "color": Color(1.0, 0.75, 0.5), "ambient": 0.3, "sky_top": Color(0.35, 0.3, 0.55)}, "sky_top": Color(0.30, 0.54, 0.86), "sky_horizon": Color(0.72, 0.83, 0.92),
"regen": {"energy": 0.6, "color": Color(0.8, 0.85, 0.9), "ambient": 0.35, "sky_top": Color(0.45, 0.48, 0.52)}, "ground_bottom": Color(0.20, 0.23, 0.27), "ground_horizon": Color(0.66, 0.75, 0.83),
"nacht": {"energy": 0.25, "color": Color(0.6, 0.7, 1.0), "ambient": 0.12, "sky_top": Color(0.03, 0.05, 0.12)}, "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_index := -1 # aktuelles Auswahl-Objekt (Kontur-Markierung)
var selected_indices: Array = [] # komplette Auswahl (Karte t_f6f29d86 — Mehrfachauswahl) var selected_indices: Array = [] # komplette Auswahl (Karte t_f6f29d86 — Mehrfachauswahl)
var hover_index := -1 # Hover-Objekt (schwächste Kontur; U6-Abstufung) var hover_index := -1 # Hover-Objekt (schwächste Kontur; U6-Abstufung)
@ -84,8 +140,11 @@ func rebuild(data: Dictionary, manifest: Dictionary) -> void:
var size_z := float(terrain.get("size_z_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 ground_y := float(terrain.get("min_height_m", 0.0))
var weather := str(data.get("weather", "mittag")) 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) _build_environment(weather, weather_sky)
_build_terrain(data) _build_terrain(data)
_build_grid(size_x, size_z, ground_y) _build_grid(size_x, size_z, ground_y)
# Gitter default AUS über sichtbarem Terrain (Karte t_7ac70014): das # Gitter default AUS über sichtbarem Terrain (Karte t_7ac70014): das
@ -125,29 +184,70 @@ func rebuild_terrain_from_doc(data: Dictionary) -> void:
terrain_view.call("rebuild_from_doc", data) terrain_view.call("rebuild_from_doc", data)
func _build_environment(weather: String) -> void: ## 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 preset: Dictionary = WEATHER_LIGHT.get(weather, WEATHER_LIGHT["mittag"])
var env := Environment.new() var env := Environment.new()
var sky := Sky.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() var sky_mat := ProceduralSkyMaterial.new()
sky_mat.sky_top_color = preset["sky_top"] sky_mat.sky_top_color = preset["sky_top"]
sky_mat.sky_horizon_color = Color(0.6, 0.65, 0.7) sky_mat.sky_horizon_color = preset["sky_horizon"]
sky_mat.ground_bottom_color = Color(0.1, 0.1, 0.12) sky_mat.ground_bottom_color = preset["ground_bottom"]
sky_mat.ground_horizon_color = Color(0.4, 0.42, 0.45) sky_mat.ground_horizon_color = preset["ground_horizon"]
sky_mat.sky_energy_multiplier = float(preset["sky_energy"])
sky.sky_material = sky_mat sky.sky_material = sky_mat
env.background_mode = Environment.BG_SKY env.background_mode = Environment.BG_SKY
env.sky = 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_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color(1, 1, 1) env.ambient_light_color = preset["ambient_color"]
env.ambient_light_energy = preset["ambient"] 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() var we := WorldEnvironment.new()
we.environment = env we.environment = env
add_child(we) add_child(we)
var sun := DirectionalLight3D.new() var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-50.0, 30.0, 0.0) var deg: Vector2 = preset["sun_deg"]
sun.light_energy = preset["energy"] sun.rotation_degrees = Vector3(deg.x, deg.y, 0.0)
sun.light_color = preset["color"] sun.light_energy = float(preset["sun_energy"])
sun.light_color = preset["sun_color"]
sun.shadow_enabled = true sun.shadow_enabled = true
add_child(sun) add_child(sun)

160
tests/test_skybox.gd Normal file
View file

@ -0,0 +1,160 @@
extends SceneTree
## Editor-Test (headless): Karte t_f72913d4 — Atmosphäre & Skybox.
## Ausführen: godot --headless --path . --script res://tests/test_skybox.gd
## Exit 0 = alles grün.
##
## T1 svmap-Validierung (V17): weather_sky optional; "sky_panorama"/
## "prozedural" gültig, Quatsch/nicht-String → V17-Fehler; alte Karte
## ohne Feld validiert unverändert.
## T2 Map-Panel „Himmel“: doc-Roundtrip (schreiben = sky_panorama,
## „Standard“ = Feld ENTFERNEN, Auffüllen aus dem doc).
## T3 Editor-Vorschau (map_view_builder): weather_sky=sky_panorama →
## PanoramaSkyMaterial aus dem Client-Nachbar-Klon + ACES-Tonemap +
## Fog am Skybox-Horizont; ohne Feld → ProceduralSky (alte Maps
## unverändert).
var failures := 0
var checks := 0
var main
var map_panel
func _initialize() -> void:
_run_all.call_deferred()
func _check(cond: bool, label: String) -> void:
checks += 1
print("%s: %s" % ["ok " if cond else "FAIL", label])
if not cond:
failures += 1
func _run_all() -> void:
var scene: PackedScene = load("res://scenes/main.tscn")
_check(scene != null, "scenes/main.tscn laedt")
if scene == null:
_finish()
return
main = scene.instantiate()
root.add_child(main)
await process_frame
await process_frame
map_panel = main.get("map_mod")
_check(map_panel != null, "Map-Panel ist in main eingebaut")
if map_panel == null:
_finish()
return
await _t1_validierung()
await _t2_panel()
await _t3_vorschau()
_finish()
func _t1_validierung() -> void:
print("== T1: svmap-Validierung weather_sky ==")
# Alte Karte ohne Feld: unverändert gültig.
var alte: Dictionary = main.doc.duplicate(true)
alte.erase("weather_sky")
SvmapIO.sign_map(alte)
var f0: PackedStringArray = SvmapIO.validate_map(alte)
_check(f0.is_empty(), "T1 Karte ohne weather_sky validiert ohne Fehler (%s)" % str(f0))
# Gültige Werte (mit frischer Prüfsumme — Feld fließt in die Payload).
for wert in ["sky_panorama", "prozedural"]:
var d: Dictionary = main.doc.duplicate(true)
d["weather_sky"] = wert
SvmapIO.sign_map(d)
var f: PackedStringArray = SvmapIO.validate_map(d)
_check(f.is_empty(), "T1 weather_sky='%s' validiert (%s)" % [wert, str(f)])
# Ungültige Werte → V17 (_validate_core ohne Prüfsumme, wie test_symmetry_core).
for wert in ["panorama", 42]:
var d2: Dictionary = main.doc.duplicate(true)
d2["weather_sky"] = wert
var f2: PackedStringArray = SvmapIO._validate_core(d2)
var hat_v17 := false
for e in f2:
if str(e).begins_with("V17"):
hat_v17 = true
_check(hat_v17, "T1 weather_sky=%s → V17-Fehler (%s)" % [str(wert), str(f2)])
func _t2_panel() -> void:
print("== T2: Map-Panel Himmel-Option ==")
var prop_sky: OptionButton = map_panel.get("prop_sky")
_check(prop_sky != null, "T2 prop_sky existiert")
if prop_sky == null:
return
_check(prop_sky.item_count == 2, "T2 Himmel-Optionen: 2 Einträge (ist %d)" % prop_sky.item_count)
_check(str(prop_sky.get_item_text(1)).contains("Pack-Skybox"),
"T2 Eintrag 2 = Pack-Skybox (ist „%s“)" % str(prop_sky.get_item_text(1)))
# doc mit Panorama → UI zeigt Pack-Skybox.
main.doc["weather_sky"] = "sky_panorama"
map_panel.call("refresh_values")
_check(prop_sky.selected == 1, "T2 doc=sky_panorama → UI Pack-Skybox (ist %d)" % prop_sky.selected)
# „Prozedural (Standard)“ → Feld ENTFERNEN (kein "prozedural"-Wert im doc).
map_panel.call("_on_sky_changed", 0)
_check(not main.doc.has("weather_sky"), "T2 Standard entfernt das Feld")
_check(bool(main.get("dirty")), "T2 Änderung markiert dirty")
# „Pack-Skybox“ → Feld = sky_panorama (Client-Vertrag, V17-Wert).
map_panel.call("_on_sky_changed", 1)
_check(str(main.doc.get("weather_sky", "")) == "sky_panorama",
"T2 Pack-Skybox schreibt weather_sky=sky_panorama")
# UI-Zustand nach externer doc-Änderung korrekt auffüllen.
map_panel.call("refresh_values")
_check(prop_sky.selected == 1, "T2 refresh_values liest das Feld (Index 1)")
# Für T3-Baseline wieder auf Standard.
map_panel.call("_on_sky_changed", 0)
_check(not main.doc.has("weather_sky"), "T2 Aufräumen: Feld entfernt")
func _t3_vorschau() -> void:
print("== T3: Editor-Vorschau (map_view_builder) ==")
var vb = main.view_builder
_check(vb != null, "T3 view_builder vorhanden")
if vb == null:
return
var bscript: Script = vb.get_script()
var sky_pfad: String = bscript.panorama_sky_pfad()
_check(not sky_pfad.is_empty(), "T3 Client-Nachbar-Klon gefunden (%s)" % sky_pfad)
if sky_pfad.is_empty() or not FileAccess.file_exists(sky_pfad):
print(" (Pack-Skybox im Nachbar-Klon nicht erreichbar — Panorama-Checks übersprungen)")
return
main.doc["weather_sky"] = "sky_panorama"
vb.call("rebuild", main.doc, main.manifest)
await process_frame
var we := _welt_env(vb)
_check(we != null, "T3 WorldEnvironment vorhanden")
if we != null and we.environment != null and we.environment.sky != null:
_check(we.environment.sky.sky_material is PanoramaSkyMaterial,
"T3 weather_sky=sky_panorama → PanoramaSkyMaterial in der Vorschau")
_check(we.environment.tonemap_mode == Environment.TONE_MAPPER_ACES,
"T3 Vorschau tonemappt ACES")
var wl: Dictionary = bscript.get_script_constant_map().get("WEATHER_LIGHT", {})
var preset: Dictionary = wl.get(str(main.doc.get("weather", "mittag")), {})
if preset.has("panorama_fog_color"):
_check((we.environment.fog_light_color as Color).is_equal_approx(
preset["panorama_fog_color"]),
"T3 Fog am Skybox-Horizont (panorama_fog_color)")
# Feld entfernen → Procedural (alte Maps laden unverändert).
main.doc.erase("weather_sky")
vb.call("rebuild", main.doc, main.manifest)
await process_frame
var we2 := _welt_env(vb)
_check(we2 != null and we2.environment != null and we2.environment.sky != null
and we2.environment.sky.sky_material is ProceduralSkyMaterial,
"T3 ohne weather_sky → ProceduralSkyMaterial (alte Maps unverändert)")
func _welt_env(vb) -> WorldEnvironment:
for child in vb.get_children():
if child is WorldEnvironment:
return child
return null
func _finish() -> void:
print("--------------------------------------------------")
print("CHECKS: %d FAILURES: %d" % [checks, failures])
quit(0 if failures == 0 else 1)

1
tests/test_skybox.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://caxg45x2wi8w5