Compare commits

...

2 Commits

Author SHA1 Message Date
9aa2dfebd6 Zmany 2026-10-17 14:28:20 +02:00
458db974c0 Zmiany 2026-10-16 23:36:49 +02:00
61 changed files with 1118 additions and 228 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d4hxo2htne3yy"
path="res://.godot/imported/combat_drone.png-1ee839fafe5ff3881843088267b833ba.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/Ships/combat_drone.png"
dest_files=["res://.godot/imported/combat_drone.png-1ee839fafe5ff3881843088267b833ba.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://baxcqpl7y3dfd"
path="res://.godot/imported/electrician.png-cd4ace91776ba47a19c9950998a779a9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/electrician.png"
dest_files=["res://.godot/imported/electrician.png-cd4ace91776ba47a19c9950998a779a9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://huejfa7inar0"
path="res://.godot/imported/combat_drones_art.png-ce5df9b0b0abb663da961f963a875f37.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/combat_drones_art.png"
dest_files=["res://.godot/imported/combat_drones_art.png-ce5df9b0b0abb663da961f963a875f37.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,17 @@
# Elektryk i drony bojowe
Wygenerowano narzędziem wbudowanym `image_gen`, z przezroczystym tłem.
## Elektryk — captain/electrician.png
Use case: stylized-concept. Asset type: square transparent sci-fi spaceship game skill icon, high quality painted game UI art. Subject: Elektryk, futuristic engineer's armored gloved hand holding a compact metallic powerbank with a brilliant cyan lightning core, copper wire contacts and tiny electric arcs. Single strong readable centered silhouette, navy steel copper and electric turquoise palette, crisp bevels, cinematic rim lighting, detailed yet readable at 48px, generous transparent margins. No text, letters, frame, background or watermark. Save the PNG asset for the project.
## Drony bojowe — extensions/combat_drones_art.png
Use case: stylized-concept. Asset type: transparent square sci-fi game equipment icon. Subject three compact autonomous combat drones in triangular formation, metallic gunmetal hulls with angular swept wings, twin small kinetic gun barrels and bright cyan engine cores. Top down three quarter view, polished hand painted realistic spaceship inventory art, crisp strong silhouettes readable at 48px, restrained gold accents and cyan rim lighting, isolated centered with transparent margins. No lettering, frame, background or watermark. Save PNG for game project.
## Dron w rozgrywce — ../Ships/combat_drone.png
Wygenerowano wbudowanym `image_gen`, z przezroczystym tłem. Kadłub jest teksturą; silniki i błyski działek animuje combat_drone.gd.
Use case: stylized-concept. Asset type: production gameplay sprite for a top-down 2D space shooter, transparent PNG. ONE small autonomous combat drone spaceship, perfectly orthographic TOP DOWN view, nose pointing straight UP, bilateral symmetry. Compact gunmetal titanium armored fuselage, swept angular short wings, two small forward-facing mechanical autocannons ending near the nose, two cyan engine exhaust ports at rear, tiny turquoise sensors, restrained brass details. Detailed painted realistic sci-fi spacecraft, crisp edges and clear silhouette readable at 44 pixels tall. Centered entire ship with 12 percent transparent padding. Lighting from upper left, dark panel grooves and bright metallic edge highlights. No pilot, perspective, isometric angle, cast shadow, engine flame, muzzle flash, text, UI, frame, scenery, stars, watermark or other spacecraft. Engines and muzzle flashes are animated separately in the game.

Binary file not shown.

View File

@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://nscqyrjfn7u4"
path="res://.godot/imported/menu_music2.mp3-7efdba0b08e1c22b65cc3d25ef9b8f90.mp3str"
[deps]
source_file="res://assets/sounds/music/menu_music2.mp3"
dest_files=["res://.godot/imported/menu_music2.mp3-7efdba0b08e1c22b65cc3d25ef9b8f90.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@ -2,8 +2,7 @@
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://bpaiuqwsxswii" path="res://scripts/abilities/afterburner_ability.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/icons/afterburner.png" id="icon"]
[ext_resource type="Texture2D" uid="uid://kukr8bhgqa7p" path="res://assets/icons/afterburner.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
@ -12,9 +11,9 @@ description = "Szybko rozpędza do 300% zwykłej prędkości (+200%), następnie
[resource]
script = ExtResource("1")
icon = ExtResource("icon")
item_id = "afterburner"
display_name = "Dopalacz"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie silnika."
active_ability = SubResource("Ability")

View File

@ -1,19 +1,22 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/icons/extensions/cloak_art.png" id="icon"]
[gd_resource type="Resource" script_class="ItemDefinition" format=3 uid="uid://drdg44cvejr2n"]
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://mjofjrs3v7by" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" uid="uid://ddqhcasuuecgc" path="res://assets/icons/extensions/cloak_art.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
kind = 1
ability_id = "cloak"
cooldown = 60.0
display_name = "Kamuflaż"
description = "Przez 10 s statek staje się niemal niewidoczny. Przeciwnicy nie wykrywają go ani nie śledzą. Nie zapewnia odporności na już wystrzelone pociski. Odnowienie: 60 s."
kind = 1
description = "Przez 10 s statek staje się niemal niewidoczny. Przeciwnicy nie wykrywają go ani nie śledzą. Oddanie strzału przerywa kamuflaż. Nie zapewnia odporności na już wystrzelone pociski. Odnowienie: 60 s."
[resource]
script = ExtResource("1")
icon = ExtResource("icon")
item_id = "cloak"
display_name = "Kamuflaż"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie statku. Zamontuj w slocie rozszerzeń i użyj przypisanego klawisza 1–5."
active_ability = SubResource("Ability")

View File

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="ItemDefinition" format=3 uid="uid://cvauqvsl4x5gk"]
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://mjofjrs3v7by" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" uid="uid://huejfa7inar0" path="res://assets/icons/extensions/combat_drones_art.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
kind = 5
ability_id = "combat_drones"
cooldown = 30.0
display_name = "Drony bojowe"
description = "Wypuszcza 3 drony na 10 s. Strzelają działkami mechanicznymi; łącznie zadają 50% DPS statku w momencie startu. Koszt: 50 szt. zwykłej amunicji. Odnowienie: 30 s."
[resource]
script = ExtResource("1")
item_id = "combat_drones"
display_name = "Drony bojowe"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie statku. Zamontuj w slocie rozszerzeń i użyj przypisanego klawisza 1–5."
active_ability = SubResource("Ability")

View File

@ -1,19 +1,22 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/icons/extensions/flares_art.png" id="icon"]
[gd_resource type="Resource" script_class="ItemDefinition" format=3 uid="uid://demqfp0to5pnx"]
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://mjofjrs3v7by" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" uid="uid://ccdxl4xyjwyg3" path="res://assets/icons/extensions/flares_art.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
kind = 3
ability_id = "flares"
cooldown = 30.0
display_name = "Flary"
description = "Wypuszcza trzy flary na 10 s. Wrogowie w zasięgu obierają je za cel zamiast gracza. Odnowienie: 30 s."
kind = 3
[resource]
script = ExtResource("1")
icon = ExtResource("icon")
item_id = "flares"
display_name = "Flary"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie statku. Zamontuj w slocie rozszerzeń i użyj przypisanego klawisza 1–5."
active_ability = SubResource("Ability")

View File

@ -1,19 +1,22 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/icons/extensions/mine_art.png" id="icon"]
[gd_resource type="Resource" script_class="ItemDefinition" format=3 uid="uid://cnlcstlicfthn"]
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://mjofjrs3v7by" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" uid="uid://dwv0fw3dyupj3" path="res://assets/icons/extensions/mine_art.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
kind = 2
ability_id = "mine_layer"
cooldown = 5.0
display_name = "Miny"
description = "Zostawia minę na 60 s. Uzbraja się po 0,7 s. Trafiony wróg otrzymuje obrażenia równe 5% maksymalnego kadłuba (bez redukcji absorpcji) i zwalnia o 50% na 5 s. Zrzut co 5 s."
kind = 2
[resource]
script = ExtResource("1")
icon = ExtResource("icon")
item_id = "mine_layer"
display_name = "Miny"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie statku. Zamontuj w slocie rozszerzeń i użyj przypisanego klawisza 1–5."
active_ability = SubResource("Ability")

View File

@ -2,8 +2,7 @@
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://bgofku10gduus" path="res://scripts/abilities/repair_drones_ability.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/icons/repair_drones.png" id="icon"]
[ext_resource type="Texture2D" uid="uid://b1v55dh3vgl78" path="res://assets/icons/repair_drones.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
@ -12,9 +11,9 @@ description = "Naprawiają tyle kadłuba, ile pozwalają części i brakujące p
[resource]
script = ExtResource("1")
icon = ExtResource("icon")
item_id = "repair_drones"
display_name = "Drony naprawcze"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie naprawcze."
active_ability = SubResource("Ability")

View File

@ -2,8 +2,7 @@
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://bwka1dwxq64sl" path="res://scripts/abilities/shield_charge_ability.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/icons/shield_charge.png" id="icon"]
[ext_resource type="Texture2D" uid="uid://bckqak06f6qxc" path="res://assets/icons/shield_charge.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
@ -12,9 +11,9 @@ description = "Przywraca 100 punktów tarcz, do ich maksimum. Wymaga i zużywa 1
[resource]
script = ExtResource("1")
icon = ExtResource("icon")
item_id = "shield_charge"
display_name = "Ładowanie tarcz"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie tarcz."
active_ability = SubResource("Ability")

View File

@ -1,19 +1,22 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/icons/extensions/solar_shield_charge_art.png" id="icon"]
[gd_resource type="Resource" script_class="ItemDefinition" format=3 uid="uid://x5cduw4c4tes"]
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://mjofjrs3v7by" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" uid="uid://jemfbex10v14" path="res://assets/icons/extensions/solar_shield_charge_art.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
kind = 4
ability_id = "solar_shield_charge"
cooldown = 30.0
display_name = "Doładowanie tarcz"
description = "W zasięgu gwiazdy przywraca 20% maksymalnych tarcz na sekundę przez maksymalnie 5 s. Opuszczenie zasięgu kończy efekt. Odnowienie: 30 s."
kind = 4
[resource]
script = ExtResource("1")
icon = ExtResource("icon")
item_id = "solar_shield_charge"
display_name = "Doładowanie tarcz"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie statku. Zamontuj w slocie rozszerzeń i użyj przypisanego klawisza 1–5."
active_ability = SubResource("Ability")

View File

@ -1,19 +1,21 @@
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=5 format=3]
[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" path="res://assets/icons/extensions/teleport_art.png" id="icon"]
[gd_resource type="Resource" script_class="ItemDefinition" format=3 uid="uid://dt406oym4bd6n"]
[ext_resource type="Script" uid="uid://d2fdtd4wjk7d" path="res://scripts/items/item_definition.gd" id="1"]
[ext_resource type="Script" uid="uid://mjofjrs3v7by" path="res://scripts/abilities/tactical_extension_ability.gd" id="2"]
[ext_resource type="Texture2D" uid="uid://c8emovr704ky4" path="res://assets/icons/extensions/teleport_art.png" id="icon"]
[sub_resource type="Resource" id="Ability"]
script = ExtResource("2")
ability_id = "teleport"
cooldown = 60.0
display_name = "Teleport"
description = "Teleportuje w losowe, bezpieczne miejsce systemu. Zużywa całą aktualną energię (wymaga dodatniej energii). Odnowienie: 60 s."
kind = 0
[resource]
script = ExtResource("1")
icon = ExtResource("icon")
item_id = "teleport"
display_name = "Teleport"
icon = ExtResource("icon")
category = 5
description = "Aktywne rozszerzenie statku. Zamontuj w slocie rozszerzeń i użyj przypisanego klawisza 1–5."
active_ability = SubResource("Ability")

View File

@ -1,16 +1,15 @@
[gd_resource type="Theme" load_steps=3 format=3]
[gd_resource type="Theme" format=3 uid="uid://c0tot0b54dn2e"]
[ext_resource type="FontFile" path="res://assets/fonts/font2.ttf" id="1_font"]
[ext_resource type="FontFile" uid="uid://drp85tpfncq5m" path="res://assets/fonts/font2.ttf" id="1_font"]
[sub_resource type="StyleBoxFlat" id="TooltipShell"]
bg_color = Color(0.035, 0.055, 0.085, 1)
content_margin_left = 2.0
content_margin_top = 2.0
content_margin_right = 2.0
content_margin_bottom = 2.0
shadow_size = 0
bg_color = Color(0.035, 0.055, 0.085, 1)
[resource]
default_font = ExtResource("1_font")
TooltipPanel/styles/panel = SubResource("TooltipShell")
TooltipLabel/font_sizes/font_size = 11
TooltipPanel/styles/panel = SubResource("TooltipShell")

View File

@ -93,10 +93,7 @@ ENERGIA: 100 / 100"
[node name="ShipEquipmentPanel" parent="HUD/HUDRoot" unique_id=1637844841 instance=ExtResource("5_equipment")]
layout_mode = 1
offset_left = -270.0
offset_top = 244.0
offset_right = -10.0
offset_bottom = 422.0
grow_vertical = 1
[node name="ShipInventoryPanel" parent="HUD/HUDRoot" unique_id=1116676462 instance=ExtResource("6_inventory")]
layout_mode = 1

View File

@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://csjojorlptcs1" path="res://scripts/ui/main_menu.gd" id="1_script"]
[ext_resource type="Texture2D" uid="uid://bfc6l14s8iwhp" path="res://assets/background/background_menu.jpg" id="2_background"]
[ext_resource type="Script" path="res://scripts/game/scene_music_player.gd" id="3_scene_music"]
[ext_resource type="Script" uid="uid://dvokq8qdccw37" path="res://scripts/game/scene_music_player.gd" id="3_scene_music"]
[sub_resource type="StyleBoxFlat" id="StyleBox_primary"]
content_margin_left = 22.0
@ -67,7 +67,7 @@ script = ExtResource("1_script")
[node name="MenuMusic" type="AudioStreamPlayer" parent="." unique_id=784369201]
script = ExtResource("3_scene_music")
track_path = "res://assets/sounds/music/menu_music.mp3"
track_path = "res://assets/sounds/music/menu_music2.mp3"
playback_volume_db = -13.0
[node name="Background" type="TextureRect" parent="." unique_id=1339534440]

View File

@ -1,9 +1,16 @@
extends ActiveAbility
@export_enum("Teleport", "Cloak", "Mine", "Flares", "Solar shields") var kind := 0
@export_enum("Teleport", "Cloak", "Mine", "Flares", "Solar shields", "Combat drones") var kind := 0
func unavailable_reason(ship: CharacterBody2D) -> String:
match kind:
5:
if float(ship.get_node("ExtensionSystems").get("drones_remaining")) > 0.0:
return "Drony już walczą"
if int(ship.get_node("ShipInventory").get("ammunition")) < 50:
return "Wymagane 50 szt. zwykłej amunicji"
if float(ship.get_node("ShipInventory").call("get_total_dps")) <= 0.0:
return "Wymagana zamontowana broń"
0:
if float(ship.get("energy")) <= 0.0:
return "Brak energii"

View File

@ -32,6 +32,9 @@ func get_skill_rank(skill: String) -> int:
func get_skill_bonus(skill: String) -> float:
return SKILLS.value(skill, get_skill_rank(skill))
func get_energy_bank_fraction() -> float:
return maxf(0.10, get_skill_bonus("electrician"))
func normalize_skill_ranks() -> void:
for skill in skills.keys():
if not SKILLS.DATA.has(skill):

View File

@ -3,12 +3,13 @@ extends RefCounted
const MAX_RANK := 3
const COOLDOWN := 30.0
const DURATION := 5.0
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer"]
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer", "electrician"]
const DATA := {
"electrician": {"name": "Elektryk", "active": false, "values": [0.15, 0.20, 0.25], "color": "70f5df"},
"lucky": {"name": "Farciarz", "active": false, "values": [0.05, 0.10, 0.25], "color": "e8c76f"},
"explorer": {"name": "Eksplorator", "active": false, "values": [1, 3, 6], "color": "76d9cc"},
"business_shark": {"name": "Rekin biznesu", "active": false, "values": [0.10, 0.30, 0.60], "color": "efb568"},
"reactor_overload": {"name": "Przeciążenie reaktora", "active": true, "values": [0.10, 0.20, 0.70], "color": "63d9f5"},
"reactor_overload": {"name": "Przeciążenie reaktora", "active": true, "values": [0.20, 0.50, 1.00], "color": "63d9f5"},
"tactical_maneuver": {"name": "Manewr taktyczny", "active": true, "values": [0.30, 0.60, 1.00], "color": "b9a0ef"},
"efficient_magazine": {"name": "Oszczędny magazynek", "active": false, "values": [0.10, 0.20, 0.50], "color": "a9d69a"},
"miner": {"name": "Górnik", "active": false, "values": [3, 6, 10], "color": "e5ac76"},
@ -27,6 +28,8 @@ static func icon(skill: String) -> Texture2D:
static func description(skill: String, rank: int) -> String:
var amount := roundi(value(skill, rank) * 100.0)
match skill:
"electrician":
return "Powerbank przywraca %d%% maksymalnej energii zamiast 10%%. Umiejętność pasywna; premie rang nie sumują się." % amount
"lucky":
return "Szczęście: %d%% szans na przedmiot o 1 poziom wyższy niż bazowy loot planszy. Dotyczy lootu z wrogów i znalezisk. Bazowo: 0%%. Sumuje się ze sprzętem do limitu 60%%. Premie rang nie sumują się." % amount
"explorer":

View File

@ -1,116 +1,88 @@
extends Node2D
const SETTLE_TIME := 8.0
var _texture: Texture2D
var _fragments: Array[Dictionary] = []
var _dust: Array[Dictionary] = []
const PICKUP := preload("res://scripts/items/mineral_pickup.gd")
var _elapsed := 0.0
var _rng := RandomNumberGenerator.new()
var _dust_color := Color("b4a99a")
var _dust: Array[Dictionary] = []
func configure(sprite: Sprite2D) -> void:
_texture = sprite.texture
func configure(sprite: Sprite2D, count: int = 6, mineral: String = "iron") -> void:
transform = sprite.global_transform
var sprite_scale := global_scale
var source_scale := 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 dimensions := sprite.texture.get_size()
var offset := randf()
var cracks: Array[PackedVector2Array] = []
for index in count:
var endpoint := _edge_point(offset + float(index) * 4.0 / count) * dimensions
cracks.append(_fracture_edge(endpoint, count))
for index in count:
# Neighbours share the same jagged fracture, so the pieces fit together.
var start := offset + float(index) * 4.0 / count
var finish := offset + float(index + 1) * 4.0 / count
var vertices := cracks[index].duplicate()
for corner in range(int(floor(start)) + 1, int(ceil(finish))):
vertices.append(_edge_point(float(corner)) * dimensions)
var next_crack := cracks[(index + 1) % count]
for vertex_index in range(next_crack.size() - 1, 0, -1):
vertices.append(next_crack[vertex_index])
var center := Vector2.ZERO
for vertex in outline:
for vertex in vertices:
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"))
center /= vertices.size()
var fragment_points := PackedVector2Array()
var fragment_uv := PackedVector2Array()
for vertex in vertices:
fragment_points.append((vertex - center) * source_scale)
fragment_uv.append(vertex / dimensions + Vector2(0.5, 0.5))
var fragment := Node2D.new()
fragment.set_script(PICKUP)
fragment.set("mineral", mineral)
fragment.set("texture", sprite.texture)
fragment.set("points", fragment_points)
fragment.set("uv", fragment_uv)
fragment.position = center * source_scale
fragment.set("drift", center.normalized() * randf_range(65.0, 115.0))
fragment.set("spin", randf_range(-2.5, 2.5))
add_child(fragment)
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
_dust.append({"velocity": Vector2.from_angle(randf() * TAU) * randf_range(45.0, 180.0), "size": randf_range(1.0, 3.0)})
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))
func _fracture_edge(endpoint: Vector2, count: int) -> PackedVector2Array:
var edge := PackedVector2Array([Vector2.ZERO])
var normal := endpoint.orthogonal().normalized()
var roughness := endpoint.length() * minf(0.14, 0.65 / count)
var side := -1.0 if randf() < 0.5 else 1.0
for index in 5:
var along := float(index + 1) / 6.0
var offset := roughness * sin(along * PI) * side * randf_range(0.6, 1.0)
edge.append(endpoint * along + normal * offset)
side *= -1.0
edge.append(end)
edge.append(endpoint)
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 _edge_point(distance: float) -> Vector2:
var t := fposmod(distance, 4.0)
if t < 1.0:
return Vector2(-0.5 + t, -0.5)
if t < 2.0:
return Vector2(0.5, -0.5 + t - 1.0)
if t < 3.0:
return Vector2(0.5 - (t - 2.0), 0.5)
return Vector2(-0.5, 0.5 - (t - 3.0))
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)
if _elapsed <= 1.0:
queue_redraw()
# Leave the settled debris visible without updating it every frame.
if _elapsed >= SETTLE_TIME:
elif not _dust.is_empty():
_dust.clear()
set_process(false)
queue_redraw()
if _elapsed > 1.0 and get_child_count() == 0:
queue_free()
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)
var fade := maxf(0.0, 1.0 - _elapsed)
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))
draw_circle(mote.velocity * (1.0 - exp(-2.0 * _elapsed)) * 0.5, mote.size, Color(0.7, 0.75, 0.8, 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))
draw_circle(Vector2.ZERO, 16.0 * (1.0 - _elapsed / 0.18), Color(0.85, 0.9, 1.0, fade * 0.5))

View File

@ -0,0 +1,116 @@
extends Node2D
const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn")
const FIRE_INTERVAL := 0.25
const HULL_TEXTURE := preload("res://assets/Ships/combat_drone.png")
var mother: CharacterBody2D
var formation_index := 0
var damage_per_shot := 1.0
var remaining := 10.0
var _fire_timer := 0.0
var _elapsed := 0.0
var _flash := 0.0
var velocity := Vector2.ZERO
var _turn_velocity := 0.0
const FORMATION := [Vector2(0, -110), Vector2(-35, -78), Vector2(35, -78)]
func _ready() -> void:
add_to_group("player_combat_drones")
z_index = 4
scale = Vector2.ONE * 0.5
rotation = mother.rotation
velocity = mother.velocity
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
_fire_timer = formation_index * FIRE_INTERVAL / 3.0
queue_redraw()
func _physics_process(delta: float) -> void:
remaining -= delta
if remaining <= 0.0 or not is_instance_valid(mother) or bool(mother.get("_is_destroyed")) or bool(mother.get("is_docked")):
queue_free()
return
_elapsed += delta
_flash = maxf(0.0, _flash - delta)
_fire_timer = maxf(0.0, _fire_timer - delta)
var target := _find_target()
var destination := formation_position()
var escort_velocity := mother.velocity
if target != null:
var anchor := mother.global_position.lerp(target.global_position, 0.45)
destination = anchor + Vector2.from_angle(TAU * formation_index / 3.0 + _elapsed * 0.7) * 85.0
escort_velocity *= 0.55
else:
# Rotating formation slots have tangential velocity as the mother turns.
var offset := destination - mother.global_position
escort_velocity += Vector2(-offset.y, offset.x) * float(mother.get("angular_velocity"))
var error := destination - global_position
var correction := (error * 3.0).limit_length(260.0)
var desired_velocity := (escort_velocity + correction).limit_length(maxf(360.0, mother.velocity.length() + 200.0))
velocity = velocity.move_toward(desired_velocity, maxf(650.0, mother.velocity.length()) * delta)
global_position += velocity * delta
var desired_rotation := mother.rotation
if target != null:
var aim := target.global_position - global_position
desired_rotation = aim.angle() + PI * 0.5
if _fire_timer <= 0.0 and aim.length() <= 650.0 and _clear_shot(target):
_fire(target)
_fire_timer = FIRE_INTERVAL
elif error.length() > 55.0 and velocity.length() > 30.0:
desired_rotation = velocity.angle() + PI * 0.5
var angle_error := angle_difference(rotation, desired_rotation)
_turn_velocity = move_toward(_turn_velocity, clampf(angle_error * 6.0, -4.5, 4.5), 14.0 * delta)
var turn := _turn_velocity * delta
if absf(turn) >= absf(angle_error) and signf(turn) == signf(angle_error):
rotation = desired_rotation
_turn_velocity = 0.0
else:
rotation += turn
queue_redraw()
func formation_position() -> Vector2:
return mother.global_position + FORMATION[clampi(formation_index, 0, 2)].rotated(mother.rotation)
func _find_target() -> Node2D:
var closest: Node2D
var distance := 800.0
for enemy in get_tree().get_nodes_in_group("enemies"):
if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0:
continue
var candidate := global_position.distance_to(enemy.global_position)
if candidate < distance and mother.global_position.distance_to(enemy.global_position) < 1000.0:
distance = candidate
closest = enemy
return closest
func _clear_shot(target: Node2D) -> bool:
var query := PhysicsRayQueryParameters2D.create(global_position, target.global_position, 13)
var hit := get_world_2d().direct_space_state.intersect_ray(query)
return hit.is_empty() or hit.get("collider") == target
func _fire(target: Node2D) -> void:
var direction := (target.global_position - global_position).normalized()
var bullet := PROJECTILE.instantiate()
bullet.call("configure", global_position + direction * 9.0, direction, ItemDefinition.WeaponKind.MACHINE_GUN, 1100.0, damage_per_shot, 650.0)
get_parent().add_child(bullet)
_flash = 0.06
mother.get_node("ExtensionSystems").call("break_cloak_on_fire")
func _draw() -> void:
modulate.a = minf(clampf(_elapsed / 0.2, 0.0, 1.0), clampf(remaining / 0.6, 0.0, 1.0))
var flicker := sin(_elapsed * 33.0 + formation_index * 2.1)
for x in [-5.5, 5.5]:
var exhaust := Vector2(x, 19.5)
draw_circle(exhaust + Vector2(0, 5), 11.0 + flicker, Color(0.08, 0.65, 1.0, 0.08))
draw_circle(exhaust + Vector2(0, 3), 6.0, Color(0.12, 0.8, 1.0, 0.18))
draw_colored_polygon(PackedVector2Array([exhaust + Vector2(-3, 0), exhaust + Vector2(0, 15 + flicker * 2), exhaust + Vector2(3, 0)]), Color(0.15, 0.72, 1.0, 0.65))
draw_colored_polygon(PackedVector2Array([exhaust + Vector2(-1.4, 0), exhaust + Vector2(0, 8 + flicker), exhaust + Vector2(1.4, 0)]), Color("c8fbff"))
var hull_size := HULL_TEXTURE.get_size() * (60.0 / HULL_TEXTURE.get_height())
var hull_rect := Rect2(-hull_size * 0.5, hull_size)
draw_texture_rect(HULL_TEXTURE, Rect2(hull_rect.position + Vector2(3, 5), hull_size), false, Color(0, 0, 0, 0.4))
draw_texture_rect(HULL_TEXTURE, hull_rect, false)
if _flash > 0.0:
for x in [-5.5, 5.5]:
var muzzle := Vector2(x, -20)
draw_circle(muzzle, 8.0, Color(1.0, 0.6, 0.18, 0.15))
draw_colored_polygon(PackedVector2Array([muzzle + Vector2(-3, 0), muzzle + Vector2(-1.5, -4), muzzle + Vector2(0, -12), muzzle + Vector2(1.5, -4), muzzle + Vector2(3, 0)]), Color("ffbd68"))
draw_line(muzzle, muzzle + Vector2(0, -7), Color("fff2cb"), 2.0, true)

View File

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

View File

@ -4,7 +4,8 @@ const VFX := preload("res://scripts/game/extension_vfx.gd")
const MINE_TEXTURE := preload("res://assets/icons/extensions/mine_art.png")
const LIFETIME := 60.0
const ARM_TIME := 0.7
const TRIGGER_RADIUS := 34.0
const TRIGGER_RADIUS := 60.0
const BLAST_RADIUS := 140.0
var drift := Vector2.ZERO
var _age := 0.0
var _triggered := false
@ -27,16 +28,29 @@ func _physics_process(delta: float) -> void:
if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0:
continue
if global_position.distance_to(enemy.global_position) <= TRIGGER_RADIUS + float(enemy.call("get_visual_size")) * 0.35:
_detonate()
return
func _detonate() -> void:
if _triggered:
return
_triggered = true
# Resolve the entire blast once; each nearby enemy receives one hit.
for enemy in get_tree().get_nodes_in_group("enemies"):
if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0:
continue
var hull_radius := float(enemy.call("get_visual_size")) * 0.35
if global_position.distance_to(enemy.global_position) <= BLAST_RADIUS + hull_radius:
enemy.call("apply_mine_hit")
var explosion := Node2D.new()
explosion.set_script(preload("res://scripts/game/deployment_flash.gd"))
explosion.set("color", Color("ffbc75"))
explosion.set("blast", true)
# The shockwave reaches the same radius as the area damage.
explosion.scale = Vector2.ONE * (BLAST_RADIUS / 88.0)
explosion.position = position
get_parent().add_child(explosion)
queue_free()
return
func _draw() -> void:
var armed := _age >= ARM_TIME

View File

@ -0,0 +1,55 @@
extends Node2D
const ARM_TIME := 0.7
const LIFETIME := 60.0
const TRIGGER_RADIUS := 54.0
var player: CharacterBody2D
var drift := Vector2.ZERO
var _age := 0.0
var _triggered := false
func _ready() -> void:
add_to_group("enemy_mines")
z_index = 12
func _physics_process(delta: float) -> void:
_age += delta
position += drift * delta
drift = drift.move_toward(Vector2.ZERO, 100.0 * delta)
queue_redraw()
if _age >= LIFETIME:
queue_free()
return
if _triggered or _age < ARM_TIME or not is_instance_valid(player) or bool(player.get("_is_destroyed")):
return
if global_position.distance_to(player.global_position) <= TRIGGER_RADIUS:
_triggered = true
player.call("take_damage", float(player.get("max_hull")) * 0.05, false, DamageTypes.Type.KINETIC)
var explosion := Node2D.new()
explosion.set_script(preload("res://scripts/game/deployment_flash.gd"))
explosion.set("color", Color("ff4265"))
explosion.set("blast", true)
explosion.position = position
get_parent().add_child(explosion)
queue_free()
func _draw() -> void:
var armed := _age >= ARM_TIME
var pulse := 0.65 + 0.35 * sin(_age * 7.0)
var accent := Color("ff4265") if armed else Color("ffb45c")
var fade := clampf(LIFETIME - _age, 0.0, 1.0)
draw_circle(Vector2.ZERO, 30.0, Color(accent, 0.08 * pulse * fade))
var shell := PackedVector2Array()
for index in 16:
var radius := 24.0 if index % 2 == 0 else 13.0
shell.append(Vector2.from_angle(TAU * index / 16.0) * radius)
draw_colored_polygon(shell, Color("292236") * Color(1, 1, 1, fade))
var outline := shell.duplicate()
outline.append(shell[0])
draw_polyline(outline, Color(accent, 0.9 * fade), 1.5, true)
draw_circle(Vector2.ZERO, 10.0, Color("13101e") * Color(1, 1, 1, fade))
draw_arc(Vector2.ZERO, 11.0, 0.0, TAU, 32, Color(accent, fade), 2.0, true)
for index in 3:
var direction := Vector2.from_angle(-PI * 0.5 + TAU * index / 3.0)
draw_line(direction * 4.0, direction * 8.0, Color(accent, pulse * fade), 2.5, true)
draw_circle(Vector2.ZERO, 3.0, Color(accent, pulse * fade))

View File

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

View File

@ -21,6 +21,10 @@ const LASER_MISS_CHANCE := 0.35
@export var weapon_range := 680.0
@export var weapon_cooldown := 1.5
@export var weapon_damage := 8.0
enum Tactic { RAM, KEEP_DISTANCE }
@export_enum("Losowa", "Taran", "Dystans") var tactic_choice := 0
var tactic := Tactic.RAM
var _orbit_direction := 1.0
var max_shields := 20.0
var shields := 20.0
@ -31,12 +35,15 @@ var _shield_visual: Node2D
var hull := 50.0
var _player: CharacterBody2D
var _activated := false
var _retreat_mine_rolled := false
var _sprite: Sprite2D
var _weapon_timer := 0.0
var _slow_remaining := 0.0
var _combat_target: Node2D
func _ready() -> void:
tactic = randi_range(0, 1) if tactic_choice == 0 else tactic_choice - 1
_orbit_direction = -1.0 if randf() < 0.5 else 1.0
hull = max_hull
_sprite = $Sprite2D
_shield_visual = Node2D.new()
@ -108,10 +115,16 @@ func _physics_process(delta: float) -> void:
_activated = true
if _activated and to_player.length_squared() > 1.0:
var direction := to_player.normalized()
var desired_rotation := direction.angle() + PI * 0.5
var fleeing := is_retreating()
if fleeing and not _retreat_mine_rolled:
_retreat_mine_rolled = true
if randf() < 0.15:
_drop_retreat_mine(to_player.normalized())
var movement := combat_movement(to_player)
var desired_rotation := (movement if fleeing else direction).angle() + PI * 0.5
rotation = rotate_toward(rotation, desired_rotation, turn_speed * delta)
velocity = velocity.move_toward(direction * approach_speed * get_slow_multiplier(), approach_acceleration * get_slow_multiplier() * delta)
if to_player.length() <= weapon_range and _weapon_timer <= 0.0:
velocity = velocity.move_toward(movement * approach_speed * (0.5 if fleeing else 1.0) * get_slow_multiplier(), approach_acceleration * get_slow_multiplier() * delta)
if not fleeing and to_player.length() <= weapon_range and _weapon_timer <= 0.0:
_fire_plasma(direction)
_weapon_timer = get_fire_interval()
else:
@ -120,6 +133,37 @@ func _physics_process(delta: float) -> void:
move_and_slide()
_check_player_collision()
func is_retreating() -> bool:
return hull > 0.0 and hull < max_hull * 0.1
func _drop_retreat_mine(toward_target: Vector2) -> void:
var mine := Node2D.new()
mine.set_script(preload("res://scripts/game/enemy_mine.gd"))
mine.set("player", _player)
mine.set("drift", toward_target * 36.0)
mine.position = position + toward_target * (get_visual_size() * 0.5 + 16.0)
get_parent().add_child.call_deferred(mine)
func combat_movement(to_target: Vector2) -> Vector2:
var distance := to_target.length()
var direction := to_target.normalized()
if is_retreating():
var escape := -direction
var half := preload("res://scripts/levels/system_generator.gd").WORLD_SIZE * 0.5 - Vector2(120, 120)
if absf(global_position.x) > half.x:
escape.x = -signf(global_position.x)
if absf(global_position.y) > half.y:
escape.y = -signf(global_position.y)
return escape.normalized()
if tactic == Tactic.RAM:
return direction
var preferred := minf(weapon_range * 0.65, 420.0)
if distance < preferred * 0.8:
return -direction
if distance > preferred * 1.15:
return direction
return direction.orthogonal() * _orbit_direction * 0.55
func get_slow_multiplier() -> float:
return 0.5 if _slow_remaining > 0.0 else 1.0
@ -144,7 +188,7 @@ func apply_mine_hit() -> void:
return
_slow_remaining = 5.0
velocity = velocity.limit_length(approach_speed * 0.5)
take_damage(max_hull * 0.05, false, DamageTypes.Type.KINETIC, true)
take_damage(hull * 0.5, false, DamageTypes.Type.KINETIC, true)
func _check_player_collision() -> void:
for collision_index in get_slide_collision_count():

View File

@ -5,6 +5,8 @@ const PULSE := preload("res://scripts/game/extension_pulse.gd")
var cooldowns: Dictionary = {}
var cloak_remaining := 0.0
var solar_remaining := 0.0
var drones_remaining := 0.0
var _drones: Array[Node2D] = []
var _cloak_material: ShaderMaterial
var _old_material: Material
var _shadow_was_visible := false
@ -15,11 +17,16 @@ var _has_destination := false
func reset_combat_state() -> void:
cooldowns.clear()
_clear_drones()
solar_remaining = 0.0
if cloak_remaining > 0.0:
_end_cloak()
func _physics_process(delta: float) -> void:
if drones_remaining > 0.0:
drones_remaining = maxf(0.0, drones_remaining - delta)
if drones_remaining <= 0.0 or bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")):
_clear_drones()
for id in cooldowns.keys():
cooldowns[id] = maxf(0.0, float(cooldowns[id]) - delta)
if cloak_remaining > 0.0:
@ -66,6 +73,25 @@ func has_teleport_destination() -> bool:
func activate(kind: int) -> void:
match kind:
5:
var inventory := ship.get_node("ShipInventory") as ShipInventory
if inventory.ammunition < 50 or drones_remaining > 0.0:
return
inventory.ammunition -= 50
inventory.ammunition_changed.emit()
drones_remaining = 10.0
break_cloak_on_fire()
var shared_dps := inventory.get_total_dps() * 0.5
for index in 3:
var drone := Node2D.new()
drone.set_script(preload("res://scripts/game/combat_drone.gd"))
drone.set("mother", ship)
drone.set("formation_index", index)
drone.set("damage_per_shot", shared_dps / 3.0 * 0.25)
drone.position = ship.position + Vector2.from_angle(TAU * index / 3.0) * 48.0
ship.get_parent().add_child(drone)
_drones.append(drone)
_deployment_effect(ship.position, Vector2.DOWN.rotated(ship.rotation), Color("70f5df"))
0:
if not has_teleport_destination():
return
@ -103,6 +129,16 @@ func activate(kind: int) -> void:
solar_remaining = 5.0
_pulse(ship.global_position, Color("70dfff"), 50.0)
func _clear_drones() -> void:
drones_remaining = 0.0
for drone in _drones:
if is_instance_valid(drone):
drone.queue_free()
_drones.clear()
func _exit_tree() -> void:
_clear_drones()
func _teleport_effect(point: Vector2, arriving: bool) -> void:
var effect := Node2D.new()
effect.set_script(preload("res://scripts/game/teleport_vortex.gd"))
@ -138,6 +174,10 @@ func _start_cloak() -> void:
visual.modulate = Color(visual.modulate, 0.08)
_pulse(ship.global_position, Color("81e4ef"), 40.0)
func break_cloak_on_fire() -> void:
if cloak_remaining > 0.0:
_end_cloak()
func _end_cloak() -> void:
cloak_remaining = 0.0
ship.get_node("Sprite2D").set("material", _old_material)

View File

@ -148,6 +148,11 @@ func _ready() -> void:
_window_manager.call("register_window", _inventory_clip, inventory_panel, inventory_panel.get_node("Contents/Header"))
$HUDRoot.resized.connect(func() -> void: _layout_sidebar.call_deferred())
_create_ability_bar()
var objectives := PanelContainer.new()
objectives.name = "MissionObjectives"
objectives.set_script(preload("res://scripts/ui/mission_objectives.gd"))
objectives.set("game", get_parent())
$HUDRoot.add_child(objectives)
debug_toggle.pressed.connect(_toggle_debug)
inventory_button.pressed.connect(_toggle_inventory)
_update_debug_toggle()
@ -178,6 +183,14 @@ func _create_sidebar() -> void:
preload("res://scripts/ui/hud_button_style.gd").apply(_debug_item_button, Color("a9ef83"))
_debug_item_button.pressed.connect(_give_debug_item)
_debug_actions.add_child(_debug_item_button)
var ammunition_button := Button.new()
ammunition_button.name = "AddAmmunition"
ammunition_button.text = "Dodaj amunicję"
ammunition_button.tooltip_text = "+10 zwykłej amunicji, +10 radiacyjnej amunicji i +10 rakiet"
ammunition_button.custom_minimum_size.y = 36
preload("res://scripts/ui/hud_button_style.gd").apply(ammunition_button, Color("efb568"))
ammunition_button.pressed.connect(_give_debug_ammunition)
_debug_actions.add_child(ammunition_button)
_debug_item_status = Label.new()
_debug_item_status.add_theme_font_size_override("font_size", 10)
_debug_item_status.add_theme_color_override("font_color", Color("a9ef83"))
@ -348,7 +361,7 @@ func _update_energy_bank_button() -> void:
var inventory := player.get_node("ShipInventory") as ShipInventory
var count := inventory.get_energy_bank_count()
(_energy_bank_button.get_node("Amount") as Label).text = str(count)
_energy_bank_button.tooltip_text = "Bank energii: %d\nR • Przywraca 10%% maksymalnej energii" % count
_energy_bank_button.tooltip_text = "Bank energii: %d\nR • Przywraca %d%% maksymalnej energii" % [count, roundi(GameState.captain.get_energy_bank_fraction() * 100.0)]
_energy_bank_button.disabled = count == 0 or float(player.get("energy")) >= float(player.get("max_energy")) or bool(player.get("_is_destroyed")) or bool(player.get("is_docked"))
_energy_bank_button.call("set_visual_state", 0.0, false, _energy_bank_button.disabled)
@ -389,6 +402,14 @@ func _toggle_debug() -> void:
_update_debug_toggle()
_layout_debug()
func _give_debug_ammunition() -> void:
var inventory := player.get_node("ShipInventory") as ShipInventory
inventory.ammunition += 10
inventory.irradiated_ammunition += 10
inventory.missile_ammunition += 10
inventory.ammunition_changed.emit()
inventory.inventory_changed.emit()
func _give_debug_item() -> bool:
var inventory := player.get_node("ShipInventory") as ShipInventory
_debug_item_status.show()

View File

@ -400,7 +400,7 @@ func start_boost(speed_bonus: float, acceleration_time: float) -> void:
func get_active_ability(slot_index: int) -> ActiveAbility:
var item: ItemInstance = $ShipInventory.get_item("extension", slot_index)
return item.definition.active_ability if item != null and item.definition != null else null
return item.get_active_ability() if item != null and item.definition != null else null
func get_luck_chance() -> float:
return GameState.captain.get_luck_chance(luck_percent)
@ -436,7 +436,7 @@ func activate_ability(slot_index: int) -> bool:
func use_energy_bank() -> void:
if $ShipInventory.use_energy_bank():
var popup := DAMAGE_POPUP_SCENE.instantiate() as Label
popup.text = "+10% ENERGII"
popup.text = "+%d%% ENERGII" % roundi(GameState.captain.get_energy_bank_fraction() * 100.0)
popup.set("text_color", Color("70f5df"))
popup.position = position + Vector2(-48, -60)
get_parent().add_child(popup)

View File

@ -135,6 +135,7 @@ func try_fire_missile(slot_index: int = 2) -> bool:
return true
func _fire_projectile(item: ItemInstance) -> void:
_break_cloak_on_fire()
var definition := item.definition
var hit: Dictionary = ship.call("roll_weapon_hit", item)
var muzzle_position := _muzzle_position()
@ -194,6 +195,7 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void:
line.visible = false
return
WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, ItemDefinition.WeaponKind.LASER)
_break_cloak_on_fire()
if not result.is_empty():
var hit: Dictionary = ship.call("roll_weapon_hit", item)
_apply_damage(result.get("collider"), float(hit["damage"]), bool(hit["critical"]))
@ -201,6 +203,11 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void:
_laser_pulse_timers[slot_index] = definition.laser_pulse_duration
_shot_timers[slot_index] = definition.fire_interval
func _break_cloak_on_fire() -> void:
var systems := ship.get_node_or_null("ExtensionSystems")
if systems != null:
systems.call("break_cloak_on_fire")
func _aim_direction_from(origin: Vector2) -> Vector2:
var offset := ship.get_global_mouse_position() - origin
return offset.normalized() if offset.length_squared() > 0.001 else Vector2.UP.rotated(ship.rotation)

View File

@ -1,33 +1,35 @@
extends Node2D
const VORTEX := preload("res://scripts/game/wormhole_vortex.gdshader")
var _time := 0.0
var _vortex_material: ShaderMaterial
func _ready() -> void:
z_index = -2
light_mask = 4
var vortex := ColorRect.new()
vortex.name = "Vortex"
vortex.position = Vector2(-144, -144)
vortex.size = Vector2(288, 288)
vortex.mouse_filter = Control.MOUSE_FILTER_IGNORE
_vortex_material = ShaderMaterial.new()
_vortex_material.shader = VORTEX
vortex.material = _vortex_material
add_child(vortex)
var label := Label.new()
label.name = "EntryLabel"
label.text = "WORMHOLE\nWleć, aby przejść do następnego układu"
label.position = Vector2(-180, 72)
label.size = Vector2(360, 44)
label.position = Vector2(-200, 106)
label.size = Vector2(400, 48)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", 13)
label.add_theme_color_override("font_color", Color("89e8ff"))
label.add_theme_color_override("font_color", Color("a0e7ff"))
label.add_theme_color_override("font_outline_color", Color("030916"))
label.add_theme_constant_override("outline_size", 5)
add_child(label)
func _process(delta: float) -> void:
_time += delta
queue_redraw()
func _draw() -> void:
var pulse := 1.0 + sin(_time * 2.5) * 0.06
for ring in range(6, 0, -1):
draw_circle(Vector2.ZERO, (48.0 + ring * 4.0) * pulse, Color(0.28, 0.35, 0.95, 0.025 * (7 - ring)))
draw_circle(Vector2.ZERO, 43.0, Color("040917"))
for arm in 3:
var points := PackedVector2Array()
for segment in 65:
var progress := segment / 64.0
var angle := progress * TAU * 1.3 + _time * 1.6 + TAU * arm / 3.0
points.append(Vector2.RIGHT.rotated(angle) * (8.0 + progress * 44.0) * pulse)
draw_polyline(points, Color(0.35 + arm * 0.12, 0.55, 1.0, 0.8), 3.0, true)
draw_arc(Vector2.ZERO, 49.0 * pulse, 0, TAU, 96, Color("8beaff"), 2.0, true)
if _vortex_material != null:
_vortex_material.set_shader_parameter("elapsed", _time)

View File

@ -0,0 +1,62 @@
shader_type canvas_item;
render_mode unshaded;
uniform float elapsed = 0.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);
f = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x),
mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);
}
float turbulence(vec2 p) {
return noise(p) * 0.55 + noise(p * 2.07) * 0.28 + noise(p * 4.13) * 0.17;
}
void fragment() {
vec2 p = (UV - 0.5) * 2.0;
float r = length(p);
float a = atan(p.y, p.x);
float t = elapsed;
float reveal = smoothstep(0.0, 1.2, t);
float pulse = 1.0 + 0.012 * sin(t * 2.1);
float radius = r / pulse;
// Use circular coordinates for seamless turbulent filaments.
float twist = a + 2.6 * log(max(radius, 0.035)) - t * 0.65;
vec2 flow = vec2(cos(twist), sin(twist));
float clouds = turbulence(flow * 5.0 + vec2(radius * 13.0, -t * 0.3));
float filaments = pow(max(0.0, sin(twist * 7.0 + clouds * 5.0)), 5.0);
float disc = exp(-pow((radius - 0.46) / 0.125, 2.0));
float rim_r = 0.432 + 0.008 * sin(a * 6.0 - t * 1.4) + (clouds - 0.5) * 0.017;
float rim = exp(-abs(radius - rim_r) * 170.0);
float bloom = exp(-abs(radius - rim_r) * 25.0);
float halo = exp(-pow(radius / 0.66, 4.0));
vec3 cyan = vec3(0.16, 0.73, 1.0);
vec3 violet = vec3(0.40, 0.15, 0.93);
vec3 tint = mix(violet, cyan, 0.5 + 0.5 * sin(twist * 2.0 + t * 0.3));
vec3 color = tint * disc * (0.18 + clouds * 0.55 + filaments * 0.85);
color += cyan * bloom * 0.3 + vec3(0.63, 0.92, 1.0) * rim * 1.5;
color += violet * halo * 0.055;
// A dark throat with contracting spiral bands suggests a deep tunnel.
float inside = 1.0 - smoothstep(0.37, 0.425, radius);
float depth = -log(max(radius / 0.425, 0.015));
float tunnel = pow(max(0.0, sin(depth * 16.0 - t * 3.3 + a * 2.0)), 12.0);
float throat_fade = smoothstep(0.035, 0.3, radius);
vec3 throat = vec3(0.003, 0.006, 0.026);
throat += mix(violet, cyan, radius / 0.425) * tunnel * throat_fade * 0.22;
throat += cyan * pow(max(0.0, sin(twist * 5.0 + clouds * 2.0)), 14.0) * throat_fade * 0.11;
color = mix(color, throat, inside);
// Orbiting pinpoints and their short luminous trails, outside the event horizon.
float orbit_angle = a - t * 0.24;
vec2 orbit = vec2(cos(orbit_angle), sin(orbit_angle)) * radius * 43.0;
float sparks = pow(turbulence(orbit), 18.0) * 32.0;
sparks *= smoothstep(0.45, 0.52, radius) * (1.0 - smoothstep(0.62, 0.86, radius));
color += vec3(0.56, 0.83, 1.0) * sparks;
float alpha = max(inside, max(disc * (0.5 + clouds * 0.5), bloom * 0.8));
alpha = max(alpha, halo * 0.1 + sparks);
alpha *= (1.0 - smoothstep(0.84, 0.98, radius)) * reveal;
COLOR = vec4(color, clamp(alpha, 0.0, 1.0));
}

View File

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

View File

@ -4,12 +4,14 @@ const MINERALS := ["iron", "titanium", "uranium", "aluminium", "sulfur"]
const NAMES := {"iron": "Żelazo", "titanium": "Tytan", "uranium": "Uran", "aluminium": "Aluminium", "sulfur": "Siarka"}
const COLORS := {"iron": Color("c5b5a0"), "titanium": Color("9ecbe9"), "uranium": Color("a9ef83"), "aluminium": Color("dce8ef"), "sulfur": Color("f4d466")}
const IDS := ["parts", "ammunition", "missile", "energy_bank", "irradiated_ammunition"]
# Base asteroid yield increased from mean 2 (1-3) to mean 6 (5-7).
# Triple mineral costs to preserve base crafts per asteroid; miner remains a bonus.
const DATA := {
"parts": {"name": "Części", "amount": 1, "cost": {"titanium": 1, "iron": 1, "aluminium": 1}},
"ammunition": {"name": "Amunicja", "amount": 50, "cost": {"titanium": 1, "sulfur": 1}},
"missile": {"name": "Rakieta", "amount": 1, "cost": {"iron": 1, "sulfur": 1, "uranium": 1}},
"energy_bank": {"name": "Powerbank", "amount": 1, "cost": {"iron": 1, "aluminium": 1}},
"irradiated_ammunition": {"name": "Amunicja radiacyjna", "amount": 50, "cost": {"titanium": 1, "uranium": 1}},
"parts": {"name": "Części", "amount": 1, "cost": {"titanium": 3, "iron": 3, "aluminium": 3}},
"ammunition": {"name": "Amunicja", "amount": 50, "cost": {"titanium": 3, "sulfur": 3}},
"missile": {"name": "Rakieta", "amount": 1, "cost": {"iron": 3, "sulfur": 3, "uranium": 3}},
"energy_bank": {"name": "Powerbank", "amount": 1, "cost": {"iron": 3, "aluminium": 3}},
"irradiated_ammunition": {"name": "Amunicja radiacyjna", "amount": 50, "cost": {"titanium": 3, "uranium": 3}},
}
static func icon(id: String) -> Texture2D:

View File

@ -0,0 +1,27 @@
extends RefCounted
const MIN_SECONDS := 5
const BASE_SECONDS := 30
# Cumulative P(cooldown <= seconds); interpolate exponentially between anchors.
const SECONDS := [5, 10, 15, 20, 25, 30]
const CHANCES := [0.0002, 0.0015, 0.01, 0.05, 0.25, 1.0]
static func cumulative_chance(seconds: int) -> float:
if seconds < MIN_SECONDS:
return 0.0
if seconds >= BASE_SECONDS:
return 1.0
for index in SECONDS.size() - 1:
if seconds <= SECONDS[index + 1]:
var progress := float(seconds - SECONDS[index]) / float(SECONDS[index + 1] - SECONDS[index])
return exp(lerpf(log(CHANCES[index]), log(CHANCES[index + 1]), progress))
return 1.0
static func from_roll(roll: float) -> int:
for seconds in range(MIN_SECONDS, BASE_SECONDS):
if roll < cumulative_chance(seconds):
return seconds
return BASE_SECONDS
static func roll_seconds() -> int:
return from_roll(randf())

View File

@ -0,0 +1 @@
uid://4nswtu72lqfp

View File

@ -1,6 +1,7 @@
extends RefCounted
const DEFINITIONS := [
preload("res://resources/items/combat_drones.tres"),
preload("res://resources/items/teleport.tres"),
preload("res://resources/items/cloak.tres"),
preload("res://resources/items/mine_layer.tres"),
@ -48,6 +49,7 @@ static func generate_for_definition(definition: ItemDefinition, level: int, enem
item.primary_value_override = (item.damage_min + item.damage_max) * 0.5
item.energy_regeneration_override = item.definition.energy_regeneration_bonus * scale_factor * randf_range(0.8, 1.2)
if definition.category == ItemDefinition.Category.ACTIVE_EXTENSION:
item.extension_cooldown = ItemInstance.EXTENSION_COOLDOWNS.roll_seconds()
return item
var roll := randf()
var count := 1 if roll < 0.5 else (2 if roll < 0.75 else (3 if roll < 0.89 else (4 if roll < 0.97 else 5)))

View File

@ -2,6 +2,30 @@ extends Resource
class_name ItemInstance
const ENERGY_REGENERATION_BONUS_MULTIPLIER := 0.5
const EXTENSION_COOLDOWNS := preload("res://scripts/items/extension_cooldowns.gd")
# Exported with the item; old items default to 30 seconds without rerolling.
@export_range(5, 30, 1) var extension_cooldown: int = 30
var _instance_ability: ActiveAbility
func get_active_ability() -> ActiveAbility:
if definition == null or definition.active_ability == null:
return null
if definition.category != ItemDefinition.Category.ACTIVE_EXTENSION:
return definition.active_ability
if _instance_ability != null and _instance_ability.cooldown == clampi(extension_cooldown, 5, 30):
return _instance_ability
if _instance_ability == null:
# Never mutate the shared ability resource in the item definition.
_instance_ability = definition.active_ability.duplicate() as ActiveAbility
_instance_ability.cooldown = clampi(extension_cooldown, 5, 30)
var description := definition.active_ability.description
var fixed_cooldown := RegEx.new()
fixed_cooldown.compile(" (Odnowienie:|Zrzut co) [0-9]+ s\\.")
description = fixed_cooldown.sub(description, "", true)
if definition.item_id == "mine_layer":
description = "Zostawia minę na 60 s. Uzbraja się po 0,7 s. Wykrywa bliski przelot wroga. Wybuch w promieniu 140 jednostek zadaje każdemu wrogowi 50% aktualnego kadłuba bez redukcji absorpcji (tarcze pochłaniają obrażenia) i spowalnia o 50% na 5 s."
_instance_ability.description = description + "\nOdnowienie: %d s." % _instance_ability.cooldown
return _instance_ability
@export var definition: ItemDefinition
@export_range(1, 1000, 1) var item_level: int = 1
@ -110,7 +134,7 @@ func get_tooltip_text() -> String:
if definition.category == ItemDefinition.Category.WEAPON:
primary_text = "Obrażenia: %s" % get_damage_range_text()
elif definition.active_ability != null:
primary_text = definition.active_ability.description
primary_text = get_active_ability().description
var lines: Array[String] = [definition.display_name, "%s • Poziom %d" % [get_rarity_name(), item_level], "", definition.description, "", "CECHA GŁÓWNA", primary_text]
if definition.energy_regeneration_bonus > 0.0:
lines.append("Regeneracja generatora: +%.1f /s" % ((energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus) * ENERGY_REGENERATION_BONUS_MULTIPLIER))

View File

@ -0,0 +1,48 @@
extends Node2D
const PICKUP_RADIUS := 48.0
const ATTRACTION_RADIUS := 150.0
var mineral := "iron"
var texture: Texture2D
var points := PackedVector2Array()
var uv := PackedVector2Array()
var drift := Vector2.ZERO
var spin := 0.0
var _age := 0.0
var _collected := false
var _player: CharacterBody2D
func _ready() -> void:
add_to_group("loot")
add_to_group("mineral_pickups")
_player = get_tree().current_scene.get_node_or_null("PlayerShip") as CharacterBody2D
z_index = 18
light_mask = 4
func _physics_process(delta: float) -> void:
_age += delta
position += drift * delta
drift *= exp(-1.8 * delta)
rotation += spin * delta
spin *= exp(-0.7 * delta)
queue_redraw()
if _collected or _age < 0.8 or not is_instance_valid(_player) or bool(_player.get("_is_destroyed")):
return
var distance := global_position.distance_to(_player.global_position)
if distance <= PICKUP_RADIUS:
_collected = true
GameState.captain.minerals[mineral] = int(GameState.captain.minerals.get(mineral, 0)) + 1
GameState.captain.emit_changed()
var popup := preload("res://scenes/game/damage_popup.tscn").instantiate() as Label
popup.text = "+1 %s" % {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu", "sulfur": "siarki"}.get(mineral, mineral)
popup.set("text_color", Color("c9e8ff"))
popup.position = _player.position + Vector2(-40, -50)
get_tree().current_scene.add_child.call_deferred(popup)
queue_free()
elif distance <= ATTRACTION_RADIUS:
global_position = global_position.move_toward(_player.global_position, 180.0 * delta)
func _draw() -> void:
if texture == null or points.size() < 3:
return
draw_polygon(points, PackedColorArray([Color.WHITE]), uv, texture)

View File

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

View File

@ -93,7 +93,7 @@ func use_energy_bank() -> bool:
var current := float(ship.get("energy"))
if current >= maximum:
return false
ship.set("energy", minf(maximum, current + maximum * 0.1))
ship.set("energy", minf(maximum, current + maximum * GameState.captain.get_energy_bank_fraction()))
energy_banks -= 1
inventory_changed.emit()
return true

View File

@ -58,7 +58,7 @@ func _update_label() -> void:
_label.text = display_name + "\nGwiazda centralna"
_label.add_theme_color_override("font_color", Color("ffdb98"))
elif is_protected:
_label.text = display_name + " — zamieszkana\n" + ("USŁUGI DOSTĘPNE" if is_safe else "POKONAJ WSZYSTKICH WROGÓW")
_label.text = display_name + " — zamieszkana" + ("\nUSŁUGI DOSTĘPNE" if is_safe else "")
_label.add_theme_color_override("font_color", Color("77e8bc") if is_safe else Color("ffbd83"))
else:
_label.text = display_name

View File

@ -121,9 +121,15 @@ func claim() -> bool:
var summary := ""
var scale := reward_scale()
if kind == "asteroid":
var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale)) + roundi(GameState.captain.get_skill_bonus("miner"))
GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount
summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu", "sulfur": "siarki"}[_mineral]]
var amount := _rng.randi_range(5, 7) + maxi(0, roundi(GameState.captain.get_skill_bonus("miner")))
var debris := Node2D.new()
debris.set_script(ASTEROID_DESTRUCTION)
debris.name = "AsteroidDebris"
debris.call("configure", _asteroid_sprite, amount, _mineral)
get_parent().add_child.call_deferred(debris)
set_physics_process(false)
queue_free()
return true
elif kind == "ruin":
var xp := roundi(_rng.randi_range(10, 20) * scale)
GameState.captain.add_rewards(xp, 0)
@ -156,13 +162,6 @@ 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

@ -9,8 +9,6 @@ var _panel: PanelContainer
var _contents: VBoxContainer
var _bar: HBoxContainer
var _buttons: Dictionary = {}
var _was_paused := false
var _owns_pause := false
var _refresh_pending := false
func _ready() -> void:
@ -78,19 +76,11 @@ func open_management() -> void:
if bool(player.get("_is_destroyed")):
return
if not _screen.visible:
_was_paused = get_tree().paused
_owns_pause = true
get_tree().paused = true
_screen.show()
_build_management()
func _close() -> void:
if not _captain.skill_offers.is_empty():
return
_screen.hide()
if _owns_pause:
get_tree().paused = _was_paused
_owns_pause = false
func _build_management() -> void:
for child in _contents.get_children():
@ -103,13 +93,12 @@ func _build_management() -> void:
var title := _label("AWANS KAPITANA" if choosing else "UMIEJĘTNOŚCI KAPITANA", 25, Color("91dbea"))
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
heading.add_child(title)
if not choosing:
var close := Button.new()
close.name = "Close"
close.text = "WRÓĆ DO LOTU"
close.pressed.connect(_close)
heading.add_child(close)
_contents.add_child(_label("POZIOM %d · GRA WSTRZYMANA" % _captain.level, 13, Color("91aabd")))
_contents.add_child(_label("POZIOM %d" % _captain.level, 13, Color("91aabd")))
if choosing:
_contents.add_child(_label("Wybierz jedną umiejętność. Wybór obowiązuje przez całą rozgrywkę.", 16))
var choices := HBoxContainer.new()
@ -327,6 +316,4 @@ func _layout() -> void:
else:
_bar.position = Vector2(16, viewport_size.y - 190)
func _exit_tree() -> void:
if _owns_pause and is_inside_tree():
get_tree().paused = _was_paused

View File

@ -24,4 +24,10 @@ func _draw() -> void:
var warning_color := Color(1.0, 0.32, 0.26, pulse)
draw_rect(Rect2(Vector2(3, 3), size - Vector2(6, 6)), warning_color, false, 3)
var warning := "NISKI KADŁUB — %d%%" % roundi(hull / max_hull * 100)
draw_string(FONT, Vector2(0, 34), warning, HORIZONTAL_ALIGNMENT_CENTER, size.x, 18, warning_color)
# Keep the warning below the navigation buttons, including after a resize.
var navigation := get_parent().get_node_or_null("TopNavigation") as Control
var warning_top := 104.0
if is_instance_valid(navigation):
warning_top = navigation.get_global_rect().end.y - global_position.y + 12.0
var baseline := warning_top + FONT.get_ascent(18)
draw_string(FONT, Vector2(0, baseline), warning, HORIZONTAL_ALIGNMENT_CENTER, size.x, 18, warning_color)

View File

@ -35,7 +35,7 @@ func _draw() -> void:
if _inventory == null or not is_instance_valid(player) or size.x < 50 or size.y < 50:
return
var outline := PackedVector2Array([Vector2(24, 0), Vector2(size.x - 24, 0), Vector2(size.x, 24), Vector2(size.x, size.y - 12), Vector2(size.x - 12, size.y), Vector2(12, size.y), Vector2(0, size.y - 12), Vector2(0, 24)])
draw_colored_polygon(outline, Color(0.025, 0.041, 0.063, 1.0))
draw_colored_polygon(outline, Color(0.025, 0.041, 0.063, 0.78))
var closed := outline.duplicate()
closed.append(outline[0])
draw_polyline(closed, Color("376779"), 1.5, true)

View File

@ -0,0 +1,50 @@
extends PanelContainer
var game: Node2D
var _objective: Label
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
var style := StyleBoxFlat.new()
style.bg_color = Color(0.025, 0.041, 0.063, 0.72)
style.border_color = Color("376779")
style.set_border_width_all(1)
style.set_corner_radius_all(6)
style.set_content_margin_all(12)
add_theme_stylebox_override("panel", style)
var contents := VBoxContainer.new()
contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
contents.add_theme_constant_override("separation", 6)
add_child(contents)
var title := Label.new()
title.text = "CELE"
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
title.add_theme_font_size_override("font_size", 12)
title.add_theme_color_override("font_color", Color("91dbea"))
contents.add_child(title)
_objective = Label.new()
_objective.name = "CurrentObjective"
_objective.mouse_filter = Control.MOUSE_FILTER_IGNORE
_objective.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_objective.add_theme_font_size_override("font_size", 11)
_objective.add_theme_color_override("font_color", Color("efb568"))
contents.add_child(_objective)
func _process(_delta: float) -> void:
if not is_instance_valid(game):
return
if bool(game.get("system_cleared")):
_objective.text = "Znajdź zamieszkaną planetę"
elif bool(game.get("_boss_spawned")):
_objective.text = "Pokonaj bossa"
else:
_objective.text = "Pokonaj wszystkich wrogów"
var cockpit := get_parent().get_node_or_null("Cockpit") as Control
var available_width := cockpit.position.x - 24.0 if is_instance_valid(cockpit) else 260.0
var panel_width := clampf(available_width, 180.0, 260.0)
size.x = panel_width
size.y = get_combined_minimum_size().y
var bottom: float = (get_parent() as Control).size.y - 12.0
if available_width < 180.0 and is_instance_valid(cockpit):
bottom = cockpit.position.y - 64.0
position = Vector2(12.0, bottom - size.y)

View File

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

View File

@ -0,0 +1,111 @@
extends Node2D
const PLAYER := preload("res://scenes/game/player_ship.tscn")
const ENEMY := preload("res://scenes/game/enemy_ship.tscn")
const DRONES := preload("res://resources/items/combat_drones.tres")
func _ready() -> void:
GameState.start_new_run()
var ship := PLAYER.instantiate() as CharacterBody2D
add_child(ship)
ship.set_physics_process(false)
var weapons := ship.get_node("WeaponController")
weapons.set_physics_process(false)
var systems := ship.get_node("ExtensionSystems")
systems.set_physics_process(false)
var inventory := ship.get_node("ShipInventory") as ShipInventory
# Every rank replaces the baseline, respects the energy cap, and uses one bank.
for rank in range(4):
GameState.captain.skills["electrician"] = rank
inventory.energy_banks = 2
ship.set("energy", 0.0)
assert(inventory.use_energy_bank())
assert(is_equal_approx(float(ship.get("energy")), float(ship.get("max_energy")) * [0.10, 0.15, 0.20, 0.25][rank]))
ship.set("energy", float(ship.get("max_energy")) - 1.0)
assert(inventory.use_energy_bank())
assert(ship.get("energy") == ship.get("max_energy"))
assert(inventory.energy_banks == 0)
assert(preload("res://scripts/captain/captain_skills.gd").icon("electrician") != null)
assert(not GameState.captain.equip_skill(0, "electrician"))
# Tactics move toward, away from, or around their target, then flee below 50%.
var enemy := ENEMY.instantiate() as CharacterBody2D
enemy.position = Vector2(250, 0)
add_child(enemy)
enemy.set_physics_process(false)
enemy.set("_player", ship)
enemy.set("tactic", 0)
assert(enemy.call("combat_movement", Vector2(200, 0)).x > 0.0)
enemy.set("tactic", 1)
assert(enemy.call("combat_movement", Vector2(100, 0)).x < 0.0)
assert(enemy.call("combat_movement", Vector2(700, 0)).x > 0.0)
assert(absf(enemy.call("combat_movement", Vector2(420, 0)).y) > 0.0)
enemy.set("hull", float(enemy.get("max_hull")) * 0.5)
assert(not enemy.call("is_retreating"))
enemy.set("hull", float(enemy.get("max_hull")) * 0.49)
for tactic in [0, 1]:
enemy.set("tactic", tactic)
assert(enemy.call("is_retreating"))
assert(enemy.call("combat_movement", Vector2(500, 0)).x < 0.0)
enemy.set("hull", 10000.0)
# Module is lootable, charges exactly 50 ammo even with the ammunition passive.
assert(DRONES in preload("res://scripts/items/item_generator.gd").DEFINITIONS)
var module := ItemInstance.new()
module.definition = DRONES
inventory.extension_slots[0] = module
GameState.captain.skills["efficient_magazine"] = 3
inventory.ammunition = 49
assert(not ship.call("activate_ability", 0))
assert(inventory.ammunition == 49)
inventory.ammunition = 100
var dps := inventory.get_total_dps()
assert(dps > 0.0)
assert(ship.call("activate_ability", 0))
assert(inventory.ammunition == 50)
assert(not ship.call("activate_ability", 0))
var drones := get_tree().get_nodes_in_group("player_combat_drones")
assert(drones.size() == 3)
var total_dps := 0.0
for drone in drones:
drone.set_physics_process(false)
total_dps += float(drone.get("damage_per_shot")) / float(drone.FIRE_INTERVAL)
assert(is_equal_approx(total_dps, dps * 0.5))
assert(is_equal_approx(inventory.get_total_dps(), dps))
await get_tree().physics_frame
await get_tree().process_frame
var before := float(enemy.get("hull")) + float(enemy.get("shields"))
drones[0].position = Vector2(100, 0)
drones[0].call("_fire", enemy)
for bullet in get_children():
if bullet is Area2D and bullet.get_script() == preload("res://scripts/game/weapon_projectile.gd"):
bullet.call("_physics_process", 0.2)
assert(float(enemy.get("hull")) + float(enemy.get("shields")) < before, "Drone bullets must damage enemies")
assert(inventory.ammunition == 50, "No extra ammunition charged per shot")
systems.call("_physics_process", 10.0)
for drone in drones:
assert(drone.is_queued_for_deletion())
await get_tree().process_frame
assert(get_tree().get_nodes_in_group("player_combat_drones").is_empty())
systems.call("reset_combat_state")
assert(ship.call("activate_ability", 0))
var deployed := get_tree().get_nodes_in_group("player_combat_drones")
for drone in deployed:
drone.set_physics_process(false)
get_tree().paused = true
assert(not ship.call("activate_ability", 0))
await get_tree().create_timer(0.05, true).timeout
assert(float(systems.get("drones_remaining")) == 10.0)
get_tree().paused = false
ship.set("is_docked", true)
systems.call("_physics_process", 0.1)
await get_tree().process_frame
assert(get_tree().get_nodes_in_group("player_combat_drones").is_empty())
ship.set("is_docked", false)
systems.call("reset_combat_state")
inventory.ammunition = 50
assert(ship.call("activate_ability", 0))
systems.call("reset_combat_state")
await get_tree().process_frame
assert(get_tree().get_nodes_in_group("player_combat_drones").is_empty())
assert(float(systems.get("drones_remaining")) == 0.0)
print("ELECTRICIAN_DRONES_TACTICS_TEST: PASS (four bank strengths/caps, passive icon, tactics and retreat, loot module, exact ammo cost, 3 drones, shared 50% DPS, actual bullet damage, lifetime and reset)")
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/abilities/electrician_drones_tactics_test.gd" id="1"]
[node name="ElectricianDronesTacticsTest" type="Node2D"]
script = ExtResource("1")

View File

@ -80,6 +80,30 @@ func _ready() -> void:
assert(enemy.call("select_combat_target") == ship)
assert(not ship.call("activate_ability", 1))
# Mine: 5% maximum hull damage despite armor; slow lasts five seconds; one hit only.
var weapons := ship.get_node("WeaponController")
for weapon_id in ["plasma_cannon", "machine_gun", "gamma_cannon", "missile_launcher", "beam_laser"]:
systems.get("cooldowns").clear()
assert(ship.call("activate_ability", 1))
var weapon := GENERATOR.generate_for_definition(load("res://resources/items/%s.tres" % weapon_id), 1)
if weapon_id == "beam_laser":
ship.set("energy", 0.0)
weapons.call("_fire_laser", 0, weapon)
assert(ship.call("is_cloaked"), "Failed shot must preserve cloak")
ship.set("energy", ship.get("max_energy"))
weapons.call("_fire_laser", 0, weapon)
elif weapon_id == "missile_launcher":
inventory.weapon_slots[2] = weapon
inventory.missile_ammunition = 0
assert(not weapons.call("try_fire_missile", 2))
assert(ship.call("is_cloaked"), "Empty launcher must preserve cloak")
inventory.missile_ammunition = 1
assert(weapons.call("try_fire_missile", 2))
else:
weapons.call("_fire_projectile", weapon)
assert(not ship.call("is_cloaked"), "Every fired weapon must break cloak")
assert(ship.get_node("Sprite2D").material == original_material)
assert(ship.call("get_ability_cooldown", 1) == 60.0)
assert(enemy.call("select_combat_target") == ship)
assert(ship.call("activate_ability", 2))
var mine: Node2D = get_tree().get_nodes_in_group("player_mines")[0]
mine.set_physics_process(false)

View File

@ -44,10 +44,10 @@ func _ready() -> void:
captain.skill_durations.clear()
var base_regeneration: float = ship.get_energy_regeneration()
assert(captain.activate_skill("reactor_overload", ship))
assert(is_equal_approx(ship.get_energy_regeneration(), base_regeneration * [1.1, 1.2, 1.7][rank - 1]))
assert(is_equal_approx(ship.get_energy_regeneration(), base_regeneration * [1.2, 1.5, 2.0][rank - 1]))
ship.energy = 0.0
ship._physics_process(0.1)
assert(is_equal_approx(ship.energy, base_regeneration * [1.1, 1.2, 1.7][rank - 1] * 0.1))
assert(is_equal_approx(ship.energy, base_regeneration * [1.2, 1.5, 2.0][rank - 1] * 0.1))
assert(not captain.activate_skill("reactor_overload", ship))
assert(captain.activate_skill("tactical_maneuver", ship))
assert(is_equal_approx(ship.get_evasion_chance(), [0.3, 0.6, 1.0][rank - 1]))
@ -61,7 +61,7 @@ func _ready() -> void:
ship.shields = ship.max_shields * 0.25
assert(captain.activate_skill("power_redirect", ship))
assert(ship.shields == 0.0)
assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0 + [0.1, 0.2, 0.7][rank - 1] + [0.5, 0.9, 1.5][rank - 1]))
assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0 + [0.2, 0.5, 1.0][rank - 1] + [0.5, 0.9, 1.5][rank - 1]))
ship.shield_on_damage_percent = 100.0
ship.restore_shields_from_damage(100.0)
assert(ship.shields == 0.0)

View File

@ -0,0 +1,63 @@
extends Node
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 := game.get_node("PlayerShip") as CharacterBody2D
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
var systems := ship.get_node("ExtensionSystems")
systems.set_physics_process(false)
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.queue_free()
await get_tree().process_frame
var hud := game.get_node("HUD")
hud.call("_toggle_debug")
var inventory := ship.get_node("ShipInventory") as ShipInventory
var before := Vector3i(inventory.ammunition, inventory.irradiated_ammunition, inventory.missile_ammunition)
var button := hud.get_node("HUDRoot/DebugActions/AddAmmunition") as Button
button.pressed.emit()
button.pressed.emit()
assert(Vector3i(inventory.ammunition, inventory.irradiated_ammunition, inventory.missile_ammunition) == before + Vector3i(20, 20, 20))
systems.call("activate", 5)
var drones := get_tree().get_nodes_in_group("player_combat_drones")
assert(drones.size() == 3)
for drone in drones:
drone.set_physics_process(false)
assert(drone.scale == Vector2(0.5, 0.5))
# Stationary formation converges to fixed positions in front, with no idle orbit.
_step(ship, drones, 120)
for drone in drones:
assert(drone.global_position.distance_to(drone.call("formation_position")) < 2.0)
assert((drone.global_position - ship.global_position).dot(Vector2.UP.rotated(ship.rotation)) > 60.0)
# Forward flight and a turn inherit mother velocity without position snapping.
ship.velocity = Vector2(0, -200)
var previous_velocity: Vector2 = drones[0].get("velocity")
_step(ship, drones, 1)
assert((drones[0].get("velocity") as Vector2).distance_to(previous_velocity) <= 650.0 / 60.0 + 0.01)
_step(ship, drones, 119)
for drone in drones:
assert(drone.global_position.distance_to(drone.call("formation_position")) < 12.0)
ship.set("angular_velocity", 0.6)
_step(ship, drones, 120, true)
for drone in drones:
assert(drone.global_position.distance_to(drone.call("formation_position")) < 15.0)
ship.set("angular_velocity", 0.0)
ship.velocity = Vector2.ZERO
_step(ship, drones, 120)
for drone in drones:
assert(drone.global_position.distance_to(drone.call("formation_position")) < 2.0)
assert((drone.get("velocity") as Vector2).length() < 3.0)
print("DEBUG_AMMO_DRONE_FORMATION_TEST: PASS (debug ammo x10 repeatable, half scale, fixed forward formation, acceleration bound, moving/turning mother tracking, smooth stop)")
get_tree().quit()
func _step(ship: CharacterBody2D, drones: Array[Node], frames: int, turning := false) -> void:
for frame in frames:
if turning:
ship.rotation += 0.6 / 60.0
ship.velocity = Vector2.UP.rotated(ship.rotation) * 200.0
ship.position += ship.velocity / 60.0
for drone in drones:
drone.call("_physics_process", 1.0 / 60.0)

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/ships/debug_ammo_drone_formation_test.gd" id="1"]
[node name="DebugAmmoDroneFormationTest" type="Node"]
script = ExtResource("1")