Compare commits
2 Commits
72839139eb
...
9aa2dfebd6
| Author | SHA1 | Date | |
|---|---|---|---|
| 9aa2dfebd6 | |||
| 458db974c0 |
BIN
assets/Ships/combat_drone.png
Normal file
BIN
assets/Ships/combat_drone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
40
assets/Ships/combat_drone.png.import
Normal file
40
assets/Ships/combat_drone.png.import
Normal 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
|
||||
BIN
assets/icons/captain/electrician.png
Normal file
BIN
assets/icons/captain/electrician.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
40
assets/icons/captain/electrician.png.import
Normal file
40
assets/icons/captain/electrician.png.import
Normal 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
|
||||
BIN
assets/icons/extensions/combat_drones_art.png
Normal file
BIN
assets/icons/extensions/combat_drones_art.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
40
assets/icons/extensions/combat_drones_art.png.import
Normal file
40
assets/icons/extensions/combat_drones_art.png.import
Normal 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
|
||||
17
assets/icons/new_skills_art.md
Normal file
17
assets/icons/new_skills_art.md
Normal 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.
|
||||
BIN
assets/sounds/music/menu_music2.mp3
Normal file
BIN
assets/sounds/music/menu_music2.mp3
Normal file
Binary file not shown.
19
assets/sounds/music/menu_music2.mp3.import
Normal file
19
assets/sounds/music/menu_music2.mp3.import
Normal 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
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
22
resources/items/combat_drones.tres
Normal file
22
resources/items/combat_drones.tres
Normal 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")
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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))
|
||||
|
||||
116
scripts/game/combat_drone.gd
Normal file
116
scripts/game/combat_drone.gd
Normal 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)
|
||||
1
scripts/game/combat_drone.gd.uid
Normal file
1
scripts/game/combat_drone.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://f2rww506fhi7
|
||||
@ -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
|
||||
|
||||
55
scripts/game/enemy_mine.gd
Normal file
55
scripts/game/enemy_mine.gd
Normal 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))
|
||||
1
scripts/game/enemy_mine.gd.uid
Normal file
1
scripts/game/enemy_mine.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dgo884psqgnsd
|
||||
@ -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():
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
62
scripts/game/wormhole_vortex.gdshader
Normal file
62
scripts/game/wormhole_vortex.gdshader
Normal 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));
|
||||
}
|
||||
1
scripts/game/wormhole_vortex.gdshader.uid
Normal file
1
scripts/game/wormhole_vortex.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cintxpcnpffcx
|
||||
@ -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:
|
||||
|
||||
27
scripts/items/extension_cooldowns.gd
Normal file
27
scripts/items/extension_cooldowns.gd
Normal 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())
|
||||
1
scripts/items/extension_cooldowns.gd.uid
Normal file
1
scripts/items/extension_cooldowns.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://4nswtu72lqfp
|
||||
@ -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)))
|
||||
|
||||
@ -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))
|
||||
|
||||
48
scripts/items/mineral_pickup.gd
Normal file
48
scripts/items/mineral_pickup.gd
Normal 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)
|
||||
1
scripts/items/mineral_pickup.gd.uid
Normal file
1
scripts/items/mineral_pickup.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cssyy83yo8lss
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
50
scripts/ui/mission_objectives.gd
Normal file
50
scripts/ui/mission_objectives.gd
Normal 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)
|
||||
1
scripts/ui/mission_objectives.gd.uid
Normal file
1
scripts/ui/mission_objectives.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bd8lju10fyxbr
|
||||
111
tests/abilities/electrician_drones_tactics_test.gd
Normal file
111
tests/abilities/electrician_drones_tactics_test.gd
Normal 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()
|
||||
1
tests/abilities/electrician_drones_tactics_test.gd.uid
Normal file
1
tests/abilities/electrician_drones_tactics_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c1xp2w857yvwq
|
||||
4
tests/abilities/electrician_drones_tactics_test.tscn
Normal file
4
tests/abilities/electrician_drones_tactics_test.tscn
Normal 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")
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
63
tests/ships/debug_ammo_drone_formation_test.gd
Normal file
63
tests/ships/debug_ammo_drone_formation_test.gd
Normal 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)
|
||||
1
tests/ships/debug_ammo_drone_formation_test.gd.uid
Normal file
1
tests/ships/debug_ammo_drone_formation_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b0bk1yhyw8t0y
|
||||
4
tests/ships/debug_ammo_drone_formation_test.tscn
Normal file
4
tests/ships/debug_ammo_drone_formation_test.tscn
Normal 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")
|
||||
Loading…
x
Reference in New Issue
Block a user