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