Randoloot/scripts/game/asteroid_destruction_effect.gd
2026-10-16 22:04:36 +02:00

117 lines
4.9 KiB
GDScript

extends Node2D
const SETTLE_TIME := 8.0
var _texture: Texture2D
var _fragments: Array[Dictionary] = []
var _dust: Array[Dictionary] = []
var _elapsed := 0.0
var _rng := RandomNumberGenerator.new()
var _dust_color := Color("b4a99a")
func configure(sprite: Sprite2D) -> void:
_texture = sprite.texture
transform = sprite.global_transform
var sprite_scale := global_scale
scale = Vector2.ONE
_rng.randomize()
var dimensions := _texture.get_size()
var grid: Array[Vector2] = []
for y in 5:
for x in 5:
var vertex := Vector2(x, y) * dimensions / 4.0
if x > 0 and x < 4:
vertex.x += _rng.randf_range(-0.3, 0.3) * dimensions.x / 4.0
if y > 0 and y < 4:
vertex.y += _rng.randf_range(-0.3, 0.3) * dimensions.y / 4.0
grid.append(vertex)
# Shared jagged cracks keep neighbouring pieces fitted together.
var horizontal: Array[PackedVector2Array] = []
var vertical: Array[PackedVector2Array] = []
for y in 5:
for x in 4:
horizontal.append(_fracture_edge(grid[y * 5 + x], grid[y * 5 + x + 1], y > 0 and y < 4))
for y in 4:
for x in 5:
vertical.append(_fracture_edge(grid[y * 5 + x], grid[(y + 1) * 5 + x], x > 0 and x < 4))
for y in 4:
for x in 4:
var outline := PackedVector2Array()
outline.append_array(_edge_vertices(horizontal[y * 4 + x], false))
outline.append_array(_edge_vertices(vertical[y * 5 + x + 1], false))
outline.append_array(_edge_vertices(horizontal[(y + 1) * 4 + x], true))
outline.append_array(_edge_vertices(vertical[y * 5 + x], true))
# Rare jagged cracks can cross at a corner. Repair the silhouette once,
# rather than submitting an invalid polygon every rendered frame.
if Geometry2D.triangulate_polygon(outline).is_empty():
outline = Geometry2D.convex_hull(outline)
if outline.size() > 1 and outline[0].is_equal_approx(outline[outline.size() - 1]):
outline.remove_at(outline.size() - 1)
if outline.size() < 3 or Geometry2D.triangulate_polygon(outline).is_empty():
continue
var center := Vector2.ZERO
for vertex in outline:
center += vertex
center /= outline.size()
var origin := (center - dimensions * 0.5) * sprite_scale
var points := PackedVector2Array()
var uv := PackedVector2Array()
for vertex in outline:
points.append((vertex - center) * sprite_scale)
uv.append(vertex / dimensions)
var direction := origin.normalized() if origin.length_squared() > 0.1 else Vector2.RIGHT
_fragments.append({"points": points, "uv": uv, "position": origin,
"velocity": direction.rotated(_rng.randf_range(-0.35, 0.35)) * _rng.randf_range(40.0, 95.0),
"angle": 0.0, "spin": _rng.randf_range(-4.0, 4.0)})
var mineral := _texture.resource_path.get_file().get_basename()
_dust_color = {"iron": Color("be9b75"), "titanium": Color("9ecbe9"), "uranium": Color("a9db71"), "aluminium": Color("d3dce0"), "sulfur": Color("e8cd65")}.get(mineral, Color("b4a99a"))
for index in 28:
_dust.append({"direction": Vector2.RIGHT.rotated(_rng.randf_range(0.0, TAU)),
"speed": _rng.randf_range(45.0, 210.0), "size": _rng.randf_range(1.0, 3.0)})
z_index = 35
light_mask = 4
func _fracture_edge(start: Vector2, end: Vector2, jagged: bool) -> PackedVector2Array:
var edge := PackedVector2Array([start])
if jagged:
var normal := (end - start).orthogonal().normalized()
var roughness := start.distance_to(end) * 0.13
var side := 1.0 if _rng.randf() > 0.5 else -1.0
for index in 3:
var along := (index + 1.0) / 4.0 + _rng.randf_range(-0.05, 0.05)
edge.append(start.lerp(end, along) + normal * roughness * side * _rng.randf_range(0.5, 1.0))
side *= -1.0
edge.append(end)
return edge
func _edge_vertices(edge: PackedVector2Array, backwards: bool) -> PackedVector2Array:
var vertices := PackedVector2Array()
# The next edge supplies the end vertex, avoiding duplicate polygon points.
for index in edge.size() - 1:
vertices.append(edge[edge.size() - 1 - index] if backwards else edge[index])
return vertices
func _process(delta: float) -> void:
_elapsed += delta
for fragment in _fragments:
fragment.position += fragment.velocity * delta
fragment.velocity *= exp(-1.2 * delta)
fragment.angle += fragment.spin * delta
fragment.spin *= exp(-0.7 * delta)
queue_redraw()
# Leave the settled debris visible without updating it every frame.
if _elapsed >= SETTLE_TIME:
_dust.clear()
set_process(false)
func _draw() -> void:
for fragment in _fragments:
draw_set_transform(fragment.position, fragment.angle)
draw_polygon(fragment.points, PackedColorArray([Color.WHITE]), fragment.uv, _texture)
draw_set_transform(Vector2.ZERO)
var dust_fade := maxf(0.0, 1.0 - _elapsed / 0.8)
for mote in _dust:
var travel: Vector2 = mote.direction * mote.speed * (1.0 - exp(-2.0 * _elapsed)) / 2.0
draw_circle(travel, mote.size * (1.0 + _elapsed), Color(_dust_color, dust_fade * 0.65))
if _elapsed < 0.18:
draw_circle(Vector2.ZERO, 16.0 * (1.0 - _elapsed / 0.18), Color(0.85, 0.9, 1.0, (1.0 - _elapsed / 0.18) * 0.55))