saumwacht-godot/scripts/luneraria/view/lune_materials.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

146 lines
7.6 KiB
GDScript

extends RefCounted
## Shared material library of Champions of Luneraria (first set in G2; meant for G4/G5 too).
## GLBs from the G2 Blender generators name their surfaces LN_*; `dress` swaps them for one
## shared ShaderMaterial per name. Everything that differs per building or per team (team
## colours, energy, damage, occlusion fade, rune family, banner silhouette) is a per-instance
## shader parameter with a fixed slot (see the instance_index hints in the shaders), so all
## buildings share the same few materials.
const StoneShader = preload("res://scripts/luneraria/view/materials/lune_stone.gdshader")
const MetalShader = preload("res://scripts/luneraria/view/materials/lune_metal.gdshader")
const CrystalShader = preload("res://scripts/luneraria/view/materials/lune_crystal.gdshader")
const RuneShader = preload("res://scripts/luneraria/view/materials/lune_rune.gdshader")
const ClothShader = preload("res://scripts/luneraria/view/materials/lune_cloth.gdshader")
const WaterGlowShader = preload("res://scripts/luneraria/view/materials/lune_water_glow.gdshader")
const HideShader = preload("res://scripts/luneraria/view/materials/lune_hide.gdshader")
const Limestone = preload("res://assets/materials/phase10/limestone-albedo.png")
const NEUTRAL: int = 2
## Team identity beyond colour: cool team (Blau) and warm team (Rot) differ in crystal and
## rune hue, metal (silver-steel / bronze), rune family, banner silhouette and emblem.
## Index 2 is the neutral grotto.
const TEAM_STYLE: Array[Dictionary] = [
{"crystal": Color("78c6ff"), "rune": Color("86d2ff"), "metal": Color(.80, .85, .92), "cloth": Color("2a5aa8"),
"emblem": Color("e8eef8"), "light": Color("8cc8ff"), "pattern": 0.0, "shape": 0.0, "name": "kühl · Mondsichel · Silber"},
{"crystal": Color("ff7d45"), "rune": Color("ffae5a"), "metal": Color(.92, .64, .34), "cloth": Color("9a2c20"),
"emblem": Color("f4d58c"), "light": Color("ffae72"), "pattern": 1.0, "shape": 1.0, "name": "warm · Sonnenscheibe · Bronze"},
{"crystal": Color("b784ff"), "rune": Color("c29cff"), "metal": Color(.70, .62, .50), "cloth": Color("45305f"),
"emblem": Color("d9c8f0"), "light": Color("b37cff"), "pattern": 2.0, "shape": 0.0, "name": "neutral · Grotte"},
]
static var _cache: Dictionary = {}
static func _shader_material(shader: Shader, params: Dictionary) -> ShaderMaterial:
var material := ShaderMaterial.new()
material.shader = shader
for key: String in params:
material.set_shader_parameter(key, params[key])
return material
## The shared material for an LN_* surface name, or null for foreign materials.
static func get_material(key: String) -> Material:
if _cache.has(key):
return _cache[key]
var material: Material = null
match key:
"LN_Stone_Pale":
material = _shader_material(StoneShader, {"base_color": Color(.72, .68, .61), "stone_map": Limestone, "lichen": .22})
"LN_Stone_Dark":
material = _shader_material(StoneShader, {"base_color": Color(.30, .30, .33), "stone_map": Limestone, "lichen": .12,
"course_height": .34, "block_length": .66, "roughness_base": .9})
"LN_Stone_Grotto":
material = _shader_material(StoneShader, {"base_color": Color(.43, .43, .46), "stone_map": Limestone, "lichen": .3,
"course_height": .5, "block_length": .9, "tint_variation": .07})
"LN_Rock":
material = _shader_material(StoneShader, {"base_color": Color(.38, .38, .40), "stone_map": Limestone, "lichen": .45,
"ashlar": false, "texture_scale": .55, "tint_variation": .05, "roughness_base": .93})
"LN_Metal_Team":
material = _shader_material(MetalShader, {"base_color": Color(.92, .92, .92), "roughness_base": .32, "wear": .6})
"LN_Metal_Iron":
material = _shader_material(MetalShader, {"base_color": Color(.24, .24, .26), "roughness_base": .52, "metallic_value": .8, "wear": .8})
"LN_Metal_Bronze":
material = _shader_material(MetalShader, {"base_color": Color(.66, .47, .24), "roughness_base": .42, "patina": .7})
"LN_Crystal_Team", "LN_Crystal_Grotto":
material = _shader_material(CrystalShader, {})
"LN_Rune_Team", "LN_Rune_Grotto":
material = _shader_material(RuneShader, {})
"LN_Cloth_Team":
material = _shader_material(ClothShader, {})
"LN_Water_Glow":
material = _shader_material(WaterGlowShader, {})
# The Schlundkrone is skinned: its surfaces read their patterns from rest positions.
"LN_Boss_Hide":
material = _shader_material(HideShader, {})
"LN_Boss_Bone":
material = _shader_material(HideShader, {"base_color": Color(.74, .71, .64), "plate_color": Color(.83, .80, .72),
"vein_amount": .25, "roughness_base": .62})
"LN_Boss_Bronze":
material = _shader_material(HideShader, {"base_color": Color(.42, .31, .18), "plate_color": Color(.52, .38, .21),
"vein_amount": 0.0, "roughness_base": .42, "metallic_value": .85, "patina": .75})
"LN_Crystal_Boss":
material = _shader_material(CrystalShader, {"heart_radius": .02, "depth_scale": .3, "vein_scale": 3.6})
"LN_Wood":
var wood := StandardMaterial3D.new()
wood.albedo_color = Color(.33, .23, .15)
wood.roughness = .82
wood.vertex_color_use_as_albedo = true
material = wood
"LN_Cave":
var cave := StandardMaterial3D.new()
cave.albedo_color = Color(.02, .015, .03)
cave.roughness = 1.0
material = cave
if material != null:
material.resource_name = key
_cache[key] = material
return material
## Swaps LN_* surface materials below root for the shared ones. Returns every geometry
## instance that carries at least one LN_* surface (the ones that take instance parameters).
static func dress(root: Node) -> Array[GeometryInstance3D]:
var result: Array[GeometryInstance3D] = []
_dress(root, result)
return result
static func _dress(node: Node, result: Array[GeometryInstance3D]) -> void:
if node is MeshInstance3D and (node as MeshInstance3D).mesh != null:
var instance := node as MeshInstance3D
var dressed: bool = false
for index: int in range(instance.mesh.get_surface_count()):
var source: Material = instance.mesh.surface_get_material(index)
if source == null or not source.resource_name.begins_with("LN_"):
continue
var shared: Material = get_material(source.resource_name)
if shared != null:
instance.set_surface_override_material(index, shared)
dressed = true
if dressed:
result.append(instance)
for child: Node in node.get_children():
_dress(child, result)
## Team colours and families on a set of instances. seed decorrelates buildings.
static func apply_team(instances: Array, team: int, seed_value: float) -> void:
var style: Dictionary = TEAM_STYLE[clampi(team, 0, 2)]
for instance: GeometryInstance3D in instances:
instance.set_instance_shader_parameter("tint", style.crystal)
instance.set_instance_shader_parameter("metal_tint", style.metal)
instance.set_instance_shader_parameter("pattern", style.pattern)
instance.set_instance_shader_parameter("cloth_color", style.cloth)
instance.set_instance_shader_parameter("emblem_color", style.emblem)
instance.set_instance_shader_parameter("shape", style.shape)
instance.set_instance_shader_parameter("seed", seed_value)
## Rune surfaces use the rune hue; crystals the crystal hue. Both share the `tint` slot, so a
## mesh that only carries runes gets the rune hue.
static func apply_rune_tint(instances: Array, team: int) -> void:
var style: Dictionary = TEAM_STYLE[clampi(team, 0, 2)]
for instance: GeometryInstance3D in instances:
instance.set_instance_shader_parameter("tint", style.rune)
static func set_param(instances: Array, parameter: String, value: Variant) -> void:
for instance: GeometryInstance3D in instances:
instance.set_instance_shader_parameter(parameter, value)
static func team_color(team: int, key: String) -> Color:
return TEAM_STYLE[clampi(team, 0, 2)][key]