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>
242 lines
10 KiB
GDScript
242 lines
10 KiB
GDScript
extends RefCounted
|
|
## Particle and glow building blocks for the strategic buildings (G2). Every effect is a
|
|
## small, bounded GPUParticles3D (no per-frame physics debris): sparks, smoke, dust bursts,
|
|
## crystal shard bursts and rising motes, plus soft sprite materials. Effects are purely
|
|
## visual and never feed back into the simulation.
|
|
|
|
static var _soft: Texture2D
|
|
static var _cache: Dictionary = {}
|
|
|
|
## Radial soft dot used by smoke, dust, motes and flashes.
|
|
static func soft_texture() -> Texture2D:
|
|
if _soft == null:
|
|
var gradient := Gradient.new()
|
|
gradient.set_color(0, Color(1, 1, 1, 1))
|
|
gradient.set_color(1, Color(1, 1, 1, 0))
|
|
gradient.add_point(.35, Color(1, 1, 1, .55))
|
|
var texture := GradientTexture2D.new()
|
|
texture.gradient = gradient
|
|
texture.fill = GradientTexture2D.FILL_RADIAL
|
|
texture.fill_from = Vector2(.5, .5)
|
|
texture.fill_to = Vector2(1.0, .5)
|
|
texture.width = 64
|
|
texture.height = 64
|
|
_soft = texture
|
|
return _soft
|
|
|
|
## Billboard sprite material: additive glow (sparks, motes, flashes) or alpha (smoke, dust).
|
|
static func sprite_material(color: Color, additive: bool, energy: float = 1.0) -> StandardMaterial3D:
|
|
var key: String = "%s:%s:%.2f" % [color.to_html(), additive, energy]
|
|
if _cache.has(key):
|
|
return _cache[key]
|
|
var material := StandardMaterial3D.new()
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD if additive else BaseMaterial3D.BLEND_MODE_MIX
|
|
material.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
|
|
material.vertex_color_use_as_albedo = true
|
|
material.albedo_texture = soft_texture()
|
|
material.albedo_color = Color(color.r * energy, color.g * energy, color.b * energy, color.a)
|
|
material.no_depth_test = false
|
|
material.disable_receive_shadows = true
|
|
_cache[key] = material
|
|
return material
|
|
|
|
static func _quad(size: float, material: Material) -> QuadMesh:
|
|
var quad := QuadMesh.new()
|
|
quad.size = Vector2(size, size)
|
|
quad.material = material
|
|
return quad
|
|
|
|
static func _ramp(stops: Array) -> GradientTexture1D:
|
|
var gradient := Gradient.new()
|
|
gradient.offsets = PackedFloat32Array(stops.map(func(s: Array) -> float: return s[0]))
|
|
gradient.colors = PackedColorArray(stops.map(func(s: Array) -> Color: return s[1]))
|
|
var texture := GradientTexture1D.new()
|
|
texture.gradient = gradient
|
|
return texture
|
|
|
|
static func _curve(points: Array) -> CurveTexture:
|
|
var curve := Curve.new()
|
|
for p: Vector2 in points:
|
|
curve.add_point(p)
|
|
var texture := CurveTexture.new()
|
|
texture.curve = curve
|
|
return texture
|
|
|
|
static func _base(name: String, amount: int, lifetime: float, box: AABB) -> GPUParticles3D:
|
|
var particles := GPUParticles3D.new()
|
|
particles.name = name
|
|
particles.amount = amount
|
|
particles.lifetime = lifetime
|
|
particles.visibility_aabb = box
|
|
particles.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
particles.local_coords = false
|
|
particles.emitting = false
|
|
return particles
|
|
|
|
## Continuous sparks falling from damaged masonry and metal.
|
|
static func sparks(color: Color, amount: int = 14, radius: float = 1.0, height: float = 2.0) -> GPUParticles3D:
|
|
var particles := _base("Sparks", amount, .9, AABB(Vector3(-4, -1, -4), Vector3(8, 9, 8)))
|
|
var process := ParticleProcessMaterial.new()
|
|
process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_RING
|
|
process.emission_ring_axis = Vector3.UP
|
|
process.emission_ring_radius = radius
|
|
process.emission_ring_inner_radius = radius * .6
|
|
process.emission_ring_height = height
|
|
process.direction = Vector3(0, 1, 0)
|
|
process.spread = 60.0
|
|
process.initial_velocity_min = .6
|
|
process.initial_velocity_max = 2.2
|
|
process.gravity = Vector3(0, -5.5, 0)
|
|
process.scale_min = .5
|
|
process.scale_max = 1.0
|
|
process.color_ramp = _ramp([[0.0, Color(1, 1, 1, 1)], [.5, Color(1, .8, .5, .9)], [1.0, Color(1, .4, .2, 0)]])
|
|
particles.process_material = process
|
|
particles.draw_pass_1 = _quad(.07, sprite_material(color, true, 3.0))
|
|
return particles
|
|
|
|
## Thin rising smoke (damage) or a heavier plume (ruin, destruction).
|
|
static func smoke(amount: int = 10, radius: float = .8, dark: float = .22, size: float = 1.1, lifetime: float = 3.2) -> GPUParticles3D:
|
|
var particles := _base("Smoke", amount, lifetime, AABB(Vector3(-5, -1, -5), Vector3(10, 14, 10)))
|
|
var process := ParticleProcessMaterial.new()
|
|
process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
|
process.emission_sphere_radius = radius
|
|
process.direction = Vector3(0, 1, 0)
|
|
process.spread = 18.0
|
|
process.initial_velocity_min = .35
|
|
process.initial_velocity_max = .8
|
|
process.gravity = Vector3(.18, .25, .08)
|
|
process.damping_min = .2
|
|
process.damping_max = .5
|
|
process.scale_min = .7
|
|
process.scale_max = 1.3
|
|
process.scale_curve = _curve([Vector2(0, .35), Vector2(1, 1.6)])
|
|
process.angle_min = 0.0
|
|
process.angle_max = 360.0
|
|
process.color_ramp = _ramp([[0.0, Color(dark, dark, dark * 1.05, 0)], [.15, Color(dark, dark, dark * 1.05, .42)], [1.0, Color(dark * 1.3, dark * 1.3, dark * 1.35, 0)]])
|
|
particles.process_material = process
|
|
particles.draw_pass_1 = _quad(size, sprite_material(Color(1, 1, 1, 1), false))
|
|
return particles
|
|
|
|
## One-shot dust and rubble burst at the foot of a collapsing structure.
|
|
static func dust_burst(amount: int = 36, radius: float = 1.4, size: float = 1.6, color: Color = Color(.62, .58, .52)) -> GPUParticles3D:
|
|
var particles := _base("DustBurst", amount, 1.9, AABB(Vector3(-7, -1, -7), Vector3(14, 10, 14)))
|
|
particles.one_shot = true
|
|
particles.explosiveness = .85
|
|
var process := ParticleProcessMaterial.new()
|
|
process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_RING
|
|
process.emission_ring_axis = Vector3.UP
|
|
process.emission_ring_radius = radius
|
|
process.emission_ring_inner_radius = radius * .3
|
|
process.emission_ring_height = .6
|
|
process.direction = Vector3(0, .35, 0)
|
|
process.spread = 80.0
|
|
process.initial_velocity_min = 1.2
|
|
process.initial_velocity_max = 3.4
|
|
process.gravity = Vector3(0, -.6, 0)
|
|
process.damping_min = 1.6
|
|
process.damping_max = 2.6
|
|
process.scale_curve = _curve([Vector2(0, .4), Vector2(1, 1.8)])
|
|
process.angle_max = 360.0
|
|
process.color_ramp = _ramp([[0.0, Color(color.r, color.g, color.b, .75)], [1.0, Color(color.r, color.g, color.b, 0)]])
|
|
particles.process_material = process
|
|
particles.draw_pass_1 = _quad(size, sprite_material(Color(1, 1, 1, 1), false))
|
|
return particles
|
|
|
|
## One-shot burst of glowing crystal fragments (crystal failure, Ancient collapse).
|
|
static func shard_burst(color: Color, amount: int = 40, speed: float = 5.0) -> GPUParticles3D:
|
|
var particles := _base("ShardBurst", amount, 1.3, AABB(Vector3(-9, -2, -9), Vector3(18, 14, 18)))
|
|
particles.one_shot = true
|
|
particles.explosiveness = .95
|
|
var process := ParticleProcessMaterial.new()
|
|
process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
|
process.emission_sphere_radius = .4
|
|
process.direction = Vector3(0, 1, 0)
|
|
process.spread = 180.0
|
|
process.initial_velocity_min = speed * .4
|
|
process.initial_velocity_max = speed
|
|
process.gravity = Vector3(0, -7.0, 0)
|
|
process.scale_min = .5
|
|
process.scale_max = 1.2
|
|
process.color_ramp = _ramp([[0.0, Color(1, 1, 1, 1)], [.6, Color(1, 1, 1, .8)], [1.0, Color(1, 1, 1, 0)]])
|
|
particles.process_material = process
|
|
var mesh := PrismMesh.new()
|
|
mesh.size = Vector3(.06, .16, .06)
|
|
var material := StandardMaterial3D.new()
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.vertex_color_use_as_albedo = true
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
material.albedo_color = Color(color.r * 1.35, color.g * 1.35, color.b * 1.35)
|
|
mesh.material = material
|
|
particles.draw_pass_1 = mesh
|
|
return particles
|
|
|
|
## Slow rising motes of energy around crystals (ambient; few particles).
|
|
static func motes(color: Color, amount: int = 18, radius: float = 2.0, height: float = 3.0, lifetime: float = 4.0) -> GPUParticles3D:
|
|
var particles := _base("Motes", amount, lifetime, AABB(Vector3(-6, -1, -6), Vector3(12, 14, 12)))
|
|
var process := ParticleProcessMaterial.new()
|
|
process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_RING
|
|
process.emission_ring_axis = Vector3.UP
|
|
process.emission_ring_radius = radius
|
|
process.emission_ring_inner_radius = radius * .3
|
|
process.emission_ring_height = height
|
|
process.direction = Vector3(0, 1, 0)
|
|
process.spread = 25.0
|
|
process.initial_velocity_min = .15
|
|
process.initial_velocity_max = .45
|
|
process.gravity = Vector3(0, .08, 0)
|
|
process.tangential_accel_min = .2
|
|
process.tangential_accel_max = .5
|
|
process.scale_min = .6
|
|
process.scale_max = 1.2
|
|
process.color_ramp = _ramp([[0.0, Color(1, 1, 1, 0)], [.2, Color(1, 1, 1, 1)], [1.0, Color(1, 1, 1, 0)]])
|
|
particles.process_material = process
|
|
particles.draw_pass_1 = _quad(.09, sprite_material(color, true, 2.2))
|
|
return particles
|
|
|
|
## Energy escaping from a cracked crystal: fast, bright sparks shooting upwards in bursts.
|
|
static func leak(color: Color, amount: int = 30, radius: float = .8) -> GPUParticles3D:
|
|
var particles := _base("Leak", amount, 1.1, AABB(Vector3(-5, -2, -5), Vector3(10, 14, 10)))
|
|
particles.explosiveness = .35
|
|
particles.randomness = .6
|
|
var process := ParticleProcessMaterial.new()
|
|
process.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
|
process.emission_sphere_radius = radius
|
|
process.direction = Vector3(0, 1, 0)
|
|
process.spread = 35.0
|
|
process.initial_velocity_min = 2.2
|
|
process.initial_velocity_max = 4.6
|
|
process.gravity = Vector3(0, -1.2, 0)
|
|
process.damping_min = .8
|
|
process.damping_max = 1.6
|
|
process.scale_min = .6
|
|
process.scale_max = 1.3
|
|
process.color_ramp = _ramp([[0.0, Color(1, 1, 1, 1)], [.6, Color(1, 1, 1, .7)], [1.0, Color(1, 1, 1, 0)]])
|
|
particles.process_material = process
|
|
particles.draw_pass_1 = _quad(.1, sprite_material(color, true, 3.2))
|
|
return particles
|
|
|
|
## Additive billboard flash (impact, overload); animate its scale and transparency.
|
|
static func flash_mesh(color: Color, size: float = 1.0) -> MeshInstance3D:
|
|
var flash := MeshInstance3D.new()
|
|
var quad := QuadMesh.new()
|
|
quad.size = Vector2(size, size)
|
|
var material := StandardMaterial3D.new()
|
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
|
material.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
|
material.albedo_texture = soft_texture()
|
|
material.albedo_color = color
|
|
material.disable_receive_shadows = true
|
|
quad.material = material
|
|
flash.mesh = quad
|
|
flash.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
return flash
|
|
|
|
## Restarts a one-shot effect at a position.
|
|
static func fire(particles: GPUParticles3D, at: Vector3) -> void:
|
|
particles.global_position = at
|
|
particles.restart()
|
|
particles.emitting = true
|