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/phase3_manifest.json"): _icons[str(entry.id)] = str(entry.resource_path) _icons["worker"] = _icons.get("worker_funkenleser", "") 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) 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_")): 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: surface = 1 elif "Cloth" in ab_key or "Hood" 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)