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>
271 lines
12 KiB
GDScript
271 lines
12 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/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)
|