saumwacht-godot/scripts/presentation/asset_library.gd
Martin Stickelbroeck 9f62e30377 G2: Ancients, Türme, Bossgrotte und Superboss; macOS- und Windows-ARM64-Pakete
Mondherz-Ancients (Saumkern-Kristall als Herz, Sichelklingen, Ringe, Krone,
Teamwappen, Energieadern) mit den Stufen stabil/instabil/kritisch, kurzem
Zusammenbruch und bleibender Ruine; der Sieg wartet nie auf die Grafik.
Turmfamilie aus einem Blender-Generator: Grenzwacht (Tower 1), Bastion
(Tower 2), Kernwacht (Basisturm in der Formensprache des Ancients), mit
Schadensstufen, Einsturz, Ruine und sichtbarem Angriff (Zielring, Aufladen,
Bolzen, Einschlag) - der Schaden bleibt vollständig in der Simulation.
Bossgrotte Mondschlund mit Tor, Treppe in die Tiefe, Altar, Säulen, Felsen
und fünf Zuständen; der Generator hält Abstand zur Top-Lane, die Höhle
braucht eine kleine Geländesenke (nur Darstellung). Superboss Schlundkrone
mit 24 Knochen und 11 Clips (inkl. Brüllen und Sieg), Adern in Teamfarbe,
dasselbe Modell für Grotte und Lane. Gemeinsame LN-Materialfamilie (Stein,
Metall, Kristall, Rune, Stoff, Mondwasser, Kreaturenhaut), Occlusion-Fade
ohne dithernde Schatten. Keine Regeländerung.

Geprüft: verify_g2 245/245, verify_g1 443/443, Regeln 91/91 und 73/73,
Stress 17/17, Balance 40 Partien identisch mit G1, verify_m1 38/38,
verify_m2 14/14, Netz 43/43 und Loopback 12/12, Legacy 209 und 463.
21 Ansichten vorher/nachher (1920x1080), Videos Turmkampf und Bossbegegnung,
Leistung G1/G2 auf demselben Mac (+0,5 ms in vollen Kampfansichten).
Pakete 0.17.0-g2 für macOS (Universal, ad-hoc signiert), Windows x64/ARM64
und Linux; macOS nativ und unter Rosetta, beide Windows-Pakete in Windows 11
ARM (Parallels, OpenGL-Fallback) mit --lune-smoke bestanden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:12:49 +02:00

281 lines
13 KiB
GDScript

extends Node
## Resolve IDs once; keep source GLBs editable while batching static parts at runtime.
var _models: Dictionary = {}
var _icons: Dictionary = {}
var _meshes: Dictionary = {}
var _textures: Dictionary = {}
var _rigs: Dictionary = {}
var _surface_materials: Dictionary = {}
const LivingMaterial = preload("res://scripts/presentation/living_material.gdshader")
const ForgedMaterial = preload("res://scripts/presentation/forged_material.gdshader")
const PrismaticCore = preload("res://scripts/presentation/prismatic_core.gdshader")
const AnimatedFigure = preload("res://scripts/presentation/animated_figure.gd")
const SkeletalFigure = preload("res://scripts/presentation/skeletal_figure.gd")
const WorkerFigure = preload("res://scripts/presentation/worker_figure.gd")
const RingborkeMaterial = preload("res://scripts/presentation/ringborke.gdshader")
const ArbeiterMaterial = preload("res://scripts/presentation/arbeiter.gdshader")
var missing: Array[String] = []
func _ready() -> void:
for entry: Dictionary in _read_manifest("res://assets/phase4_manifest.json"):
_models[str(entry.id)] = entry
for entry: Dictionary in _read_manifest("res://assets/phase11_manifest.json"):
_models[str(entry.id)] = entry
for entry: Dictionary in _read_manifest("res://assets/phase12_manifest.json"):
_models[str(entry.id)] = entry
for entry: Dictionary in _read_manifest("res://assets/phase15_manifest.json"):
_models[str(entry.id)] = entry
for entry: Dictionary in _read_manifest("res://assets/phase16_manifest.json"):
_models[str(entry.id)] = entry
for entry: Dictionary in _read_manifest("res://assets/phase18_manifest.json"):
_models[str(entry.id)] = entry
for entry: Dictionary in _read_manifest("res://assets/phase19_manifest.json"):
_models[str(entry.id)] = entry
for entry: Dictionary in _read_manifest("res://assets/luneraria/peoples_manifest.json"):
_models[str(entry.id)] = entry
# G2: strategic buildings, grotto and the Schlundkrone (the only skeletal entry there).
for entry: Dictionary in _read_manifest("res://assets/luneraria/g2/g2_manifest.json"):
_models[str(entry.id)] = entry
for entry: Dictionary in _read_manifest("res://assets/phase3_manifest.json"):
_icons[str(entry.id)] = str(entry.resource_path)
_icons["worker"] = _icons.get("worker_funkenleser", "")
func has_model(id: String) -> bool:
return _models.has(id)
func _read_manifest(path: String) -> Array:
if not FileAccess.file_exists(path):
return []
var value: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
return value.get("assets", []) if value is Dictionary else []
func icon(id: String) -> Texture2D:
if not _textures.has(id):
var path: String = _icons.get(id, "")
_textures[id] = load(path) as Texture2D if not path.is_empty() and ResourceLoader.exists(path) else null
return _textures[id]
func model_height(id: String) -> float:
var record: Dictionary = _models.get(id, {})
var bounds: Dictionary = record.get("bounds_godot", {})
return float(bounds.get("max", [0, 1.6, 0])[1])
func instantiate_model(id: String) -> Node3D:
if not _models.has(id):
return null
if not _meshes.has(id):
var path: String = str(_models[id].resource_path)
if not ResourceLoader.exists(path):
if not missing.has(id):
missing.append(id)
push_warning("Missing visual asset: " + id)
return null
var packed: PackedScene = load(path) as PackedScene
if packed == null:
return null
var source: Node3D = packed.instantiate() as Node3D
var mesh: ArrayMesh = bake_mesh(source)
source.free()
_meshes[id] = mesh
var instance := MeshInstance3D.new()
instance.name = "Model_" + id
instance.mesh = _meshes[id]
instance.set_meta("asset_id", id)
return instance
func bake_mesh(source: Node3D) -> ArrayMesh:
var surfaces: Dictionary = {}
_collect(source, Transform3D.IDENTITY, surfaces)
var mesh := ArrayMesh.new()
for material: Material in surfaces:
var builder: SurfaceTool = surfaces[material]
builder.set_material(material)
builder.commit(mesh)
return mesh
func instantiate_fighter(id: String) -> Node3D:
if _models.get(id, {}).get("skeletal", false):
var scene := load(str(_models[id].resource_path)) as PackedScene
var source: Node3D = scene.instantiate()
decorate_model(source)
var info: Dictionary = _models[id].bounds_godot
var low := Vector3(info.min[0],info.min[1],info.min[2])
var high := Vector3(info.max[0],info.max[1],info.max[2])
var box := AABB(low,high-low)
if id == "worker":
var worker := WorkerFigure.new()
worker.setup(source, box)
return worker
var figure := SkeletalFigure.new()
figure.setup(source,box)
if _models[id].has("attack_cycle"):
figure.attack_cycle.assign(_models[id].attack_cycle)
if _models[id].has("stride"):
figure.stride = float(_models[id].stride)
return figure
if not (id.begins_with("g") or id.begins_with("e") or id.begins_with("w") or id.begins_with("core_team")) or not _models.has(id):
return instantiate_model(id)
if not _rigs.has(id):
var packed := load(str(_models[id].resource_path)) as PackedScene
if packed == null:
return instantiate_model(id)
var source := packed.instantiate() as Node3D
var groups: Dictionary = {}
_collect_rig(source, Transform3D.IDENTITY, id, groups)
var meshes: Dictionary = {}
var bounds := AABB()
var first: bool = true
for key: String in groups:
var mesh := ArrayMesh.new()
for material: Material in groups[key]:
var builder: SurfaceTool = groups[key][material]
builder.set_material(material)
builder.commit(mesh)
var box: AABB = mesh.get_aabb()
bounds = box if first else bounds.merge(box)
first = false
# Meshes are collected in model space. Move their vertices around joints.
var pivot: Vector3 = box.get_center()
if key.begins_with("Leg") or key.begins_with("Arm"):
pivot.y = box.end.y - box.size.y * .10
elif key == "Flame":
pivot.y = box.position.y
var centered := ArrayMesh.new()
for surface: int in range(mesh.get_surface_count()):
var builder := SurfaceTool.new()
builder.begin(Mesh.PRIMITIVE_TRIANGLES)
builder.append_from(mesh, surface, Transform3D(Basis.IDENTITY, -pivot))
builder.set_material(mesh.surface_get_material(surface))
builder.commit(centered)
meshes[key] = {"mesh": centered, "pivot": pivot}
source.free()
_rigs[id] = {"groups": meshes, "bounds": bounds}
var figure := AnimatedFigure.new()
figure.setup(id, _rigs[id].groups, _rigs[id].bounds)
return figure
func _collect_rig(node: Node3D, parent: Transform3D, id: String, groups: Dictionary) -> void:
var transform: Transform3D = parent * node.transform
if node is MeshInstance3D and node.mesh != null:
var part_name: String = str(node.name)
var key: String = "Body"
for prefix: String in ["LegL", "LegR", "ArmL", "ArmR", "Weapon"]:
if part_name.begins_with(prefix):
key = prefix
if part_name.begins_with("Leg"):
key = part_name.get_slice("_", 0)
if part_name.begins_with("WeaponL_"):
key = "ArmL"
elif part_name.begins_with("WeaponR_"):
key = "ArmR"
if part_name.begins_with("Core_Crystal"):
key = "Crystal"
if id.begins_with("w") and part_name.begins_with("Head_"):
key = "Weapon"
if id == "g05" and key.begins_with("Arm"):
key = "Weapon"
if id in ["g07", "g08"] and (part_name.begins_with("Body_Mortar") or part_name.begins_with("Body_Rim") or key == "Weapon"):
key = "Mortar"
if part_name.begins_with("Weapon_Flame"):
key = "Flame"
if not groups.has(key):
groups[key] = {}
for index: int in range(node.mesh.get_surface_count()):
var material: Material = presentation_material(node.get_active_material(index))
if not groups[key].has(material):
var builder := SurfaceTool.new()
builder.begin(Mesh.PRIMITIVE_TRIANGLES)
groups[key][material] = builder
(groups[key][material] as SurfaceTool).append_from(node.mesh, index, transform)
for child: Node in node.get_children():
if child is Node3D:
_collect_rig(child, transform, id, groups)
func _collect(node: Node3D, parent_transform: Transform3D, surfaces: Dictionary) -> void:
var transform: Transform3D = parent_transform * node.transform
if node is MeshInstance3D and node.mesh != null:
for index: int in range(node.mesh.get_surface_count()):
var material: Material = presentation_material(node.get_active_material(index))
if not surfaces.has(material):
var builder := SurfaceTool.new()
builder.begin(Mesh.PRIMITIVE_TRIANGLES)
surfaces[material] = builder
(surfaces[material] as SurfaceTool).append_from(node.mesh, index, transform)
for child: Node in node.get_children():
if child is Node3D:
_collect(child, transform, surfaces)
func presentation_material(source: Material) -> Material:
if source is StandardMaterial3D and (source.resource_name.begins_with("AB_") or source.resource_name.begins_with("BB_") or source.resource_name.begins_with("QG_") or source.resource_name.begins_with("FL_") or source.resource_name.begins_with("GB_") or source.resource_name.begins_with("LP_")):
var ab_key: String = source.resource_name
if not _surface_materials.has(ab_key):
if source.emission_enabled or ab_key in ["AB_Lamp", "AB_Spark", "AB_Glass", "AB_Lens", "BB_Magma", "BB_Ember", "QG_Spring", "FL_Ember", "FL_Crystal", "GB_Crystal", "GB_Ember"]:
_surface_materials[ab_key] = source.duplicate()
else:
var ab := ShaderMaterial.new()
ab.shader = ArbeiterMaterial
ab.set_shader_parameter("roughness_value", source.roughness)
ab.set_shader_parameter("metallic_value", source.metallic)
var surface: int = 5 if source.metallic > .5 else 0
if "Leather" in ab_key or "Boot" in ab_key or "Glove" in ab_key or ab_key.begins_with("LP_") and ab_key.ends_with("_Skin"):
surface = 1
elif "Cloth" in ab_key or "Hood" in ab_key or "LP_" in ab_key and ("Fur" in ab_key or "Hair" in ab_key):
surface = 2
elif "Bark" in ab_key or "Wood" in ab_key or "Vine" in ab_key:
surface = 3
elif "Leaf" in ab_key or "Moss" in ab_key or "Petal" in ab_key:
surface = 4
ab.set_shader_parameter("surface_kind", surface)
ab.set_shader_parameter("grain_amount", .12 if surface in [0, 3] else .065)
ab.set_shader_parameter("grain_scale", 72.0 if surface in [0, 3] else 140.0)
ab.set_shader_parameter("relief_depth", .0009 if surface == 0 else (.0006 if surface == 3 else .00015))
ab.resource_name = ab_key
_surface_materials[ab_key] = ab
return _surface_materials[ab_key]
if source is StandardMaterial3D and source.resource_name.begins_with("RB_") and source.resource_name != "RB_Cutwood" and not source.emission_enabled:
var key: String = source.resource_name
if not _surface_materials.has(key):
var material := ShaderMaterial.new()
material.shader = RingborkeMaterial
material.set_shader_parameter("wood_height",preload("res://assets/materials/phase10/oak-height-4k.png"))
material.set_shader_parameter("bark",key in ["RB_Bark","RB_Heartwood"])
material.set_shader_parameter("endgrain",key == "RB_Cutwood")
material.set_shader_parameter("leaf",key in ["RB_Leaf","RB_Moss","RB_Petal"])
_surface_materials[key] = material
return _surface_materials[key]
if source is StandardMaterial3D and source.resource_name.begins_with("SW_HD_Core_"):
var crystal_key: String = source.resource_name
if not _surface_materials.has(crystal_key):
var crystal := ShaderMaterial.new()
crystal.shader = PrismaticCore
crystal.set_shader_parameter("base_color", source.albedo_color)
_surface_materials[crystal_key] = crystal
return _surface_materials[crystal_key]
if not source is StandardMaterial3D or not source.resource_name.begins_with("SW_HD_") or source.emission_enabled:
return source
var key: String = source.resource_name
if not _surface_materials.has(key):
var material := ShaderMaterial.new()
material.shader = LivingMaterial if key.begins_with("SW_HD_W_") else ForgedMaterial
if key.begins_with("SW_HD_W_"):
material.set_shader_parameter("bark", "bark" in key or "wood" in key or "groove" in key)
material.set_shader_parameter("endgrain", "endgrain" in key)
material.set_shader_parameter("moss", "moss" in key)
material.set_shader_parameter("oak_color", load("res://assets/materials/phase10/oak-albedo-4k.png"))
material.set_shader_parameter("oak_height", load("res://assets/materials/phase10/oak-height-4k.png"))
material.set_shader_parameter("rock_map", preload("res://assets/materials/phase10/limestone-albedo.png"))
material.resource_name = key
material.set_shader_parameter("base_color", source.albedo_color)
material.set_shader_parameter("base_roughness", source.roughness)
material.set_shader_parameter("metalness", source.metallic)
material.set_shader_parameter("grain_amount", .17 if "basalt" in key or "edge" in key else .07)
_surface_materials[key] = material
return _surface_materials[key]
func decorate_model(root: Node) -> void:
if root is MeshInstance3D and root.mesh != null:
for index: int in range(root.mesh.get_surface_count()):
root.set_surface_override_material(index, presentation_material(root.get_active_material(index)))
for child: Node in root.get_children():
decorate_model(child)