Zmiany
This commit is contained in:
parent
9808a03d13
commit
5dd71181bc
BIN
assets/sounds/effects/explosion.mp3
Normal file
BIN
assets/sounds/effects/explosion.mp3
Normal file
Binary file not shown.
19
assets/sounds/effects/explosion.mp3.import
Normal file
19
assets/sounds/effects/explosion.mp3.import
Normal file
@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="mp3"
|
||||
type="AudioStreamMP3"
|
||||
uid="uid://bt61nivobxmjc"
|
||||
path="res://.godot/imported/explosion.mp3-9b15137be8aff53d245e350137bf182a.mp3str"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/sounds/effects/explosion.mp3"
|
||||
dest_files=["res://.godot/imported/explosion.mp3-9b15137be8aff53d245e350137bf182a.mp3str"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
50
resources/effects/ship_fireball.gdshader
Normal file
50
resources/effects/ship_fireball.gdshader
Normal file
@ -0,0 +1,50 @@
|
||||
shader_type canvas_item;
|
||||
render_mode unshaded, blend_add;
|
||||
|
||||
uniform float phase = 0.0;
|
||||
uniform float variation = 0.0;
|
||||
uniform float strength = 1.0;
|
||||
|
||||
float hash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||
}
|
||||
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0)), u.x), u.y);
|
||||
}
|
||||
|
||||
float fbm(vec2 p) {
|
||||
float value = 0.0;
|
||||
float weight = 0.5;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
value += noise(p) * weight;
|
||||
p = mat2(vec2(1.6, 1.2), vec2(-1.2, 1.6)) * p + vec2(17.0, 9.0);
|
||||
weight *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 p = (UV - vec2(0.5)) * 2.0;
|
||||
float radius = length(p);
|
||||
float age = max(phase, 0.0);
|
||||
float expansion = 0.16 + 0.44 * (1.0 - exp(-age * 7.0));
|
||||
vec2 flow = p * 5.0 / max(expansion, 0.12);
|
||||
flow += vec2(variation, -age * 3.2);
|
||||
vec2 warp = vec2(fbm(flow + vec2(age * 1.3, 0.0)), fbm(flow + vec2(8.0, -age)));
|
||||
float turbulence = fbm(flow + warp * 3.0);
|
||||
float edge = expansion * (0.65 + turbulence * 0.65);
|
||||
float body = 1.0 - smoothstep(edge * 0.58, edge, radius);
|
||||
float fade = 1.0 - smoothstep(0.35, 1.3, age);
|
||||
float heat = clamp(body * (0.35 + turbulence * 0.9) * exp(-age * 1.0), 0.0, 1.0);
|
||||
vec3 fire = mix(vec3(0.85, 0.12, 0.015), vec3(1.0, 0.52, 0.035), smoothstep(0.08, 0.35, heat));
|
||||
fire = mix(fire, vec3(1.0, 0.88, 0.46), smoothstep(0.3, 0.6, heat));
|
||||
fire = mix(fire, vec3(1.0, 0.98, 0.88), smoothstep(0.6, 0.9, heat));
|
||||
float glow = exp(-radius * radius / max(0.02, expansion * expansion * 0.85)) * 0.11 * exp(-age * 3.5);
|
||||
float flash = exp(-radius * radius * 35.0) * exp(-age * 26.0);
|
||||
vec3 light = fire * body * fade * (0.5 + turbulence) + vec3(1.0, 0.25, 0.035) * glow + vec3(1.0, 0.97, 0.85) * flash;
|
||||
COLOR = vec4(light * strength, clamp((body * fade + glow + flash) * strength, 0.0, 1.0));
|
||||
}
|
||||
1
resources/effects/ship_fireball.gdshader.uid
Normal file
1
resources/effects/ship_fireball.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://iwpo5xctaw06
|
||||
@ -10,13 +10,13 @@ icon = ExtResource("icon")
|
||||
item_id = "beam_laser"
|
||||
display_name = "Laser wiązkowy"
|
||||
category = 0
|
||||
description = "Oszczędny laser pulsacyjny: małe obrażenia i bardzo duży zasięg. Koszt impulsu rośnie z obrażeniami i addonami egzemplarza."
|
||||
description = "Oszczędny laser pulsacyjny: małe obrażenia i zasięg około jednego ekranu. Koszt impulsu rośnie z obrażeniami i addonami egzemplarza."
|
||||
primary_stat = "damage"
|
||||
primary_value = 3.0
|
||||
stat_suffix = " DMG"
|
||||
weapon_kind = 2
|
||||
fire_interval = 0.2
|
||||
weapon_range = 800.0
|
||||
weapon_range = 600.0
|
||||
energy_cost = 2.5
|
||||
damage_lower_roll_min = 1
|
||||
damage_lower_roll_max = 3
|
||||
|
||||
74
scripts/game/asteroid_destruction_effect.gd
Normal file
74
scripts/game/asteroid_destruction_effect.gd
Normal file
@ -0,0 +1,74 @@
|
||||
extends Node2D
|
||||
|
||||
const DURATION := 1.6
|
||||
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)
|
||||
for y in 4:
|
||||
for x in 4:
|
||||
var a := grid[y * 5 + x]
|
||||
var b := grid[y * 5 + x + 1]
|
||||
var c := grid[(y + 1) * 5 + x + 1]
|
||||
var d := grid[(y + 1) * 5 + x]
|
||||
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) * sprite_scale
|
||||
var points := PackedVector2Array()
|
||||
var uv := PackedVector2Array()
|
||||
for vertex: Vector2 in triangle:
|
||||
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(70.0, 170.0),
|
||||
"angle": 0.0, "spin": _rng.randf_range(-4.0, 4.0)})
|
||||
_dust_color = Color("a1c7df") if "ice" in _texture.resource_path else 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 _process(delta: float) -> void:
|
||||
_elapsed += delta
|
||||
if _elapsed >= DURATION:
|
||||
queue_free()
|
||||
return
|
||||
for fragment in _fragments:
|
||||
fragment.position += fragment.velocity * delta
|
||||
fragment.velocity *= exp(-1.2 * delta)
|
||||
fragment.angle += fragment.spin * delta
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
var fade := 1.0 - smoothstep(0.65, DURATION, _elapsed)
|
||||
for fragment in _fragments:
|
||||
draw_set_transform(fragment.position, fragment.angle)
|
||||
draw_polygon(fragment.points, PackedColorArray([Color(1.0, 1.0, 1.0, fade)]), 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))
|
||||
1
scripts/game/asteroid_destruction_effect.gd.uid
Normal file
1
scripts/game/asteroid_destruction_effect.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dnqrd4p1yobkx
|
||||
@ -89,7 +89,7 @@ func get_visual_size() -> float:
|
||||
return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not is_instance_valid(_player):
|
||||
if not is_instance_valid(_player) or bool(_player.get("_is_destroyed")):
|
||||
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
306
scripts/game/player_destruction_effect.gd
Normal file
306
scripts/game/player_destruction_effect.gd
Normal file
@ -0,0 +1,306 @@
|
||||
extends Node2D
|
||||
|
||||
signal finished
|
||||
|
||||
const CHARGE_TIME := 2.3
|
||||
const DURATION := CHARGE_TIME + 3.0
|
||||
const EXPLOSION_AUDIO_PATH := "res://assets/sounds/effects/explosion.mp3"
|
||||
const FIRE_SHADER := preload("res://resources/effects/ship_fireball.gdshader")
|
||||
static var _smoke_texture: Texture2D
|
||||
var _elapsed := 0.0
|
||||
var _texture: Texture2D
|
||||
var _ship_scale := Vector2.ONE
|
||||
var _drift := Vector2.ZERO
|
||||
var _fragments: Array[Dictionary] = []
|
||||
var _sparks: Array[Dictionary] = []
|
||||
var _smoke: Array[Dictionary] = []
|
||||
var _camera: Camera2D
|
||||
var _camera_offset := Vector2.ZERO
|
||||
var _detonated := false
|
||||
var _smoke_timer := 0.0
|
||||
var _done := false
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var _fireball: MeshInstance2D
|
||||
var _fire_material: ShaderMaterial
|
||||
var _charge_audio: AudioStreamPlayer2D
|
||||
var _burns: Array[Dictionary] = []
|
||||
var _ignition_timer := 0.0
|
||||
|
||||
func configure(sprite: Sprite2D, drift: Vector2, camera: Camera2D) -> void:
|
||||
_texture = sprite.texture
|
||||
global_position = sprite.global_position
|
||||
global_rotation = sprite.global_rotation
|
||||
_ship_scale = sprite.global_scale
|
||||
_drift = drift.rotated(-global_rotation) * 0.12
|
||||
_camera = camera
|
||||
if is_instance_valid(_camera):
|
||||
_camera_offset = _camera.offset
|
||||
|
||||
func _ready() -> void:
|
||||
z_index = 45
|
||||
light_mask = 4
|
||||
_rng.randomize()
|
||||
_build_fragments()
|
||||
_prepare_smoke_texture()
|
||||
_ignite_burn()
|
||||
_ignite_burn()
|
||||
_fireball = MeshInstance2D.new()
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(240, 240)
|
||||
_fireball.mesh = quad
|
||||
_fire_material = ShaderMaterial.new()
|
||||
_fire_material.shader = FIRE_SHADER
|
||||
_fire_material.set_shader_parameter("variation", _rng.randf_range(0, 100))
|
||||
_fireball.material = _fire_material
|
||||
_fireball.light_mask = 4
|
||||
_fireball.z_index = 1
|
||||
_fireball.hide()
|
||||
add_child(_fireball)
|
||||
_play_charge_sound()
|
||||
|
||||
func _build_fragments() -> void:
|
||||
if _texture == null:
|
||||
return
|
||||
var dimensions := _texture.get_size()
|
||||
var image := _texture.get_image()
|
||||
for y in 8:
|
||||
for x in 8:
|
||||
var a := Vector2(x, y) * dimensions / 8.0
|
||||
var b := Vector2(x + 1, y) * dimensions / 8.0
|
||||
var c := Vector2(x + 1, y + 1) * dimensions / 8.0
|
||||
var d := Vector2(x, y + 1) * dimensions / 8.0
|
||||
for triangle in [[a, b, c], [a, c, d]]:
|
||||
var center: Vector2 = (triangle[0] + triangle[1] + triangle[2]) / 3.0
|
||||
if image != null and image.get_pixel(clampi(int(center.x / dimensions.x * image.get_width()), 0, image.get_width() - 1), clampi(int(center.y / dimensions.y * image.get_height()), 0, image.get_height() - 1)).a < 0.1:
|
||||
continue
|
||||
var origin := (center - dimensions * 0.5) * _ship_scale
|
||||
var points := PackedVector2Array()
|
||||
var uv := PackedVector2Array()
|
||||
for vertex in triangle:
|
||||
points.append((vertex - center) * _ship_scale)
|
||||
uv.append(vertex / dimensions)
|
||||
var wreck := origin.length() < 12.0
|
||||
var radial := origin.normalized() if origin.length_squared() > 0.1 else Vector2.RIGHT.rotated(_rng.randf_range(0, TAU))
|
||||
_fragments.append({"points": points, "uv": uv, "position": origin, "velocity": radial.rotated(_rng.randf_range(-0.45, 0.45)) * _rng.randf_range(65, 155) + _drift if not wreck else radial * 5.0, "angle": 0.0, "spin": _rng.randf_range(-5, 5) if not wreck else _rng.randf_range(-0.3, 0.3), "wreck": wreck})
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _done and _elapsed >= DURATION + 1.8:
|
||||
_smoke.clear()
|
||||
_sparks.clear()
|
||||
set_process(false)
|
||||
queue_redraw()
|
||||
return
|
||||
_elapsed += delta
|
||||
if not _detonated and _elapsed < CHARGE_TIME:
|
||||
_ignition_timer -= delta
|
||||
if _ignition_timer <= 0.0:
|
||||
_ignite_burn()
|
||||
var progress := _elapsed / CHARGE_TIME
|
||||
_ignition_timer = _rng.randf_range(0.1, 0.22) * lerpf(1.0, 0.55, progress)
|
||||
for index in range(_burns.size() - 1, -1, -1):
|
||||
var burn := _burns[index]
|
||||
burn["age"] += delta
|
||||
var age := clampf(float(burn["age"]) / float(burn["life"]), 0, 1)
|
||||
var intensity := sin(age * PI)
|
||||
var wobble := sin(_elapsed * 23 + float(burn["phase"]))
|
||||
var flame: MeshInstance2D = burn["visual"]
|
||||
flame.position = burn["position"] + Vector2.UP.rotated(float(burn["angle"])) * 4 * intensity
|
||||
flame.rotation = float(burn["angle"]) + wobble * 0.3
|
||||
flame.scale = Vector2(0.8 + intensity * 0.3, 0.7 + intensity * 0.4 + wobble * 0.1)
|
||||
var material: ShaderMaterial = burn["material"]
|
||||
material.set_shader_parameter("phase", 0.12 + age * 0.65)
|
||||
material.set_shader_parameter("strength", intensity * (1.4 + wobble * 0.3))
|
||||
if age >= 1.0:
|
||||
flame.queue_free()
|
||||
_burns.remove_at(index)
|
||||
if _elapsed >= CHARGE_TIME and not _detonated:
|
||||
_detonated = true
|
||||
for burn in _burns:
|
||||
burn["visual"].queue_free()
|
||||
_burns.clear()
|
||||
_play_explosion_sound()
|
||||
for index in 16:
|
||||
var outward := Vector2.RIGHT.rotated(_rng.randf_range(0, TAU))
|
||||
_smoke.append({"position": outward * _rng.randf_range(4, 18), "velocity": outward * _rng.randf_range(25, 65), "age": -0.08, "life": _rng.randf_range(1.8, 2.8), "radius": _rng.randf_range(15, 28)})
|
||||
for index in 75:
|
||||
var direction := Vector2.RIGHT.rotated(_rng.randf_range(0, TAU))
|
||||
_sparks.append({"position": direction * _rng.randf_range(3, 24), "velocity": direction * _rng.randf_range(80, 330), "life": _rng.randf_range(0.45, 1.8), "age": 0.0})
|
||||
if _detonated:
|
||||
for fragment in _fragments:
|
||||
fragment["position"] += fragment["velocity"] * delta
|
||||
fragment["velocity"] *= exp(-0.7 * delta)
|
||||
fragment["angle"] += float(fragment["spin"]) * delta
|
||||
_smoke_timer += delta
|
||||
var smoke_interval := 0.065 if _detonated else 0.12
|
||||
while _smoke_timer >= smoke_interval and _elapsed < CHARGE_TIME + 2.5:
|
||||
_smoke_timer -= smoke_interval
|
||||
_emit_smoke()
|
||||
for index in range(_sparks.size() - 1, -1, -1):
|
||||
var spark := _sparks[index]
|
||||
spark["position"] += spark["velocity"] * delta
|
||||
spark["velocity"] *= exp(-1.3 * delta)
|
||||
spark["age"] += delta
|
||||
if float(spark["age"]) >= float(spark["life"]):
|
||||
_sparks.remove_at(index)
|
||||
for index in range(_smoke.size() - 1, -1, -1):
|
||||
var puff := _smoke[index]
|
||||
puff["position"] += puff["velocity"] * delta
|
||||
puff["age"] += delta
|
||||
if float(puff["age"]) >= float(puff["life"]):
|
||||
_smoke.remove_at(index)
|
||||
if not _done:
|
||||
_update_camera()
|
||||
var blast_age := maxf(0, _elapsed - CHARGE_TIME)
|
||||
_fireball.visible = _detonated and blast_age < 1.3
|
||||
_fire_material.set_shader_parameter("phase", blast_age)
|
||||
queue_redraw()
|
||||
if _elapsed >= DURATION and not _done:
|
||||
_done = true
|
||||
_restore_camera()
|
||||
finished.emit()
|
||||
|
||||
func _hull_burn_position() -> Vector2:
|
||||
if not _fragments.is_empty():
|
||||
return _fragments[_rng.randi_range(0, _fragments.size() - 1)]["position"]
|
||||
return Vector2.ZERO
|
||||
|
||||
func _ignite_burn() -> void:
|
||||
var origin := _hull_burn_position()
|
||||
var angle := origin.angle() + PI * 0.5 + _rng.randf_range(-0.65, 0.65)
|
||||
var flame := MeshInstance2D.new()
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(_rng.randf_range(24, 34), _rng.randf_range(36, 52))
|
||||
flame.mesh = quad
|
||||
var material := ShaderMaterial.new()
|
||||
material.shader = FIRE_SHADER
|
||||
material.set_shader_parameter("variation", _rng.randf_range(0, 100))
|
||||
material.set_shader_parameter("strength", 0.0)
|
||||
flame.material = material
|
||||
flame.position = origin
|
||||
flame.rotation = angle
|
||||
flame.z_index = 1
|
||||
add_child(flame)
|
||||
_burns.append({"position": origin, "angle": angle, "age": 0.0, "life": _rng.randf_range(0.3, 0.7), "phase": _rng.randf_range(0, TAU), "visual": flame, "material": material})
|
||||
var direction := Vector2.UP.rotated(angle)
|
||||
for index in _rng.randi_range(3, 6):
|
||||
_sparks.append({"position": origin, "velocity": direction.rotated(_rng.randf_range(-0.9, 0.9)) * _rng.randf_range(25, 100), "life": _rng.randf_range(0.18, 0.5), "age": 0.0})
|
||||
|
||||
func _emit_smoke() -> void:
|
||||
var position := Vector2.ZERO
|
||||
if not _detonated:
|
||||
position = _burns[_rng.randi_range(0, _burns.size() - 1)]["position"] if not _burns.is_empty() else _hull_burn_position()
|
||||
elif not _fragments.is_empty() and _rng.randf() < 0.7:
|
||||
position = _fragments[_rng.randi_range(0, _fragments.size() - 1)]["position"]
|
||||
_smoke.append({"position": position, "velocity": Vector2(_rng.randf_range(-12, 12), _rng.randf_range(-22, 8)), "age": 0.0, "life": _rng.randf_range(0.9, 1.7), "radius": _rng.randf_range(5, 12)})
|
||||
|
||||
func _update_camera() -> void:
|
||||
if not is_instance_valid(_camera):
|
||||
return
|
||||
var time := _elapsed - CHARGE_TIME
|
||||
var strength := 0.8 if time < 0.0 else 7.0 * exp(-5.0 * time)
|
||||
_camera.offset = _camera_offset + Vector2(sin(_elapsed * 83), cos(_elapsed * 67)) * strength
|
||||
|
||||
func _restore_camera() -> void:
|
||||
if is_instance_valid(_camera):
|
||||
_camera.offset = _camera_offset
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_restore_camera()
|
||||
|
||||
func _draw() -> void:
|
||||
if _texture == null:
|
||||
return
|
||||
var time := maxf(0.0, _elapsed - CHARGE_TIME)
|
||||
for puff in _smoke:
|
||||
var progress: float = float(puff["age"]) / float(puff["life"])
|
||||
var opacity := (1.0 - progress) * minf(1, float(puff["age"]) * 8.0)
|
||||
var radius := float(puff["radius"]) * (1 + maxf(0, progress) * 1.5)
|
||||
var soot := Color("6c3e25").lerp(Color("3c4149"), clampf(progress * 3, 0, 1))
|
||||
soot.a = maxf(0, opacity) * 0.7
|
||||
draw_texture_rect(_smoke_texture, Rect2(Vector2(puff["position"]) - Vector2.ONE * radius, Vector2.ONE * radius * 2), false, soot)
|
||||
if not _detonated:
|
||||
var progress := clampf(_elapsed / CHARGE_TIME, 0, 1)
|
||||
var pulse := (sin(_elapsed * 19) + 1.0) * 0.5
|
||||
var dimensions := _texture.get_size() * _ship_scale
|
||||
draw_texture_rect(_texture, Rect2(-dimensions * 0.5, dimensions), false, Color(1, 0.95 - progress * 0.1 + pulse * 0.05, 0.88 - progress * 0.08 + pulse * 0.1))
|
||||
_draw_sparks()
|
||||
return
|
||||
for fragment in _fragments:
|
||||
var fade := 1.0 if bool(fragment["wreck"]) else clampf((3.4 - time) / 1.1, 0, 1)
|
||||
var shade := lerpf(1.0, 0.22 if bool(fragment["wreck"]) else 0.55, minf(1, time))
|
||||
draw_set_transform(fragment["position"], fragment["angle"])
|
||||
draw_polygon(fragment["points"], PackedColorArray([Color(shade, shade * 0.85, shade * 0.7, fade)]), fragment["uv"], _texture)
|
||||
if time < 1.7 and not bool(fragment["wreck"]):
|
||||
draw_line(Vector2.ZERO, -Vector2(fragment["velocity"]).normalized().rotated(-float(fragment["angle"])) * 9, Color(1, 0.35, 0.06, (1.0 - time / 1.7) * 0.7), 2, true)
|
||||
draw_set_transform(Vector2.ZERO)
|
||||
if time < 0.55:
|
||||
var fade := 1.0 - time / 0.55
|
||||
for layer in 3:
|
||||
draw_arc(Vector2.ZERO, 18 + time * 230 + layer * 2, 0, TAU, 96, Color(0.85, 0.75, 0.6, fade * 0.08), 2, true)
|
||||
_draw_sparks()
|
||||
if time > 0.8:
|
||||
for index in 4:
|
||||
var flicker := maxf(0, sin(_elapsed * 17 + index * 8)) * clampf((3.3 - time) / 2, 0, 1)
|
||||
draw_circle(Vector2.RIGHT.rotated(index * 2.3) * (4 + index * 2), 2 + flicker * 2, Color(1, 0.28, 0.04, flicker * 0.75))
|
||||
|
||||
func _draw_sparks() -> void:
|
||||
for spark in _sparks:
|
||||
var fade := 1 - float(spark["age"]) / float(spark["life"])
|
||||
var length := clampf(Vector2(spark["velocity"]).length() * 0.03, 2, 8)
|
||||
draw_line(spark["position"], spark["position"] - Vector2(spark["velocity"]).normalized() * length, Color(1, 0.65 + fade * 0.2, 0.2, fade), 1.2, true)
|
||||
|
||||
func _prepare_smoke_texture() -> void:
|
||||
if _smoke_texture != null:
|
||||
return
|
||||
var image := Image.create(96, 96, false, Image.FORMAT_RGBA8)
|
||||
var noise := FastNoiseLite.new()
|
||||
noise.seed = 71539
|
||||
noise.frequency = 0.06
|
||||
noise.fractal_octaves = 4
|
||||
for y in 96:
|
||||
for x in 96:
|
||||
var p := (Vector2(x, y) - Vector2(47.5, 47.5)) / 47.5
|
||||
var radial := 1.0 - smoothstep(0.2, 1.0, p.length())
|
||||
var density := clampf(noise.get_noise_2d(x, y) * 0.8 + 0.55, 0, 1)
|
||||
image.set_pixel(x, y, Color(1, 1, 1, radial * density))
|
||||
_smoke_texture = ImageTexture.create_from_image(image)
|
||||
|
||||
func _play_explosion_sound() -> void:
|
||||
if is_instance_valid(_charge_audio):
|
||||
_charge_audio.stop()
|
||||
_charge_audio.queue_free()
|
||||
var audio := AudioStreamPlayer2D.new()
|
||||
audio.name = "ExplosionAudio"
|
||||
audio.stream = load(EXPLOSION_AUDIO_PATH) as AudioStream
|
||||
if audio.stream == null:
|
||||
audio.free()
|
||||
return
|
||||
audio.volume_db = -6
|
||||
audio.max_distance = 1800
|
||||
add_child(audio)
|
||||
audio.finished.connect(audio.queue_free)
|
||||
audio.play()
|
||||
|
||||
func _play_charge_sound() -> void:
|
||||
var rate := 22050
|
||||
var frames := int(rate * CHARGE_TIME)
|
||||
var data := PackedByteArray()
|
||||
data.resize(frames * 2)
|
||||
var filtered := 0.0
|
||||
for index in frames:
|
||||
var t := float(index) / rate
|
||||
filtered = lerpf(filtered, _rng.randf_range(-1, 1), 0.16)
|
||||
var value := sin(TAU * (80 + t * 70 / CHARGE_TIME) * t) * 0.06 * (t / CHARGE_TIME) + filtered * 0.025
|
||||
data.encode_s16(index * 2, int(clampf(value, -1, 1) * 25000))
|
||||
var sound := AudioStreamWAV.new()
|
||||
sound.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
sound.mix_rate = rate
|
||||
sound.data = data
|
||||
var audio := AudioStreamPlayer2D.new()
|
||||
_charge_audio = audio
|
||||
audio.stream = sound
|
||||
audio.volume_db = -8
|
||||
audio.max_distance = 1800
|
||||
add_child(audio)
|
||||
audio.finished.connect(audio.queue_free)
|
||||
audio.play()
|
||||
1
scripts/game/player_destruction_effect.gd.uid
Normal file
1
scripts/game/player_destruction_effect.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b3vtjeqhycmqk
|
||||
@ -8,6 +8,7 @@ const CAPTAIN_BALANCE := preload("res://scripts/captain/captain_balance.gd")
|
||||
const SHIP_CATALOG := preload("res://scripts/game/ship_catalog.gd")
|
||||
const COMBAT_FLASH := preload("res://scripts/game/combat_flash.gd")
|
||||
const MINI_BOOST_ENERGY_PER_SECOND := 20.0
|
||||
const DESTRUCTION_EFFECT := preload("res://scripts/game/player_destruction_effect.gd")
|
||||
|
||||
@export var max_speed := 76.0
|
||||
@export var reverse_max_speed := 210.0
|
||||
@ -212,19 +213,45 @@ func _update_shield_visual() -> void:
|
||||
_shield_visual.call("set_shield_values", shields, max_shields)
|
||||
|
||||
func _destroy_ship() -> void:
|
||||
if _is_destroyed:
|
||||
return
|
||||
_is_destroyed = true
|
||||
var death_velocity := velocity
|
||||
velocity = Vector2.ZERO
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
var tween := create_tween()
|
||||
tween.tween_property(self, "modulate:a", 0.0, 1.1)
|
||||
_restart_level_after_destruction()
|
||||
_mini_boost_active = false
|
||||
var explosion := Node2D.new()
|
||||
explosion.name = "PlayerDestructionEffect"
|
||||
explosion.set_script(DESTRUCTION_EFFECT)
|
||||
explosion.call("configure", _sprite, death_velocity, get_node_or_null("Camera2D"))
|
||||
get_parent().add_child(explosion)
|
||||
explosion.connect("finished", _show_defeat_screen)
|
||||
_sprite.hide()
|
||||
for node_name in ["ShieldVisual", "EngineVisual", "EnergyIndicator", "SolarEnergyEffect"]:
|
||||
var visual := get_node_or_null(node_name) as CanvasItem
|
||||
if visual != null:
|
||||
visual.hide()
|
||||
$EngineAudio.stop()
|
||||
$EngineAudio.set_process(false)
|
||||
$EngineVisual.set_process(false)
|
||||
$ShieldVisual.set_process(false)
|
||||
var radiation := get_node_or_null("RadiationStatus")
|
||||
if radiation != null:
|
||||
radiation.set_physics_process(false)
|
||||
|
||||
func _show_defeat_screen() -> void:
|
||||
if not is_inside_tree() or get_parent().has_node("DefeatOverlay"):
|
||||
return
|
||||
var overlay := CanvasLayer.new()
|
||||
overlay.set_script(preload("res://scripts/ui/defeat_overlay.gd"))
|
||||
overlay.set("ship", self)
|
||||
get_parent().add_child(overlay)
|
||||
|
||||
func _restart_level_after_destruction() -> void:
|
||||
await get_tree().create_timer(1.1).timeout
|
||||
if is_inside_tree():
|
||||
$ShipInventory.preserve_for_restart()
|
||||
get_tree().reload_current_scene()
|
||||
get_tree().reload_current_scene.call_deferred()
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if _is_destroyed or is_docked:
|
||||
|
||||
@ -4,6 +4,7 @@ const GENERATOR := preload("res://scripts/items/item_generator.gd")
|
||||
const ASTEROID_TEXTURES := [preload("res://assets/asteroids/asteroid_rock.png"), preload("res://assets/asteroids/asteroid_metal.png"), preload("res://assets/asteroids/asteroid_ice.png")]
|
||||
const WRECK_TEXTURES := [preload("res://assets/wrecks/wreck_frigate.png"), preload("res://assets/wrecks/wreck_freighter.png"), preload("res://assets/wrecks/wreck_interceptor.png")]
|
||||
const LOOT := preload("res://scripts/items/loot_pickup.gd")
|
||||
const ASTEROID_DESTRUCTION := preload("res://scripts/game/asteroid_destruction_effect.gd")
|
||||
var kind := "wreck"
|
||||
var poi_id := ""
|
||||
var reward_seed := 1
|
||||
@ -147,6 +148,13 @@ func claim() -> bool:
|
||||
popup.position = position + Vector2(-80, -65)
|
||||
get_parent().add_child.call_deferred(popup)
|
||||
set_physics_process(false)
|
||||
if kind == "asteroid" and is_instance_valid(_asteroid_sprite):
|
||||
var debris := Node2D.new()
|
||||
debris.set_script(ASTEROID_DESTRUCTION)
|
||||
debris.name = "AsteroidDebris"
|
||||
debris.call("configure", _asteroid_sprite)
|
||||
# The POI may be destroyed during a physics query; add debris outside it.
|
||||
get_parent().add_child.call_deferred(debris)
|
||||
queue_free()
|
||||
return true
|
||||
|
||||
|
||||
62
scripts/ui/defeat_overlay.gd
Normal file
62
scripts/ui/defeat_overlay.gd
Normal file
@ -0,0 +1,62 @@
|
||||
extends CanvasLayer
|
||||
|
||||
var ship: CharacterBody2D
|
||||
var _retrying := false
|
||||
|
||||
func _ready() -> void:
|
||||
layer = 40
|
||||
name = "DefeatOverlay"
|
||||
var root := Control.new()
|
||||
add_child(root)
|
||||
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
var shade := ColorRect.new()
|
||||
shade.color = Color(0.015, 0.025, 0.045, 0.55)
|
||||
root.add_child(shade)
|
||||
shade.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
var center := CenterContainer.new()
|
||||
root.add_child(center)
|
||||
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size.x = 390
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color("0b1520f5")
|
||||
style.border_color = Color("bf604d")
|
||||
style.set_border_width_all(1)
|
||||
style.border_width_top = 3
|
||||
style.set_content_margin_all(24)
|
||||
style.set_corner_radius_all(5)
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
center.add_child(panel)
|
||||
var contents := VBoxContainer.new()
|
||||
contents.add_theme_constant_override("separation", 14)
|
||||
panel.add_child(contents)
|
||||
var title := Label.new()
|
||||
title.text = "STATEK ZNISZCZONY"
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 20)
|
||||
title.add_theme_color_override("font_color", Color("ffae8c"))
|
||||
contents.add_child(title)
|
||||
var note := Label.new()
|
||||
note.text = "Układ %d\nWyposażenie i zapasy zostają zachowane." % GameState.system_level
|
||||
note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
note.add_theme_font_size_override("font_size", 11)
|
||||
note.add_theme_color_override("font_color", Color("a6bdce"))
|
||||
contents.add_child(note)
|
||||
var retry := Button.new()
|
||||
retry.name = "RetryButton"
|
||||
retry.text = "SPRÓBUJ PONOWNIE [R]"
|
||||
preload("res://scripts/ui/hud_button_style.gd").apply(retry)
|
||||
var key := InputEventKey.new()
|
||||
key.physical_keycode = KEY_R
|
||||
retry.shortcut = Shortcut.new()
|
||||
retry.shortcut.events = [key]
|
||||
retry.pressed.connect(_retry)
|
||||
contents.add_child(retry)
|
||||
root.modulate.a = 0
|
||||
create_tween().tween_property(root, "modulate:a", 1.0, 0.3)
|
||||
|
||||
func _retry() -> void:
|
||||
if _retrying or not is_instance_valid(ship):
|
||||
return
|
||||
_retrying = true
|
||||
ship.call("_restart_level_after_destruction")
|
||||
1
scripts/ui/defeat_overlay.gd.uid
Normal file
1
scripts/ui/defeat_overlay.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://coeyame45wvng
|
||||
63
tests/items/player_destruction_test.gd
Normal file
63
tests/items/player_destruction_test.gd
Normal file
@ -0,0 +1,63 @@
|
||||
extends Node2D
|
||||
|
||||
var _finished_count := 0
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.start_new_run()
|
||||
var game := preload("res://scenes/game/game.tscn").instantiate()
|
||||
add_child(game)
|
||||
game.set_physics_process(false)
|
||||
var ship: CharacterBody2D = game.get_node("PlayerShip")
|
||||
ship.set_physics_process(false)
|
||||
ship.get_node("WeaponController").set_physics_process(false)
|
||||
var camera: Camera2D = ship.get_node("Camera2D")
|
||||
camera.offset = Vector2(3, 5)
|
||||
ship.velocity = Vector2(100, -40)
|
||||
ship.call("_destroy_ship")
|
||||
assert(ship.get("_is_destroyed") and ship.collision_layer == 0 and ship.velocity == Vector2.ZERO)
|
||||
assert(not ship.get_node("Sprite2D").visible and not ship.get_node("ShieldVisual").visible)
|
||||
var effect: Node = game.get_node("PlayerDestructionEffect")
|
||||
effect.set_process(false)
|
||||
assert(effect.get("_texture") == ship.get_node("Sprite2D").texture)
|
||||
assert(effect.get("_fragments").size() >= 4)
|
||||
assert(not game.has_node("DefeatOverlay"))
|
||||
assert(not effect.has_node("ExplosionAudio"))
|
||||
effect.finished.connect(func() -> void: _finished_count += 1)
|
||||
effect._process(0.2)
|
||||
assert(not effect.get("_detonated") and camera.offset != Vector2(3, 5))
|
||||
for step in 18:
|
||||
effect._process(0.1)
|
||||
assert(not effect.get("_detonated") and not game.has_node("DefeatOverlay"))
|
||||
assert(effect.get("_smoke").size() > 0)
|
||||
effect._process(0.35)
|
||||
assert(effect.get("_detonated") and effect.get("_sparks").size() > 30)
|
||||
assert(effect.has_node("ExplosionAudio"))
|
||||
assert(effect.get_node("ExplosionAudio").stream.resource_path == "res://assets/sounds/effects/explosion.mp3")
|
||||
assert(not game.has_node("DefeatOverlay"))
|
||||
effect._process(2.7)
|
||||
assert(not game.has_node("DefeatOverlay"))
|
||||
effect._process(0.3)
|
||||
assert(game.has_node("DefeatOverlay") and _finished_count == 1)
|
||||
assert(camera.offset == Vector2(3, 5))
|
||||
effect._process(0.1)
|
||||
ship.call("_destroy_ship")
|
||||
assert(_finished_count == 1)
|
||||
var deaths := 0
|
||||
for child in game.get_children():
|
||||
if child.name == "PlayerDestructionEffect":
|
||||
deaths += 1
|
||||
assert(deaths == 1)
|
||||
for chassis in preload("res://scripts/game/ship_catalog.gd").SHIPS:
|
||||
var source := Sprite2D.new()
|
||||
source.texture = load(preload("res://scripts/game/ship_catalog.gd").texture_path(chassis))
|
||||
source.scale = Vector2.ONE * 64.0 / source.texture.get_width()
|
||||
var variant := Node2D.new()
|
||||
variant.set_script(preload("res://scripts/game/player_destruction_effect.gd"))
|
||||
variant.configure(source, Vector2.ZERO, null)
|
||||
add_child(variant)
|
||||
variant.set_process(false)
|
||||
assert(variant.get("_fragments").size() >= 4)
|
||||
source.free()
|
||||
variant.queue_free()
|
||||
print("PLAYER_DESTRUCTION_TEST: PASS (ship texture fragments, collision shutdown, overload, detonation, delayed defeat, camera restoration, single death)")
|
||||
get_tree().quit()
|
||||
1
tests/items/player_destruction_test.gd.uid
Normal file
1
tests/items/player_destruction_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://xkpr5tm2f1ed
|
||||
4
tests/items/player_destruction_test.tscn
Normal file
4
tests/items/player_destruction_test.tscn
Normal file
@ -0,0 +1,4 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://tests/items/player_destruction_test.gd" id="1"]
|
||||
[node name="PlayerDestructionTest" type="Node2D"]
|
||||
script = ExtResource("1")
|
||||
@ -80,7 +80,11 @@ func _ready() -> void:
|
||||
assert(menu.visible)
|
||||
player.position = initial_data["player_start"]
|
||||
player.call("take_damage", 100000.0)
|
||||
await get_tree().create_timer(1.3).timeout
|
||||
await get_tree().create_timer(preload("res://scripts/game/player_destruction_effect.gd").DURATION + 0.2).timeout
|
||||
assert(game.has_node("DefeatOverlay"))
|
||||
game.get_node("DefeatOverlay").call("_retry")
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
game = get_tree().current_scene as Node2D
|
||||
_freeze(game)
|
||||
player = game.get_node("PlayerShip") as CharacterBody2D
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user