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>
281 lines
13 KiB
GDScript
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)
|