This commit is contained in:
Tomasz Boruc 2026-10-14 22:30:56 +02:00
parent 9808a03d13
commit 5dd71181bc
18 changed files with 631 additions and 9 deletions

Binary file not shown.

View 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

View 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));
}

View File

@ -0,0 +1 @@
uid://iwpo5xctaw06

View File

@ -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

View 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))

View File

@ -0,0 +1 @@
uid://dnqrd4p1yobkx

View File

@ -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

View 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()

View File

@ -0,0 +1 @@
uid://b3vtjeqhycmqk

View File

@ -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:

View File

@ -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

View 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")

View File

@ -0,0 +1 @@
uid://coeyame45wvng

View 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()

View File

@ -0,0 +1 @@
uid://xkpr5tm2f1ed

View 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")

View File

@ -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