saumwacht-godot/tools/verify_phase3_imports.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

140 lines
5.5 KiB
GDScript

extends SceneTree
## Read-only verification after Godot has imported the Phase 3 assets.
## Run: godot --headless --path . --script res://tools/verify_phase3_imports.gd
const MANIFEST_PATH: String = "res://assets/phase3_manifest.json"
const EXPECTED_ASSET_COUNT: int = 66
const ALPHA_TOLERANCE: float = 0.02
var _errors: Array[String] = []
var _checked: int = 0
var _loaded: int = 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
if not FileAccess.file_exists(MANIFEST_PATH):
_fail("manifest", "File is missing: " + MANIFEST_PATH)
_finish()
return
var file: FileAccess = FileAccess.open(MANIFEST_PATH, FileAccess.READ)
if file == null:
_fail("manifest", "Cannot open file: %s" % FileAccess.get_open_error())
_finish()
return
var parser: JSON = JSON.new()
var parse_error: Error = parser.parse(file.get_as_text())
file.close()
if parse_error != OK:
_fail("manifest", "JSON line %d: %s" % [parser.get_error_line(), parser.get_error_message()])
_finish()
return
var parsed: Variant = parser.data
if not parsed is Dictionary:
_fail("manifest", "Root must be an object.")
_finish()
return
var manifest: Dictionary = parsed
if manifest.get("phase") != 3:
_fail("manifest", "Expected phase 3.")
var entries_value: Variant = manifest.get("assets")
if not entries_value is Array:
_fail("manifest", "assets must be an array.")
_finish()
return
var entries: Array = entries_value
if entries.size() != EXPECTED_ASSET_COUNT:
_fail("manifest", "Expected %d assets, found %d." % [EXPECTED_ASSET_COUNT, entries.size()])
var seen_ids: Dictionary = {}
for index: int in range(entries.size()):
_checked += 1
var entry_value: Variant = entries[index]
if not entry_value is Dictionary:
_fail("entry_%d" % index, "Asset entry must be an object.")
continue
var entry: Dictionary = entry_value
var asset_id: String = str(entry.get("id", ""))
if asset_id.is_empty():
_fail("entry_%d" % index, "Asset ID is missing.")
continue
if seen_ids.has(asset_id):
_fail(asset_id, "Duplicate asset ID.")
seen_ids[asset_id] = true
_verify_asset(asset_id, entry)
_finish()
func _verify_asset(asset_id: String, entry: Dictionary) -> void:
for required_key: String in ["kind", "resource_path", "width", "height", "import_size_limit", "requires_alpha"]:
if not entry.has(required_key):
_fail(asset_id, "Missing manifest field: " + required_key)
return
var resource_path: String = str(entry["resource_path"])
var native_size: Vector2i = Vector2i(int(entry["width"]), int(entry["height"]))
var size_limit: int = int(entry["import_size_limit"])
if native_size.x <= 0 or native_size.y <= 0 or size_limit < 0:
_fail(asset_id, "Invalid native size or import limit.")
return
if not resource_path.begins_with("res://") or not ResourceLoader.exists(resource_path):
_fail(asset_id, "Resource is missing: " + resource_path)
return
var texture: Texture2D = ResourceLoader.load(resource_path) as Texture2D
if texture == null:
_fail(asset_id, "Resource did not load as Texture2D: " + resource_path)
return
_loaded += 1
var expected_size: Vector2i = _expected_size(native_size, size_limit)
var texture_size: Vector2i = Vector2i(texture.get_width(), texture.get_height())
if texture_size != expected_size:
_fail(asset_id, "Texture size %s; expected %s (native %s, limit %d)." % [texture_size, expected_size, native_size, size_limit])
var texture_image: Image = texture.get_image()
if texture_image == null or texture_image.is_empty():
_fail(asset_id, "Texture2D.get_image() returned no pixel data.")
return
if texture_image.get_size() != expected_size:
_fail(asset_id, "Image size %s; expected %s." % [texture_image.get_size(), expected_size])
if not bool(entry["requires_alpha"]):
return
if texture_image.is_compressed():
# Decompress the returned in-memory Image only; never save or reimport it.
var decompress_error: Error = texture_image.decompress()
if decompress_error != OK:
_fail(asset_id, "Cannot inspect compressed image alpha: %s" % decompress_error)
return
if texture_image.detect_alpha() == Image.ALPHA_NONE:
_fail(asset_id, "Image has no effective alpha transparency.")
var corner_alpha: float = texture_image.get_pixel(0, 0).a
if corner_alpha > ALPHA_TOLERANCE:
_fail(asset_id, "Corner alpha %.4f; expected at most %.4f." % [corner_alpha, ALPHA_TOLERANCE])
if str(entry["kind"]) == "ui_frame":
var center_x: int = floori(float(texture_image.get_width()) * 0.5)
var center_y: int = floori(float(texture_image.get_height()) * 0.5)
var center_alpha: float = texture_image.get_pixel(center_x, center_y).a
if center_alpha > ALPHA_TOLERANCE:
_fail(asset_id, "Panel center alpha %.4f; expected at most %.4f." % [center_alpha, ALPHA_TOLERANCE])
func _expected_size(native_size: Vector2i, size_limit: int) -> Vector2i:
if size_limit == 0:
return native_size
var longest_side: int = maxi(native_size.x, native_size.y)
var scale_factor: float = minf(1.0, float(size_limit) / float(longest_side))
return Vector2i(
maxi(1, roundi(float(native_size.x) * scale_factor)),
maxi(1, roundi(float(native_size.y) * scale_factor))
)
func _fail(asset_id: String, message: String) -> void:
_errors.append("[%s] %s" % [asset_id, message])
func _finish() -> void:
for message: String in _errors:
printerr("FAIL: " + message)
var result_label: String = "PASS" if _errors.is_empty() else "FAIL"
print("%s: Phase 3 imports: %d entries checked, %d/%d textures loaded, %d errors." % [result_label, _checked, _loaded, EXPECTED_ASSET_COUNT, _errors.size()])
quit(0 if _errors.is_empty() else 1)