198 lines
7.8 KiB
GDScript
198 lines
7.8 KiB
GDScript
extends Node2D
|
|
## Short, layered death effect. All particles are local visuals, without physics bodies.
|
|
const FIRE_SHADER := preload("res://resources/effects/ship_fireball.gdshader")
|
|
const EXPLOSION_AUDIO := preload("res://assets/sounds/effects/explosion.mp3")
|
|
const MAX_EFFECTS := 18
|
|
static var _smoke_texture: Texture2D
|
|
var _elapsed := 0.0
|
|
var _duration := 2.1
|
|
var _size := 56.0
|
|
var _tier := 0
|
|
var _texture: Texture2D
|
|
var _tint := Color.WHITE
|
|
var _ship_scale := Vector2.ONE
|
|
var _angle := 0.0
|
|
var _drift := Vector2.ZERO
|
|
var _fragments: Array[Dictionary] = []
|
|
var _sparks: Array[Dictionary] = []
|
|
var _smoke: Array[Dictionary] = []
|
|
var _bursts: Array[Dictionary] = []
|
|
var _rng := RandomNumberGenerator.new()
|
|
var play_audio := true
|
|
|
|
func configure(sprite: Sprite2D, drift: Vector2, tier: int) -> void:
|
|
top_level = true
|
|
_texture = sprite.texture
|
|
_ship_scale = sprite.global_scale
|
|
_angle = sprite.global_rotation
|
|
_tint = sprite.modulate
|
|
global_position = sprite.global_position
|
|
_drift = drift * 0.65
|
|
_tier = clampi(tier,0,2)
|
|
_size = maxf(_texture.get_width()*_ship_scale.x,_texture.get_height()*_ship_scale.y) if _texture != null else 56.0
|
|
_duration = 2.1 + _tier * 0.25
|
|
|
|
func _ready() -> void:
|
|
z_index = 35
|
|
light_mask = 4
|
|
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
|
physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF
|
|
add_to_group("enemy_explosions")
|
|
var active := get_tree().get_nodes_in_group("enemy_explosions")
|
|
if active.size() > MAX_EFFECTS:
|
|
for old in active:
|
|
if old != self and not old.is_queued_for_deletion():
|
|
old.queue_free()
|
|
break
|
|
_rng.randomize()
|
|
_prepare_smoke()
|
|
_build_fragments()
|
|
_build_particles()
|
|
_add_fireball(Vector2.ZERO,_size*3.8,0.0)
|
|
for index in 2 + _tier:
|
|
var direction := Vector2.from_angle(_rng.randf()*TAU)
|
|
_add_fireball(direction*_size*_rng.randf_range(0.2,0.5),_size*_rng.randf_range(1.5,2.3),0.07+index*0.07)
|
|
if play_audio: _play_sound()
|
|
_update_fireballs()
|
|
|
|
func _add_fireball(origin: Vector2, diameter: float, delay: float) -> void:
|
|
var mesh := MeshInstance2D.new()
|
|
var quad := QuadMesh.new()
|
|
quad.size = Vector2.ONE * diameter
|
|
mesh.mesh = quad
|
|
mesh.position = origin
|
|
mesh.rotation = _rng.randf()*TAU
|
|
mesh.light_mask = 4
|
|
mesh.z_index = 1
|
|
var fire := ShaderMaterial.new()
|
|
fire.shader = FIRE_SHADER
|
|
fire.set_shader_parameter("variation",_rng.randf_range(0,100))
|
|
mesh.material = fire
|
|
add_child(mesh)
|
|
_bursts.append({"visual":mesh,"material":fire,"delay":delay})
|
|
|
|
func _build_fragments() -> void:
|
|
if _texture == null: return
|
|
var dimensions := _texture.get_size()
|
|
# Textured triangles preserve the actual hull, rather than generic square debris.
|
|
var grid := 3 if _tier < 2 else 4
|
|
for y in grid:
|
|
for x in grid:
|
|
var a := Vector2(x,y)*dimensions/grid
|
|
var b := Vector2(x+1,y)*dimensions/grid
|
|
var c := Vector2(x+1,y+1)*dimensions/grid
|
|
var d := Vector2(x,y+1)*dimensions/grid
|
|
for triangle in [[a,b,c],[a,c,d]]:
|
|
var center: Vector2 = (triangle[0]+triangle[1]+triangle[2])/3.0
|
|
var origin := ((center-dimensions*0.5)*_ship_scale).rotated(_angle)
|
|
var points := PackedVector2Array()
|
|
var uv := PackedVector2Array()
|
|
for vertex in triangle:
|
|
points.append((vertex-center)*_ship_scale)
|
|
uv.append(vertex/dimensions)
|
|
var direction := origin.normalized().rotated(_rng.randf_range(-0.4,0.4))
|
|
_fragments.append({"origin":origin,"velocity":direction*_rng.randf_range(80,175)*(1.0+_tier*0.2),"points":points,"uv":uv,"spin":_rng.randf_range(-5,5)})
|
|
|
|
func _build_particles() -> void:
|
|
for index in 32 + _tier * 14:
|
|
var direction := Vector2.from_angle(_rng.randf()*TAU)
|
|
_sparks.append({"velocity":direction*_rng.randf_range(100,370)*(1.0+0.18*_tier),"life":_rng.randf_range(0.4,1.35),"width":_rng.randf_range(0.8,2.1),"warm":_rng.randf()})
|
|
for index in 10 + _tier * 3:
|
|
var direction := Vector2.from_angle(_rng.randf()*TAU)
|
|
_smoke.append({"origin":direction*_size*_rng.randf_range(0.1,0.35),"velocity":direction*_rng.randf_range(16,48),"radius":_size*_rng.randf_range(0.22,0.4),"angle":_rng.randf()*TAU,"delay":_rng.randf_range(0.08,0.25)})
|
|
|
|
static func _prepare_smoke() -> void:
|
|
if _smoke_texture != null: return
|
|
var noise := FastNoiseLite.new()
|
|
noise.seed = 9172
|
|
noise.frequency = 0.075
|
|
var image := Image.create(64,64,false,Image.FORMAT_RGBA8)
|
|
for y in 64:
|
|
for x in 64:
|
|
var point := (Vector2(x,y)-Vector2(31.5,31.5))/31.5
|
|
var edge := 1.0-smoothstep(0.2,1.0,point.length())
|
|
var density := clampf(0.65+noise.get_noise_2d(x,y)*0.65,0,1)
|
|
image.set_pixel(x,y,Color(1,1,1,edge*density))
|
|
_smoke_texture = ImageTexture.create_from_image(image)
|
|
|
|
func _play_sound() -> void:
|
|
if get_tree().get_nodes_in_group("enemy_explosion_audio").size() >= 8: return
|
|
# Separate audio lifetime lets the tail finish after the visual is freed.
|
|
var audio := AudioStreamPlayer2D.new()
|
|
audio.stream = EXPLOSION_AUDIO
|
|
audio.volume_db = -15.0 + _tier*2.5
|
|
audio.pitch_scale = _rng.randf_range(0.95,1.12) - _tier*0.13
|
|
audio.max_distance = 1500
|
|
audio.max_polyphony = 1
|
|
audio.add_to_group("enemy_explosion_audio")
|
|
get_parent().add_child(audio)
|
|
audio.global_position = global_position
|
|
audio.finished.connect(audio.queue_free)
|
|
audio.play()
|
|
|
|
func _process(delta: float) -> void:
|
|
_elapsed += delta
|
|
global_position += _drift * delta
|
|
_drift *= exp(-1.4*delta)
|
|
if _elapsed >= _duration:
|
|
queue_free()
|
|
return
|
|
_update_fireballs()
|
|
queue_redraw()
|
|
|
|
func _update_fireballs() -> void:
|
|
for burst in _bursts:
|
|
var age: float = _elapsed - float(burst.delay)
|
|
var visual: MeshInstance2D = burst.visual
|
|
visual.visible = age >= 0 and age < 1.3
|
|
if visual.visible:
|
|
var fire: ShaderMaterial = burst.material
|
|
fire.set_shader_parameter("phase",age)
|
|
fire.set_shader_parameter("strength",1.25 if _tier == 2 else 1.0)
|
|
|
|
func _draw() -> void:
|
|
var time := _elapsed
|
|
# A soft, irregular smoke cloud stays behind as the fire cools.
|
|
for puff in _smoke:
|
|
var age := maxf(0,time-float(puff.delay))
|
|
var alpha := smoothstep(0.0,0.3,age)*(1.0-smoothstep(0.6,_duration,age))*0.45
|
|
var center: Vector2 = puff.origin + puff.velocity * (1.0-exp(-age*1.5))/1.5
|
|
var radius: float = puff.radius*(0.6+age*0.85)
|
|
draw_set_transform(center,float(puff.angle)+age*0.15)
|
|
draw_texture_rect(_smoke_texture,Rect2(-Vector2.ONE*radius,Vector2.ONE*radius*2),false,Color(0.12,0.14,0.18,alpha))
|
|
draw_set_transform(Vector2.ZERO)
|
|
# Thin blue ion shock followed by a wider amber pressure front.
|
|
for ring in 2:
|
|
var age := time-ring*0.055
|
|
if age < 0 or age > 0.65: continue
|
|
var progress := age/0.65
|
|
var radius := _size*(0.16+1.75*(1.0-pow(1.0-progress,3)))
|
|
var fade := pow(1.0-progress,2)
|
|
var color := Color("87dfff") if ring == 0 else Color("ff9d49")
|
|
draw_arc(Vector2.ZERO,radius,0,TAU,80,Color(color,fade*0.1),9.0*(1.0-progress)+1,true)
|
|
draw_arc(Vector2.ZERO,radius,0,TAU,80,Color(color,fade*0.75),1.2+fade,true)
|
|
var debris_fade := 1.0-smoothstep(0.7,_duration,time)
|
|
var travel := (1.0-exp(-time*1.4))/1.4
|
|
for piece in _fragments:
|
|
var center: Vector2 = piece.origin + piece.velocity*travel
|
|
draw_set_transform(center,_angle+float(piece.spin)*time)
|
|
var hot := exp(-time*4.0)
|
|
var color := _tint.lerp(Color("ffd28b"),hot).darkened((1.0-hot)*0.55)
|
|
color.a = debris_fade
|
|
draw_polygon(piece.points,PackedColorArray([color]),piece.uv,_texture)
|
|
draw_set_transform(Vector2.ZERO)
|
|
for spark in _sparks:
|
|
if time > float(spark.life): continue
|
|
var fade := pow(1.0-time/float(spark.life),1.5)
|
|
var velocity: Vector2 = spark.velocity
|
|
var point := velocity*travel
|
|
var tail := point-velocity.normalized()*(3.0+fade*16.0)
|
|
var color := Color("ffc35c").lerp(Color("fff2c4"),float(spark.warm))
|
|
draw_line(tail,point,Color(color,fade*0.14),float(spark.width)*4,true)
|
|
draw_line(tail,point,Color(color,fade),float(spark.width),true)
|
|
var flash := exp(-time*30.0)
|
|
if flash > 0.01:
|
|
for layer in range(5,0,-1):
|
|
draw_circle(Vector2.ZERO,_size*0.45*layer/5.0,Color(1,0.83,0.55,flash*0.12))
|
|
draw_line(Vector2(-_size*1.2,0),Vector2(_size*1.2,0),Color(1,0.92,0.75,flash*0.75),1.6,true)
|