diff --git a/artifacts/auxiliary_shield_preview.png b/artifacts/auxiliary_shield_preview.png new file mode 100644 index 0000000..3cff721 Binary files /dev/null and b/artifacts/auxiliary_shield_preview.png differ diff --git a/artifacts/auxiliary_shield_validation.md b/artifacts/auxiliary_shield_validation.md new file mode 100644 index 0000000..f5cf429 --- /dev/null +++ b/artifacts/auxiliary_shield_validation.md @@ -0,0 +1,12 @@ +# Tarcza zewnętrzna — weryfikacja + +Godot 4.7.2, 2026-10-26. + +- PASS: `tests/abilities/auxiliary_shield_test.tscn`: odblokowanie w laboratorium, aktywne rozszerzenie bez addonów, koszt 70% maksymalnej energii i blokada przy niewystarczającym zapasie, pojemność 50% maksymalnych tarcz w chwili aktywacji. Osobna pula nie regeneruje się i nie nakłada; obrażenia zużywają ją przed zwykłymi tarczami, a nadmiar przechodzi dalej na tarcze/kadłub. Prawidłowy cooldown, reset, dokowanie i wyłączenie przez Przekierowanie zasilania. Pociski i lasery zatrzymują się na większym obwodzie również przy pustych tarczach głównych. Zwykłe technologie tarcz nie zużywają odnowienia, gdy całe trafienie pochłonęła warstwa zewnętrzna. +- PASS: `tests/items/shield_collision_test.tscn`: dotychczasowe pociski, lasery, tarcze i przeszkody działają poprawnie. +- PASS: `tests/previews/auxiliary_shield_preview.tscn`: rzeczywista aktywacja rozszerzenia w scenie gry, podgląd obu warstw, ikona i odczyt dodatkowej pojemności, 1280×720. +- PASS: `git diff --check`. + +Nowa warstwa ma promień 1,45× promień zwykłej tarczy. Efekt: fioletowy obwód i siatka sześciokątów, osobne błyski trafień oraz licznik punktów osłony. + +Ikona: `assets/icons/extensions/auxiliary_shield.png`, wygenerowana wbudowanym image_gen z przezroczystym tłem. Prompt: `assets/icons/extensions/auxiliary_shield_prompt.md`. diff --git a/artifacts/external_turret_preview.png b/artifacts/external_turret_preview.png new file mode 100644 index 0000000..5708666 Binary files /dev/null and b/artifacts/external_turret_preview.png differ diff --git a/artifacts/external_turret_preview.png.import b/artifacts/external_turret_preview.png.import new file mode 100644 index 0000000..de59003 --- /dev/null +++ b/artifacts/external_turret_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dly01imnvtint" +path="res://.godot/imported/external_turret_preview.png-71ead23f36f1b832eab1a61b36fe5af2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/external_turret_preview.png" +dest_files=["res://.godot/imported/external_turret_preview.png-71ead23f36f1b832eab1a61b36fe5af2.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 diff --git a/artifacts/external_turret_validation.md b/artifacts/external_turret_validation.md new file mode 100644 index 0000000..bf53a38 --- /dev/null +++ b/artifacts/external_turret_validation.md @@ -0,0 +1,10 @@ +# Działka zewnętrzne — weryfikacja + +Godot 4.7.2, 2026-10-26. + +- PASS: `tests/items/external_turret_test.tscn`. Odblokowanie w laboratorium; dokładnie połowa obrażeń działka maszynowego przy tym samym losowaniu na poziomach 1, 5, 30, 120 i 1000; 49 amunicji nie wystarcza; rozstawienie pobiera dokładnie 50 pomimo Oszczędnego magazynka III. Jedno kliknięcie rozstawia jedno działko, przytrzymanie nie rozstawia kolejnych. Działko zostaje w swoim miejscu po odlocie statku, zachowuje amunicję bez celu, wykrywa cel i oddaje dokładnie 50 rzeczywistych pocisków. Zweryfikowano obrażenia przeciwnika, brak dodatkowego pobierania amunicji i usunięcie po wyczerpaniu magazynka/dokowaniu. Zmiana lub zdjęcie broni nie zmienia już rozstawionego działka. +- PASS: `tests/ships/laboratory_loot_test.tscn`. Broń nie występuje w początkowych łupach i pojawia się po badaniu; brak duplikatów w puli. +- PASS: `tests/previews/external_turret_preview.tscn`. Podgląd grafiki i pocisków w działającej scenie gry, 1280×720. +- PASS: `git diff --check`. + +Ikona i przezroczysty sprite powstały przez wbudowane image_gen. Pliki oraz prompty opisano w `assets/icons/external_turret_prompts.md`. diff --git a/artifacts/new_captain_skills_combat.png b/artifacts/new_captain_skills_combat.png new file mode 100644 index 0000000..c293f57 Binary files /dev/null and b/artifacts/new_captain_skills_combat.png differ diff --git a/artifacts/new_captain_skills_combat.png.import b/artifacts/new_captain_skills_combat.png.import new file mode 100644 index 0000000..1c0481e --- /dev/null +++ b/artifacts/new_captain_skills_combat.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cuyxd70erubuh" +path="res://.godot/imported/new_captain_skills_combat.png-258aa6a2d3c85d330ddcc3813c5322ab.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/new_captain_skills_combat.png" +dest_files=["res://.godot/imported/new_captain_skills_combat.png-258aa6a2d3c85d330ddcc3813c5322ab.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 diff --git a/artifacts/new_captain_skills_economy.png b/artifacts/new_captain_skills_economy.png new file mode 100644 index 0000000..c363f88 Binary files /dev/null and b/artifacts/new_captain_skills_economy.png differ diff --git a/artifacts/new_captain_skills_economy.png.import b/artifacts/new_captain_skills_economy.png.import new file mode 100644 index 0000000..35154dd --- /dev/null +++ b/artifacts/new_captain_skills_economy.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://te0rvaurv6rb" +path="res://.godot/imported/new_captain_skills_economy.png-4bbe4716bac2c848e6a45ba3c3e58f0d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/new_captain_skills_economy.png" +dest_files=["res://.godot/imported/new_captain_skills_economy.png-4bbe4716bac2c848e6a45ba3c3e58f0d.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 diff --git a/artifacts/new_captain_skills_validation.md b/artifacts/new_captain_skills_validation.md new file mode 100644 index 0000000..bbfe06b --- /dev/null +++ b/artifacts/new_captain_skills_validation.md @@ -0,0 +1,17 @@ +# Captain skills validation + +Godot 4.7.2, 2026-10-26. + +- PASS: `tests/captain/new_skills_test.tscn` — 14 skills; five new icons; training; all three ranks; laboratory timing; normal/radiation ammunition production and mineral costs; enemy bounty; missile cooldown; emergency repair resource checks, docking, cooldown, full hull and repair cap. +- PASS: `tests/ships/laboratory_loot_test.tscn` — starting plasma-only weapon pool, locked extensions, all research unlocks and no duplicate loot entries. +- PASS: `tests/previews/new_captain_skills_preview.tscn` — real game UI, all 14 owned skills, new choices, F1 assignment, emergency repair on the actual ship, and screenshots at 1280×720. +- PASS: `git diff --check`. + +Additional older regression scenes do not pass: + +- `tests/abilities/missile_system_test.gd:20` expects movement into weapon slot 2 without unlocking that slot. This change does not alter slot unlock rules. +- `tests/items/crafting_recipes_test.gd:54` expects immediate mineral credit after asteroid destruction; its null asteroid sprite also fails in `asteroid_destruction_effect.gd:8`. This change does not alter asteroid reward or debris behavior. The recipe transactions before that section succeed. + +The headless editor/runtime also reports sandbox access warnings for external Godot/MCP settings directories. New skill tests and the rendered preview complete successfully. + +Icons were generated with the built-in image_gen tool. Original prompts are saved in `assets/icons/captain/new_skills_prompts.md`. diff --git a/artifacts/pulse_laser_flying_preview.png b/artifacts/pulse_laser_flying_preview.png new file mode 100644 index 0000000..b28f9c3 Binary files /dev/null and b/artifacts/pulse_laser_flying_preview.png differ diff --git a/artifacts/pulse_laser_flying_preview.png.import b/artifacts/pulse_laser_flying_preview.png.import new file mode 100644 index 0000000..c5127c1 --- /dev/null +++ b/artifacts/pulse_laser_flying_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cm2lhdq7a3vul" +path="res://.godot/imported/pulse_laser_flying_preview.png-0c3fa1f2605505b790bdcb6cb95ee605.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/pulse_laser_flying_preview.png" +dest_files=["res://.godot/imported/pulse_laser_flying_preview.png-0c3fa1f2605505b790bdcb6cb95ee605.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 diff --git a/artifacts/pulse_laser_preview.png b/artifacts/pulse_laser_preview.png new file mode 100644 index 0000000..1da2e16 Binary files /dev/null and b/artifacts/pulse_laser_preview.png differ diff --git a/artifacts/pulse_laser_preview.png.import b/artifacts/pulse_laser_preview.png.import new file mode 100644 index 0000000..d0b5326 --- /dev/null +++ b/artifacts/pulse_laser_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://3b4vuo80tuyj" +path="res://.godot/imported/pulse_laser_preview.png-875bd161de519004da7a35acd303054d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/pulse_laser_preview.png" +dest_files=["res://.godot/imported/pulse_laser_preview.png-875bd161de519004da7a35acd303054d.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 diff --git a/artifacts/pulse_laser_validation.md b/artifacts/pulse_laser_validation.md new file mode 100644 index 0000000..acfe098 --- /dev/null +++ b/artifacts/pulse_laser_validation.md @@ -0,0 +1,13 @@ +# Laser pulsacyjny — weryfikacja + +Godot 4.7.2, 2026-10-26. + +- PASS: `tests/items/pulse_laser_test.tscn`. Odblokowanie w laboratorium, 50% szansy na pełne ominięcie tarcz, granica losowania i zgodność z orbem przebicia. Przy identycznych losowaniach na poziomach 1, 5, 30, 120 i 1000 broń zadaje 70% obrażeń lasera wiązkowego i zużywa 130% jego energii. Pojedyncze czerwone impulsy lecą z prędkością 1500 px/s (bazowo strzał co 0,2 s). Sprawdzone przesunięcie o 60 px w 0,04 s, brak natychmiastowych obrażeń i naliczanie trafienia dopiero po dotarciu do tarcz lub kadłuba; współdziałanie z technologią podwójnego trafienia i brak strzału przy zbyt małej ilości energii. +- PASS: `tests/items/external_turret_test.tscn`. Brak regresji obrażeń działek zewnętrznych po ujednoliceniu mnożników obrażeń przedmiotów. +- PASS: `tests/previews/pulse_laser_preview.tscn`. Podgląd dwóch osobnych, krótkich czerwonych pocisków w scenie gry, 1280×720. Plik: `artifacts/pulse_laser_flying_preview.png`. +- PASS: `tests/items/shield_collision_test.tscn`. Zwykłe pociski i lasery nadal zatrzymują się na tarczach; osłony fizyczne i trafienia styczne działają poprawnie. +- PASS: `tests/items/laser_energy_regeneration_test.tscn` (600 klatek fizyki, fixed-fps 60). Zwykły laser nadal prawidłowo zużywa energię podczas 10 s strzelania. +- PASS: `tests/items/weapon_icon_transparency_test.tscn`. Wszystkie 11 ikon broni ma przezroczyste tło i widoczną grafikę. Pięć edytowanych plików i prompt opisano w `assets/icons/weapon_background_removal.md`. +- PASS: `git diff --check`. + +Ikona powstała przez wbudowane image_gen. Plik `assets/icons/pulse_laser.png`, prompt `assets/icons/pulse_laser_prompt.md`; tło usunięto zgodnie z późniejszą prośbą. Lecące impulsy rysowane są przez istniejący system pocisków. Zwykłe i radiacyjne lasery zachowują dotychczasowe wiązki. diff --git a/artifacts/salvage_drone_skills.png b/artifacts/salvage_drone_skills.png new file mode 100644 index 0000000..dcea259 Binary files /dev/null and b/artifacts/salvage_drone_skills.png differ diff --git a/artifacts/salvage_drone_skills.png.import b/artifacts/salvage_drone_skills.png.import new file mode 100644 index 0000000..c48f5e8 --- /dev/null +++ b/artifacts/salvage_drone_skills.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dt73tyy7qplad" +path="res://.godot/imported/salvage_drone_skills.png-fda1f5a8c1e672dd96715b417d8c5d76.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/salvage_drone_skills.png" +dest_files=["res://.godot/imported/salvage_drone_skills.png-fda1f5a8c1e672dd96715b417d8c5d76.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 diff --git a/artifacts/salvage_drone_validation.md b/artifacts/salvage_drone_validation.md new file mode 100644 index 0000000..36ddf74 --- /dev/null +++ b/artifacts/salvage_drone_validation.md @@ -0,0 +1,9 @@ +# Zbieracz złomu i Dowódca dronów + +Godot 4.7.2, 2026-10-26. + +- PASS: `tests/captain/salvage_drone_skills_test.tscn`: 16 umiejętności, obie nowe są pasywne i dostępne do nauki, ikony są wczytywane. Sprawdzone prawdopodobieństwa 0/15/25/40%, 240 rzeczywistych wraków oraz brak ponownego naliczania części. Sprawdzone pociski trzech dronów dla wszystkich rang (0/10/20/30%), koszt amunicji i brak premii do broni statku. +- PASS: `tests/previews/salvage_drone_skills_preview.tscn`: prawdziwy panel kapitana i podgląd obu nowych umiejętności w 1280×720. +- PASS: `git diff --check`. + +Grafiki wygenerowano przez wbudowane image_gen. Pliki: `assets/icons/captain/scrap_collector.png` i `assets/icons/captain/drone_commander.png`. Prompty: `assets/icons/captain/salvage_drone_prompts.md`. diff --git a/artifacts/weapon_icons_transparent.png b/artifacts/weapon_icons_transparent.png new file mode 100644 index 0000000..250abc9 Binary files /dev/null and b/artifacts/weapon_icons_transparent.png differ diff --git a/artifacts/weapon_icons_transparent.png.import b/artifacts/weapon_icons_transparent.png.import new file mode 100644 index 0000000..47ede9f --- /dev/null +++ b/artifacts/weapon_icons_transparent.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dx4jv5dk1xbrs" +path="res://.godot/imported/weapon_icons_transparent.png-8a1321b5ba9f77e927c721ea7866d572.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/weapon_icons_transparent.png" +dest_files=["res://.godot/imported/weapon_icons_transparent.png-8a1321b5ba9f77e927c721ea7866d572.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 diff --git a/assets/Ships/external_turret.png b/assets/Ships/external_turret.png new file mode 100644 index 0000000..31b0da5 Binary files /dev/null and b/assets/Ships/external_turret.png differ diff --git a/assets/Ships/external_turret.png.import b/assets/Ships/external_turret.png.import new file mode 100644 index 0000000..36ef688 --- /dev/null +++ b/assets/Ships/external_turret.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://s5oof01scm1m" +path="res://.godot/imported/external_turret.png-58ad502f88f5ae7f1f60d289aeaf4a6b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/Ships/external_turret.png" +dest_files=["res://.godot/imported/external_turret.png-58ad502f88f5ae7f1f60d289aeaf4a6b.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 diff --git a/assets/icons/artillery_cannon_v2.png b/assets/icons/artillery_cannon_v2.png index 5ef7a9b..1177540 100644 Binary files a/assets/icons/artillery_cannon_v2.png and b/assets/icons/artillery_cannon_v2.png differ diff --git a/assets/icons/captain/bounty_hunter.png b/assets/icons/captain/bounty_hunter.png new file mode 100644 index 0000000..d72db6f Binary files /dev/null and b/assets/icons/captain/bounty_hunter.png differ diff --git a/assets/icons/captain/bounty_hunter.png.import b/assets/icons/captain/bounty_hunter.png.import new file mode 100644 index 0000000..65b02f8 --- /dev/null +++ b/assets/icons/captain/bounty_hunter.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d1vcvsbrhrmf4" +path="res://.godot/imported/bounty_hunter.png-8b18ddf2681343815eedd4d59beb82f6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/captain/bounty_hunter.png" +dest_files=["res://.godot/imported/bounty_hunter.png-8b18ddf2681343815eedd4d59beb82f6.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 diff --git a/assets/icons/captain/drone_commander.png b/assets/icons/captain/drone_commander.png new file mode 100644 index 0000000..ca74463 Binary files /dev/null and b/assets/icons/captain/drone_commander.png differ diff --git a/assets/icons/captain/drone_commander.png.import b/assets/icons/captain/drone_commander.png.import new file mode 100644 index 0000000..78cba6e --- /dev/null +++ b/assets/icons/captain/drone_commander.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cm0iwunhagi6v" +path="res://.godot/imported/drone_commander.png-5dc78d116784f5c76b3c0d1b41129369.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/captain/drone_commander.png" +dest_files=["res://.godot/imported/drone_commander.png-5dc78d116784f5c76b3c0d1b41129369.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 diff --git a/assets/icons/captain/emergency_procedure.png b/assets/icons/captain/emergency_procedure.png new file mode 100644 index 0000000..6c2c257 Binary files /dev/null and b/assets/icons/captain/emergency_procedure.png differ diff --git a/assets/icons/captain/emergency_procedure.png.import b/assets/icons/captain/emergency_procedure.png.import new file mode 100644 index 0000000..25a4764 --- /dev/null +++ b/assets/icons/captain/emergency_procedure.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ceqc0126fy1wf" +path="res://.godot/imported/emergency_procedure.png-12a516e7ce74a224dea88e0528e6aaf2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/captain/emergency_procedure.png" +dest_files=["res://.godot/imported/emergency_procedure.png-12a516e7ce74a224dea88e0528e6aaf2.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 diff --git a/assets/icons/captain/gunsmith.png b/assets/icons/captain/gunsmith.png new file mode 100644 index 0000000..8d3c3f6 Binary files /dev/null and b/assets/icons/captain/gunsmith.png differ diff --git a/assets/icons/captain/gunsmith.png.import b/assets/icons/captain/gunsmith.png.import new file mode 100644 index 0000000..d2241ae --- /dev/null +++ b/assets/icons/captain/gunsmith.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bmiwj4d4vsn2d" +path="res://.godot/imported/gunsmith.png-3fe35693f8c8ccdc7b74c1394e7c7767.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/captain/gunsmith.png" +dest_files=["res://.godot/imported/gunsmith.png-3fe35693f8c8ccdc7b74c1394e7c7767.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 diff --git a/assets/icons/captain/missile_specialist.png b/assets/icons/captain/missile_specialist.png new file mode 100644 index 0000000..f74e174 Binary files /dev/null and b/assets/icons/captain/missile_specialist.png differ diff --git a/assets/icons/captain/missile_specialist.png.import b/assets/icons/captain/missile_specialist.png.import new file mode 100644 index 0000000..57324e0 --- /dev/null +++ b/assets/icons/captain/missile_specialist.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://do5k2oipk04kx" +path="res://.godot/imported/missile_specialist.png-475ab16333c302ad739380e687c16621.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/captain/missile_specialist.png" +dest_files=["res://.godot/imported/missile_specialist.png-475ab16333c302ad739380e687c16621.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 diff --git a/assets/icons/captain/new_skills_prompts.md b/assets/icons/captain/new_skills_prompts.md new file mode 100644 index 0000000..216833a --- /dev/null +++ b/assets/icons/captain/new_skills_prompts.md @@ -0,0 +1,17 @@ +Captain skill icons +Generated with the built-in image_gen tool. + +scientist.png +Use case: stylized-concept. Asset type: square captain skill inventory icon for a sci-fi space RPG. A futuristic laboratory research apparatus: silver microscope examining a glowing cyan crystal, small orbital atom rings, scientific discovery. Polished detailed 3D game item render, silver gunmetal and brass materials, strong readable silhouette, centered single compact composition filling 80% of the square, subtle colored glow. Dark navy nearly black vignette background. Match a premium sci-fi loot icon collection. No lettering, no words, no numbers, no border, no watermark. Deliver one square icon. + +gunsmith.png +Use case: stylized-concept. Asset type: square captain ability icon for a science-fiction space exploration game. A compact futuristic industrial fabrication station with three copper cylindrical components, steel gear and precision caliper, warm amber glow. Polished detailed 3D game item render, silver gunmetal and brass, readable silhouette, centered compact composition filling 80% of square, subtle colored glow. Dark navy nearly black vignette background. No lettering, words, numbers, border or watermark. One square icon. + +bounty_hunter.png +Use case: stylized-concept. Asset type: square captain ability icon for a science-fiction space exploration game. A golden reward coin inside a futuristic silver navigation target reticle, glowing amber center, a small star embossed on the coin. Polished detailed 3D game item render, silver gunmetal and brass, readable silhouette, centered compact composition filling 80% of square, subtle colored glow. Dark navy nearly black vignette background. No lettering, words, numbers, border or watermark. One square icon. + +missile_specialist.png +Use case: stylized-concept. Asset type: square captain ability icon for a science-fiction space exploration game. Two miniature sleek silver aerospace rockets with red nose cones, cyan orbital guidance arcs, orange exhaust glow, space exploration theme. Polished detailed 3D game item render, silver gunmetal and brass, readable silhouette, centered compact composition filling 80% of square, subtle colored glow. Dark navy nearly black vignette background. No lettering, words, numbers, border or watermark. One square icon. + +emergency_procedure.png +Use case: stylized-concept. Asset type: square captain ability icon for a science-fiction space exploration game. A futuristic maintenance device: steel mechanical clamp repairing a cracked spaceship armor panel with bright green energy sparks and luminous green plus symbol. Polished detailed 3D game item render, silver gunmetal and brass, readable silhouette, centered compact composition filling 80% of square, subtle colored glow. Dark navy nearly black vignette background. No lettering, words, numbers, border or watermark. One square icon. diff --git a/assets/icons/captain/salvage_drone_prompts.md b/assets/icons/captain/salvage_drone_prompts.md new file mode 100644 index 0000000..8ab2a96 --- /dev/null +++ b/assets/icons/captain/salvage_drone_prompts.md @@ -0,0 +1,11 @@ +# Zbieracz złomu i Dowódca dronów — ikony + +Wygenerowano wbudowanym narzędziem image_gen. Pliki PNG w tym folderze. + +## scrap_collector.png + +Use case: stylized-concept. Asset type: square captain skill icon for a science fiction space exploration game. A futuristic salvage manipulator claw holding a single valuable silver mechanical spare part above a small pile of worn spaceship metal plates, amber and turquoise glow. Polished detailed 3D game item render, silver gunmetal and brass, strong readable silhouette, centered composition filling 80 percent of the square. Dark navy nearly black vignette background. Match premium sci-fi inventory icons. No text, letters, numbers, borders or watermarks. One square icon. + +## drone_commander.png + +Use case: stylized-concept. Asset type: square captain skill icon for a science fiction space exploration game. Three compact futuristic silver escort drones flying in a triangular formation around a glowing cyan command network emblem, blue engine glow, precise coordinated formation. Polished detailed 3D game item render, silver gunmetal and brass, strong readable silhouette, centered composition filling 80 percent of the square. Dark navy nearly black vignette background. Match premium sci-fi inventory icons. No text, letters, numbers, borders or watermarks. One square icon. diff --git a/assets/icons/captain/scientist.png b/assets/icons/captain/scientist.png new file mode 100644 index 0000000..8ba03b6 Binary files /dev/null and b/assets/icons/captain/scientist.png differ diff --git a/assets/icons/captain/scientist.png.import b/assets/icons/captain/scientist.png.import new file mode 100644 index 0000000..789218b --- /dev/null +++ b/assets/icons/captain/scientist.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c77wfevhr44c4" +path="res://.godot/imported/scientist.png-abf7bdb4ef5ddc4d4814cba7c7d9c41b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/captain/scientist.png" +dest_files=["res://.godot/imported/scientist.png-abf7bdb4ef5ddc4d4814cba7c7d9c41b.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 diff --git a/assets/icons/captain/scrap_collector.png b/assets/icons/captain/scrap_collector.png new file mode 100644 index 0000000..6783fcf Binary files /dev/null and b/assets/icons/captain/scrap_collector.png differ diff --git a/assets/icons/captain/scrap_collector.png.import b/assets/icons/captain/scrap_collector.png.import new file mode 100644 index 0000000..cbb9bf4 --- /dev/null +++ b/assets/icons/captain/scrap_collector.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c6m75lcwlno1s" +path="res://.godot/imported/scrap_collector.png-817780b245e5e65824c57f32c6d36774.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/captain/scrap_collector.png" +dest_files=["res://.godot/imported/scrap_collector.png-817780b245e5e65824c57f32c6d36774.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 diff --git a/assets/icons/extensions/auxiliary_shield.png b/assets/icons/extensions/auxiliary_shield.png new file mode 100644 index 0000000..576d1ea Binary files /dev/null and b/assets/icons/extensions/auxiliary_shield.png differ diff --git a/assets/icons/extensions/auxiliary_shield.png.import b/assets/icons/extensions/auxiliary_shield.png.import new file mode 100644 index 0000000..5fa76f5 --- /dev/null +++ b/assets/icons/extensions/auxiliary_shield.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://l1nalaps8sx5" +path="res://.godot/imported/auxiliary_shield.png-37edfc6ae717c3f9315f43e2835224a3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/extensions/auxiliary_shield.png" +dest_files=["res://.godot/imported/auxiliary_shield.png-37edfc6ae717c3f9315f43e2835224a3.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 diff --git a/assets/icons/extensions/auxiliary_shield_prompt.md b/assets/icons/extensions/auxiliary_shield_prompt.md new file mode 100644 index 0000000..d367aa6 --- /dev/null +++ b/assets/icons/extensions/auxiliary_shield_prompt.md @@ -0,0 +1,5 @@ +# Tarcza zewnętrzna — ikona + +Wygenerowano wbudowanym image_gen z przezroczystym tłem. Plik: `assets/icons/extensions/auxiliary_shield.png`. Efekt większej osłony jest rysowany w grze przez `scripts/game/auxiliary_shield_visual.gd`. + +Use case: stylized-concept. Asset type: square inventory extension icon for a science fiction spaceship game, isolated on truly transparent alpha background. A compact silver gunmetal and brass shield generator with glowing violet and cyan capacitor coils, projecting two concentric holographic protective domes: a smaller cyan shield inside a visibly larger violet hexagonal energy barrier. Centered premium detailed 3D game item render, strong readable silhouette filling 80 percent of square, matching realistic silver/brass sci-fi equipment icons. Keep all parts within canvas. No floor, no background, no text, no numbers, no border. Soft translucent energy glow allowed around the generator. diff --git a/assets/icons/external_turret.png b/assets/icons/external_turret.png new file mode 100644 index 0000000..ceb7806 Binary files /dev/null and b/assets/icons/external_turret.png differ diff --git a/assets/icons/external_turret.png.import b/assets/icons/external_turret.png.import new file mode 100644 index 0000000..e157c62 --- /dev/null +++ b/assets/icons/external_turret.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dg66tr30feho4" +path="res://.godot/imported/external_turret.png-e487322423837fe1dade2618e2a5c7a9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/external_turret.png" +dest_files=["res://.godot/imported/external_turret.png-e487322423837fe1dade2618e2a5c7a9.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 diff --git a/assets/icons/external_turret_prompts.md b/assets/icons/external_turret_prompts.md new file mode 100644 index 0000000..c18ec85 --- /dev/null +++ b/assets/icons/external_turret_prompts.md @@ -0,0 +1,11 @@ +# Działka zewnętrzne — grafiki + +Wygenerowano wbudowanym image_gen. Ikona: `assets/icons/external_turret.png`. Sprite: `assets/Ships/external_turret.png` (przezroczyste tło). + +## external_turret_icon + +Use case: stylized-concept. Asset type: square inventory weapon icon for a science fiction space game. One small autonomous floating sentry turret, compact circular silver gunmetal and brass chassis, twin forward-facing short mechanical barrels, visible brass ammunition drum holding cartridges, three small cyan levitation thrusters. Polished detailed 3D game item render, centered three-quarter view, strong readable silhouette, fills 80 percent of square, dark navy nearly black vignette background. No words, letters, numbers, border or watermark. + +## external_turret_sprite + +Use case: stylized-concept. Asset type: top-down 2D game sprite, isolated on actual transparent background. One small autonomous science fiction floating sentry turret. Strict orthographic view directly from above, no perspective. Compact circular silver gunmetal armored chassis with brass accents, two short parallel mechanical barrels pointing toward TOP of image, small ammunition drum, symmetrical side housings, three compact cyan anti-gravity thruster lights around the base. Hard-surface polished detailed 3D render matching a realistic sci-fi spaceship sprite set. Complete object centered fully inside square with generous transparent padding; crisp readable silhouette for rendering at 48 pixels. No ground, no shadow outside object, no backdrop, no words, no letters, no numbers, no border. diff --git a/assets/icons/incendiary_missile.png b/assets/icons/incendiary_missile.png index 9cd1296..ef22c5c 100644 Binary files a/assets/icons/incendiary_missile.png and b/assets/icons/incendiary_missile.png differ diff --git a/assets/icons/pulse_laser.png b/assets/icons/pulse_laser.png new file mode 100644 index 0000000..f1bdc26 Binary files /dev/null and b/assets/icons/pulse_laser.png differ diff --git a/assets/icons/pulse_laser.png.import b/assets/icons/pulse_laser.png.import new file mode 100644 index 0000000..92eda5f --- /dev/null +++ b/assets/icons/pulse_laser.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cyhy0cu1rdkaq" +path="res://.godot/imported/pulse_laser.png-ee7e313e6cd939780e2ffdbbfdbf414f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/pulse_laser.png" +dest_files=["res://.godot/imported/pulse_laser.png-ee7e313e6cd939780e2ffdbbfdbf414f.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 diff --git a/assets/icons/pulse_laser_prompt.md b/assets/icons/pulse_laser_prompt.md new file mode 100644 index 0000000..d896661 --- /dev/null +++ b/assets/icons/pulse_laser_prompt.md @@ -0,0 +1,5 @@ +# Laser pulsacyjny — ikona + +Wygenerowano wbudowanym image_gen. Plik: `assets/icons/pulse_laser.png`. + +Use case: stylized-concept. Asset type: square inventory weapon icon for a science fiction space game. One compact pulse laser emitter, silver gunmetal and brass housing, three red glowing capacitor rings, forward optical aperture emitting a short vivid crimson red energy pulse. Polished detailed 3D game item render, centered three-quarter view, strong readable silhouette filling 80 percent of square, dark navy nearly black vignette background, premium sci-fi equipment icon matching silver and brass spaceship technology. No words, letters, numbers, border or watermark. diff --git a/assets/icons/radiation_laser.png b/assets/icons/radiation_laser.png index b91433b..83da400 100644 Binary files a/assets/icons/radiation_laser.png and b/assets/icons/radiation_laser.png differ diff --git a/assets/icons/weapon_background_removal.md b/assets/icons/weapon_background_removal.md new file mode 100644 index 0000000..4ae59d9 --- /dev/null +++ b/assets/icons/weapon_background_removal.md @@ -0,0 +1,15 @@ +# Ikony broni — usunięcie tła + +Edytowano wbudowanym image_gen z transparent_background=true. Zastąpione pliki w assets/icons/: + +- incendiary_missile.png +- radiation_laser.png +- artillery_cannon_v2.png +- external_turret.png +- pulse_laser.png + +Pozostałe sześć ikon broni zachowano — już miały przezroczystość. + +Prompt dla każdej z pięciu edycji (oryginalna ikona jako edit target): + +Use case: background-extraction. Edit target: the provided inventory weapon icon. Remove ONLY the dark navy/black background and its vignette. Output actual transparent alpha background, including spaces around and between mechanical parts. Preserve the exact same weapon, silhouette, position, crop, scale, metallic textures, colors, lighting, detail and luminous accents. Keep attached flames/energy/thruster light as soft translucent glow where present. Do not redesign, add, remove or reposition any part of the weapon; do not add a floor, shadow, outline or replacement backdrop. Preserve the square canvas composition. diff --git a/resources/items/auxiliary_shield.tres b/resources/items/auxiliary_shield.tres new file mode 100644 index 0000000..819e18b --- /dev/null +++ b/resources/items/auxiliary_shield.tres @@ -0,0 +1,20 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=5 format=3] +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="definition"] +[ext_resource type="Script" path="res://scripts/abilities/auxiliary_shield_ability.gd" id="ability_script"] +[ext_resource type="Texture2D" path="res://assets/icons/extensions/auxiliary_shield.png" id="icon"] + +[sub_resource type="Resource" id="ability"] +script = ExtResource("ability_script") +ability_id = "auxiliary_shield" +cooldown = 30.0 +display_name = "Tarcza zewnętrzna" +description = "Zużywa 70% maksymalnej energii i tworzy osobną tarczę o pojemności 50% aktualnych maksymalnych tarcz statku. Większa, fioletowa osłona pochłania obrażenia przed zwykłymi tarczami. Działa do wyczerpania, nie regeneruje się i nie nakłada się na kolejną kopię. Dokowanie i Przekierowanie zasilania ją wyłączają." + +[resource] +script = ExtResource("definition") +item_id = "auxiliary_shield" +display_name = "Tarcza zewnętrzna" +icon = ExtResource("icon") +category = 5 +description = "Aktywne rozszerzenie statku. Tworzy dodatkową, większą warstwę ochronną. Zamontuj w slocie rozszerzeń i użyj przypisanego klawisza 1–5." +active_ability = SubResource("ability") diff --git a/resources/items/beam_laser.tres b/resources/items/beam_laser.tres index 124be9a..8b48d58 100644 --- a/resources/items/beam_laser.tres +++ b/resources/items/beam_laser.tres @@ -10,7 +10,7 @@ icon = ExtResource("icon") item_id = "beam_laser" display_name = "Laser wiązkowy" category = 0 -description = "Oszczędny laser pulsacyjny: małe obrażenia i zasięg około jednego ekranu. Koszt impulsu rośnie z obrażeniami i addonami egzemplarza." +description = "Oszczędny laser wiązkowy: małe obrażenia i zasięg około jednego ekranu. Koszt impulsu rośnie z obrażeniami i addonami egzemplarza." primary_stat = "damage" primary_value = 3.0 stat_suffix = " DMG" diff --git a/resources/items/external_turret.tres b/resources/items/external_turret.tres new file mode 100644 index 0000000..e94c313 --- /dev/null +++ b/resources/items/external_turret.tres @@ -0,0 +1,27 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1_definition"] +[ext_resource type="Texture2D" path="res://assets/icons/external_turret.png" id="icon"] + +[resource] +script = ExtResource("1_definition") +icon = ExtResource("icon") +item_id = "external_turret" +display_name = "Działka zewnętrzne" +category = 0 +description = "Kliknięcie rozstawia autonomiczne działko lewitujące w miejscu. Koszt: dokładnie 50 amunicji, która trafia do jego magazynka. Działko samodzielnie celuje i oddaje 50 strzałów; potem znika. Obrażenia każdego pocisku są o 50% niższe niż działka maszynowego z takimi samymi parametrami. Nie zużywa dalszej amunicji statku. Znika przy dokowaniu lub zniszczeniu statku." +primary_stat = "damage" +primary_value = 3.5 +stat_suffix = " DMG" +weapon_kind = 1 +weapon_effect = "external_turret" +fire_interval = 0.1 +weapon_cooldown = 1.0 +ammunition_cost = 50 +projectile_speed = 1150.0 +weapon_range = 550.0 +energy_cost = 0.0 +damage_lower_roll_min = 2 +damage_lower_roll_max = 4 +damage_spread_roll_min = 1 +damage_spread_roll_max = 1 diff --git a/resources/items/pulse_laser.tres b/resources/items/pulse_laser.tres new file mode 100644 index 0000000..6adcab1 --- /dev/null +++ b/resources/items/pulse_laser.tres @@ -0,0 +1,25 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=3 format=3] +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="definition"] +[ext_resource type="Texture2D" path="res://assets/icons/pulse_laser.png" id="icon"] + +[resource] +script = ExtResource("definition") +icon = ExtResource("icon") +item_id = "pulse_laser" +display_name = "Laser pulsacyjny" +category = 0 +description = "Wystrzeliwuje pojedyncze krótkie czerwone wiązki, które szybko lecą przez przestrzeń. Każdy impuls ma 50% szansy całkowicie ominąć tarcze i trafić w kadłub. Zadaje o 30% mniej obrażeń niż laser wiązkowy z takimi samymi parametrami i zużywa o 30% więcej energii." +primary_stat = "damage" +primary_value = 3.0 +stat_suffix = " DMG" +weapon_kind = 2 +weapon_effect = "pulse_laser" +fire_interval = 0.2 +laser_pulse_duration = 0.05 +projectile_speed = 1500.0 +weapon_range = 600.0 +energy_cost = 3.25 +damage_lower_roll_min = 1 +damage_lower_roll_max = 3 +damage_spread_roll_min = 1 +damage_spread_roll_max = 3 diff --git a/scripts/abilities/auxiliary_shield_ability.gd b/scripts/abilities/auxiliary_shield_ability.gd new file mode 100644 index 0000000..71cffee --- /dev/null +++ b/scripts/abilities/auxiliary_shield_ability.gd @@ -0,0 +1,16 @@ +extends ActiveAbility + +func unavailable_reason(ship: CharacterBody2D) -> String: + if GameState.captain.is_skill_active("power_redirect"): + return "Tarcze wyłączone przez Przekierowanie zasilania" + if float(ship.get("max_shields")) <= 0.0: + return "Wymagana dodatnia maksymalna pojemność tarcz" + if float(ship.get("auxiliary_shields")) > 0.0: + return "Dodatkowa tarcza już działa" + if float(ship.get("max_energy")) <= 0.0 or float(ship.get("energy")) < float(ship.get("max_energy"))*0.7: + return "Wymagane 70% maksymalnej energii" + return "" + +func activate(ship: CharacterBody2D) -> void: + if not unavailable_reason(ship).is_empty(): return + ship.call("create_auxiliary_shield") diff --git a/scripts/abilities/auxiliary_shield_ability.gd.uid b/scripts/abilities/auxiliary_shield_ability.gd.uid new file mode 100644 index 0000000..24bdbe5 --- /dev/null +++ b/scripts/abilities/auxiliary_shield_ability.gd.uid @@ -0,0 +1 @@ +uid://dl7dvyvf0ad1s diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index 5c81cac..314747b 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -37,6 +37,12 @@ func get_skill_rank(skill: String) -> int: func get_skill_bonus(skill: String) -> float: return SKILLS.value(skill, get_skill_rank(skill)) +func roll_wreck_spare_part(roll: float) -> bool: + if roll >= get_skill_bonus("scrap_collector"): + return false + parts += 1.0 + return true + func get_energy_bank_fraction() -> float: return maxf(0.10, get_skill_bonus("electrician")) @@ -173,16 +179,26 @@ func skill_unavailable_reason(skill: String, ship: CharacterBody2D) -> String: return "Odnowienie: %.1f s" % float(skill_cooldowns[skill]) if skill == "power_redirect" and (float(ship.get("max_shields")) <= 0.0 or float(ship.get("shields")) < float(ship.get("max_shields")) * 0.25): return "Wymagane min. 25% tarcz" + if skill == "emergency_procedure": + if parts < 1.0: return "Wymagana 1 część" + if float(ship.get("hull")) <= 0.0: return "Statek jest zniszczony" + if float(ship.get("hull")) >= float(ship.get("max_hull")): return "Kadłub nie wymaga naprawy" return "" func activate_skill(skill: String, ship: CharacterBody2D) -> bool: if not skill_unavailable_reason(skill, ship).is_empty(): return false - skill_cooldowns[skill] = SKILLS.COOLDOWN + skill_cooldowns[skill] = SKILLS.cooldown(skill) skill_durations[skill] = SKILLS.duration(skill) if skill == "power_redirect": ship.set("shields", 0.0) ship.call("_update_shield_visual") + if skill == "emergency_procedure": + parts -= 1.0 + var maximum: float = ship.get("max_hull") + ship.set("hull", minf(maximum, float(ship.get("hull")) + maximum * get_skill_bonus(skill))) + if ship.has_method("_hull_technology_feedback"): + ship.call("_hull_technology_feedback", "PROCEDURA AWARYJNA") emit_changed() return true @@ -203,6 +219,6 @@ func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_mul var index := clampi(tier, 0, 2) var scale_factor := preload("res://scripts/levels/level_balance.gd").item_stat_scale(clampi(system_level, 1, 1000)) var xp := encounter_xp(BALANCE.KILL_XP[index] * scale_factor, system_level, xp_multiplier) - var money := roundi(BALANCE.KILL_CREDITS[index] * scale_factor * maxf(0.0, credit_multiplier)) + var money := roundi(BALANCE.KILL_CREDITS[index] * scale_factor * maxf(0.0, credit_multiplier) * (1.0 + get_skill_bonus("bounty_hunter"))) add_rewards(xp, money if grant_credits else 0) return Vector2i(xp, money) diff --git a/scripts/captain/captain_skills.gd b/scripts/captain/captain_skills.gd index 9ed6db9..f02d9b2 100644 --- a/scripts/captain/captain_skills.gd +++ b/scripts/captain/captain_skills.gd @@ -4,8 +4,15 @@ const MAX_RANK := 3 static var _icon_cache: Dictionary = {} const COOLDOWN := 30.0 const DURATION := 5.0 -const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer", "electrician"] +const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer", "electrician", "scientist", "gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure", "scrap_collector", "drone_commander"] const DATA := { + "scrap_collector": {"name": "Zbieracz złomu", "active": false, "values": [0.15, 0.25, 0.40], "color": "d9b27c"}, + "drone_commander": {"name": "Dowódca dronów", "active": false, "values": [0.10, 0.20, 0.30], "color": "70dfff"}, + "scientist": {"name": "Naukowiec", "active": false, "values": [0.10, 0.20, 0.30], "color": "76d9ed"}, + "gunsmith": {"name": "Rusznikarz", "active": false, "values": [0.10, 0.20, 0.30], "color": "e8b26e"}, + "bounty_hunter": {"name": "Łowca nagród", "active": false, "values": [0.10, 0.20, 0.30], "color": "efcc70"}, + "missile_specialist": {"name": "Specjalista rakietowy", "active": false, "values": [0.10, 0.15, 0.20], "color": "f29576"}, + "emergency_procedure": {"name": "Procedura awaryjna", "active": true, "values": [0.05, 0.10, 0.15], "color": "85deb1"}, "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"}, @@ -23,11 +30,22 @@ static func value(skill: String, rank: int) -> float: return float(DATA[skill]["values"][clampi(rank, 1, MAX_RANK) - 1]) static func duration(skill: String) -> float: + if skill == "emergency_procedure": return 0.0 return 15.0 if skill == "reactor_overload" else (7.0 if skill == "power_redirect" else DURATION) +static func cooldown(skill: String) -> float: + return 60.0 if skill == "emergency_procedure" else COOLDOWN + static func icon(skill: String) -> Texture2D: if skill not in IDS: return null if not _icon_cache.has(skill): + if IDS.find(skill) >= 9: + var texture: Texture2D = load("res://assets/icons/captain/%s.png" % skill) + var artwork := texture.get_image() + artwork.resize(384,384,Image.INTERPOLATE_LANCZOS) + artwork.generate_mipmaps() + _icon_cache[skill] = ImageTexture.create_from_image(artwork) + return _icon_cache[skill] var atlas: Texture2D = load("res://assets/icons/captain/unified_skills.png") var cell := atlas.get_size()/3.0 var index := IDS.find(skill) @@ -44,6 +62,20 @@ static func icon(skill: String) -> Texture2D: static func description(skill: String, rank: int) -> String: var amount := roundi(value(skill, rank) * 100.0) match skill: + "scrap_collector": + return "%d%% szansy na 1 dodatkową część z przeszukanego wraku. Losowanie raz na wrak. Premie rang nie sumują się." % amount + "drone_commander": + return "Obrażenia dronów bojowych większe o %d%%. Premie rang nie sumują się." % amount + "scientist": + return "Nowe badania w laboratorium trwają o %d%% krócej. Premie rang nie sumują się." % amount + "gunsmith": + return "Produkcja zwykłej i radiacyjnej amunicji daje o %d%% więcej sztuk za ten sam koszt. Nie dotyczy rakiet. Premie rang nie sumują się." % amount + "bounty_hunter": + return "O %d%% więcej kredytów za pokonanych wrogów. Premia obejmuje kredyty do zebrania po walce. Premie rang nie sumują się." % amount + "missile_specialist": + return "Czas między wystrzeleniem rakiet krótszy o %d%%. Działa po premiach sprzętu; minimalny odstęp to 1 s. Premie rang nie sumują się." % amount + "emergency_procedure": + return "Natychmiast naprawia %d%% maksymalnego kadłuba za 1 część. Wymaga uszkodzonego statku i aktywnego lotu." % amount "electrician": return "Powerbank przywraca %d%% maksymalnej energii zamiast 10%%. Umiejętność pasywna; premie rang nie sumują się." % amount "lucky": diff --git a/scripts/game/auxiliary_shield_visual.gd b/scripts/game/auxiliary_shield_visual.gd new file mode 100644 index 0000000..3214a68 --- /dev/null +++ b/scripts/game/auxiliary_shield_visual.gd @@ -0,0 +1,26 @@ +extends "res://scripts/game/shield_visual.gd" + +func _draw() -> void: + if current_shields > 0.0: + var ratio := clampf(current_shields/max_shields,0.0,1.0) + var tint := Color("ba8cff") + var pulse := 0.85+sin(_time*2.0)*0.15 + draw_circle(Vector2.ZERO,shield_radius,Color(tint,0.025*pulse)) + draw_arc(Vector2.ZERO,shield_radius,0,TAU,112,Color(tint,0.55*pulse),1.8,true) + draw_arc(Vector2.ZERO,shield_radius+2.0,0,TAU,112,Color(tint,0.11),5.0,true) + for index in 12: + var start := TAU*index/12.0+_time*0.12 + draw_arc(Vector2.ZERO,shield_radius-4.0,start,start+0.25,8,Color(Color("8ee9ff"),0.2+ratio*0.3),1.0,true) + for row in range(-3,4): + for column in range(-3,4): + var center := Vector2(column*18.0+9.0*(posmod(row,2)),row*15.6) + if center.length()+10.0 >= shield_radius: continue + var hexagon := PackedVector2Array() + for vertex in 7: hexagon.append(center+Vector2.from_angle(vertex*TAU/6.0+PI/6.0)*10.0) + draw_polyline(hexagon,Color(tint,0.045*pulse),0.7,true) + # Counter-rotate the readout so it stays legible as the ship turns. + draw_set_transform(Vector2.ZERO,-global_rotation) + draw_string(ThemeDB.fallback_font,Vector2(-65,-shield_radius-12),"OSŁONA %.0f / %.0f" % [current_shields,max_shields],HORIZONTAL_ALIGNMENT_CENTER,130,11,Color("d7bfff")) + draw_set_transform(Vector2.ZERO) + # Keep the existing directional impact ripples on the larger perimeter. + if not _hits.is_empty(): super._draw() diff --git a/scripts/game/auxiliary_shield_visual.gd.uid b/scripts/game/auxiliary_shield_visual.gd.uid new file mode 100644 index 0000000..a8ad350 --- /dev/null +++ b/scripts/game/auxiliary_shield_visual.gd.uid @@ -0,0 +1 @@ +uid://d2r7pt27irbcr diff --git a/scripts/game/combat_drone.gd b/scripts/game/combat_drone.gd index 6346adf..bb8fe21 100644 --- a/scripts/game/combat_drone.gd +++ b/scripts/game/combat_drone.gd @@ -90,7 +90,8 @@ func _clear_shot(target: Node2D) -> bool: 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) + var damage := damage_per_shot * (1.0 + GameState.captain.get_skill_bonus("drone_commander")) + bullet.call("configure", global_position + direction * 9.0, direction, ItemDefinition.WeaponKind.MACHINE_GUN, 1100.0, damage, 650.0) get_parent().add_child(bullet) _flash = 0.06 mother.get_node("ExtensionSystems").call("break_cloak_on_fire") diff --git a/scripts/game/deployed_turret.gd b/scripts/game/deployed_turret.gd new file mode 100644 index 0000000..719cb15 --- /dev/null +++ b/scripts/game/deployed_turret.gd @@ -0,0 +1,85 @@ +extends Node2D + +const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn") +const ARTWORK := preload("res://assets/Ships/external_turret.png") +const AUDIO := preload("res://scripts/game/weapon_audio.gd") +static var _sprite: Texture2D +var mother: CharacterBody2D +var weapon: ItemInstance +var ammunition := 50 +var _fire_timer := 0.35 +var _age := 0.0 +var _flash := 0.0 + +func _ready() -> void: + if _sprite == null: + var image := ARTWORK.get_image() + image.resize(256,256,Image.INTERPOLATE_LANCZOS) + image.generate_mipmaps() + _sprite = ImageTexture.create_from_image(image) + add_to_group("player_external_turrets") + z_index = 5 + texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS + +func _physics_process(delta: float) -> void: + if not is_instance_valid(mother) or bool(mother.get("_is_destroyed")) or bool(mother.get("is_docked")) or ammunition <= 0: + queue_free() + return + _age += delta + _flash = maxf(0.0,_flash-delta) + _fire_timer = maxf(0.0,_fire_timer-delta) + var target := _find_target() + if target != null: + var direction := target.global_position-global_position + rotation = rotate_toward(rotation,direction.angle()+PI*0.5,8.0*delta) + if _fire_timer <= 0.0 and absf(angle_difference(rotation,direction.angle()+PI*0.5)) < 0.12: + _fire(target) + queue_redraw() + +func _find_target() -> Node2D: + var closest: Node2D + var distance := weapon.definition.weapon_range + for enemy in get_tree().get_nodes_in_group("enemies"): + if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0 or bool(enemy.get("virus_controlled")): + continue + var candidate := global_position.distance_to(enemy.global_position) + if candidate < distance and _clear_shot(enemy): + distance = candidate + closest = enemy + return closest + +func _clear_shot(target: Node2D) -> bool: + var query := PhysicsRayQueryParameters2D.create(global_position,target.global_position,77,[mother.get_rid()]) + 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: + if ammunition <= 0 or is_queued_for_deletion() or not is_instance_valid(target): return + var direction := (target.global_position-global_position).normalized() + var hit: Dictionary = mother.call("roll_weapon_hit",weapon) + var bullet := PROJECTILE.instantiate() + bullet.configure(global_position+direction*25.0,direction,ItemDefinition.WeaponKind.MACHINE_GUN,weapon.definition.projectile_speed,float(hit.damage),weapon.definition.weapon_range,null,bool(hit.critical)) + bullet.shield_piercing = weapon.shield_piercing() + bullet.damage_type_override = weapon.get_damage_type() + bullet.source_ship = mother + get_parent().add_child(bullet) + ammunition -= 1 + _fire_timer = weapon.get_turret_fire_interval() + _flash = 0.05 + AUDIO.play(get_parent(),global_position,ItemDefinition.WeaponKind.MACHINE_GUN,-8.0) + mother.get_node("WeaponController").call("_break_cloak_on_fire") + if ammunition == 0: queue_free() + +func _draw() -> void: + var bob := Vector2(0,sin(_age*2.5)*2.0) + var appear := smoothstep(0.0,0.3,_age) + draw_circle(Vector2.ZERO,19.0,Color(0.1,0.7,1.0,0.08)) + draw_arc(Vector2.ZERO,30.0,-PI*0.5,-PI*0.5+TAU*float(ammunition)/50.0,48,Color("70dfff"),1.6,true) + draw_set_transform(bob,0.0,Vector2.ONE*lerpf(0.4,1.0,appear)) + draw_texture_rect(_sprite,Rect2(-30,-30,60,60),false) + if _flash > 0.0: + for x in [-7.0,7.0]: + draw_line(Vector2(x,-24),Vector2(x,-38),Color("ffdb86"),3.0,true) + draw_circle(Vector2(x,-28),5.0,Color(1.0,0.65,0.2,0.45)) + draw_set_transform(Vector2.ZERO,-rotation) + draw_string(ThemeDB.fallback_font,Vector2(-11,43),str(ammunition),HORIZONTAL_ALIGNMENT_CENTER,22,11,Color("b9e7f5")) diff --git a/scripts/game/deployed_turret.gd.uid b/scripts/game/deployed_turret.gd.uid new file mode 100644 index 0000000..d93e396 --- /dev/null +++ b/scripts/game/deployed_turret.gd.uid @@ -0,0 +1 @@ +uid://3w0qsvkh8rob diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index 1b0a734..7a06cbd 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -367,7 +367,7 @@ func _fire_plasma(direction: Vector2) -> void: WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, weapon_kind, -4.0) func take_piercing_weapon_hit(amount: float, critical: bool, damage_type: int, piercing: float) -> void: - _weapon_piercing = clampf(piercing,0.0,0.5) + _weapon_piercing = clampf(piercing,0.0,1.0) take_weapon_hit(amount,critical,damage_type) _weapon_piercing = 0.0 @@ -376,7 +376,7 @@ func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTyp func take_research_weapon_hit(amount: float, critical: bool, damage_type: int, piercing: float, disabled_system: int, damage_scale := 1.0) -> void: _research_damage_scale = damage_scale - _weapon_piercing = clampf(piercing,0.0,0.5) + _weapon_piercing = clampf(piercing,0.0,1.0) _take_player_hit(amount,critical,damage_type,true,disabled_system) _weapon_piercing = 0.0 _research_damage_scale = 1.0 @@ -443,7 +443,7 @@ func take_damage(amount: float, critical: bool = false, damage_type: int = Damag var piercing_damage := actual * _weapon_piercing if _research_player_hit else 0.0 var shield_damage := minf(shields, (actual - piercing_damage) * shield_multiplier) var double_hit := _research_direct_hit and research.learned("weapons_a_next_next_next") - if double_hit: shield_damage = minf(shields,actual*shield_multiplier) + if double_hit and _weapon_piercing < 1.0: shield_damage = minf(shields,actual*shield_multiplier) var remaining := actual if double_hit else maxf(0.0, actual - shield_damage / shield_multiplier) var shields_before := shields shields -= shield_damage diff --git a/scripts/game/extension_systems.gd b/scripts/game/extension_systems.gd index dfb8d9e..a84306e 100644 --- a/scripts/game/extension_systems.gd +++ b/scripts/game/extension_systems.gd @@ -17,6 +17,7 @@ var _has_destination := false func reset_combat_state() -> void: cooldowns.clear() + ship.clear_auxiliary_shield() _clear_drones() solar_remaining = 0.0 if cloak_remaining > 0.0: @@ -167,7 +168,7 @@ func _start_cloak() -> void: if shadow != null: _shadow_was_visible = shadow.visible shadow.hide() - for name in ["ShieldVisual", "EngineVisual", "SolarEnergyEffect", "EnergyIndicator"]: + for name in ["ShieldVisual", "AuxiliaryShieldVisual", "EngineVisual", "SolarEnergyEffect", "EnergyIndicator"]: var visual := ship.get_node_or_null(name) as CanvasItem if visual != null: _visual_colors[name] = visual.modulate diff --git a/scripts/game/shield_intersections.gd b/scripts/game/shield_intersections.gd index 252e5c7..a32f3f9 100644 --- a/scripts/game/shield_intersections.gd +++ b/scripts/game/shield_intersections.gd @@ -9,9 +9,13 @@ static func intercept(world_hit: Dictionary, start: Vector2, end: Vector2, mask: for target in tree.get_nodes_in_group("shielded_combatants"): if not is_instance_valid(target) or target.is_queued_for_deletion(): continue if (int(target.collision_layer)&mask)==0 or target.get_rid() in excluded: continue - if float(target.shields)<=0.0 or float(target.hull)<=0.0: continue + if float(target.hull)<=0.0: continue if target.has_method("is_mini_boosting") and (bool(target.is_docked) or bool(target._is_destroyed)): continue - var shield := target.get_node_or_null("ShieldVisual") as Node2D + var shield: Node2D + if target.has_method("get_active_shield_visual"): + shield = target.get_active_shield_visual() + elif float(target.shields) > 0.0: + shield = target.get_node_or_null("ShieldVisual") as Node2D if shield == null: continue var radius := float(shield.shield_radius)*shield.global_transform.x.length() var offset := start-shield.global_position diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 343a485..6f34ae2 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -34,6 +34,9 @@ var scanner_range := 1000.0 @export var max_energy := 100.0 var hull := 100.0 var shields := 100.0 +var auxiliary_shields := 0.0 +var auxiliary_max_shields := 0.0 +var _auxiliary_shield_visual: Node2D var energy := 100.0 var energy_regeneration := 5.0 var shield_regeneration := 0.0 @@ -125,6 +128,7 @@ func _ready() -> void: extension_systems.set_script(preload("res://scripts/game/extension_systems.gd")) extension_systems.set("cooldowns", $ShipInventory.extension_cooldowns) add_child(extension_systems) + _ensure_auxiliary_shield_visual() for index in ShipInventory.EXTENSION_SLOT_COUNT: _ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key) _ensure_input_action(&"use_energy_bank", KEY_R) @@ -201,9 +205,14 @@ func take_damage(amount: float, _critical: bool = false, damage_type: int = Dama get_parent().add_child(popup) return var remaining_damage: float = hit["damage"] + var auxiliary_damage := minf(auxiliary_shields,remaining_damage) + auxiliary_shields -= auxiliary_damage + remaining_damage -= auxiliary_damage + if auxiliary_damage > 0.0 and is_instance_valid(_auxiliary_shield_visual): + _auxiliary_shield_visual.flash_hit(visual_contact.point,damage_type) var research := preload("res://scripts/planets/alien_research.gd") var shields_before := shields - if shields > 0.0: + if shields > 0.0 and remaining_damage > 0.0: if research.learned("shields_b_next_next") and _salvo_cooldown <= 0.0: remaining_damage *= 0.1 _salvo_cooldown = 60.0 @@ -385,6 +394,38 @@ func _spawn_hit_effect(at_position: Vector2, effect_color: Color) -> void: func _update_shield_visual() -> void: if is_instance_valid(_shield_visual): _shield_visual.call("set_shield_values", shields, max_shields) + if is_instance_valid(_auxiliary_shield_visual): + if GameState.captain.is_skill_active("power_redirect"): auxiliary_shields = 0.0 + _auxiliary_shield_visual.shield_radius = $ShieldVisual.shield_radius*1.45 + _auxiliary_shield_visual.scale = $ShieldVisual.scale + _auxiliary_shield_visual.set_shield_values(auxiliary_shields,auxiliary_max_shields) + +func _ensure_auxiliary_shield_visual() -> void: + if is_instance_valid(_auxiliary_shield_visual): return + _auxiliary_shield_visual = Node2D.new() + _auxiliary_shield_visual.name = "AuxiliaryShieldVisual" + _auxiliary_shield_visual.set_script(preload("res://scripts/game/auxiliary_shield_visual.gd")) + _auxiliary_shield_visual.z_index = 3 + add_child(_auxiliary_shield_visual) + _update_shield_visual() + +func create_auxiliary_shield() -> void: + if _is_destroyed or is_docked or max_shields <= 0.0 or max_energy <= 0.0 or auxiliary_shields > 0.0 or energy < max_energy*0.7 or GameState.captain.is_skill_active("power_redirect"): + return + energy -= max_energy*0.7 + auxiliary_max_shields = max_shields*0.5 + auxiliary_shields = auxiliary_max_shields + _ensure_auxiliary_shield_visual() + _update_shield_visual() + _auxiliary_shield_visual.flash_hit() + +func clear_auxiliary_shield() -> void: + auxiliary_shields = 0.0 + _update_shield_visual() + +func get_active_shield_visual() -> Node2D: + if auxiliary_shields > 0.0 and is_instance_valid(_auxiliary_shield_visual): return _auxiliary_shield_visual + return $ShieldVisual if shields > 0.0 else null func _destroy_ship() -> void: if _is_destroyed: @@ -463,6 +504,7 @@ func _physics_process(delta: float) -> void: if GameState.captain.is_skill_active("power_redirect"): shields = 0.0 if _is_destroyed or is_docked: + if auxiliary_shields > 0.0: clear_auxiliary_shield() _mini_boost_active = false return _refill_recovery_budgets(delta) diff --git a/scripts/game/weapon_controller.gd b/scripts/game/weapon_controller.gd index 9fa21ae..b4e7b83 100644 --- a/scripts/game/weapon_controller.gd +++ b/scripts/game/weapon_controller.gd @@ -104,6 +104,11 @@ func _physics_process(delta: float) -> void: continue var definition := item.definition + if definition.weapon_effect == "external_turret": + _laser_pulse_timers[slot_index] = 0.0 + _laser_lines[slot_index].visible = false + if is_just_pressed: try_deploy_turret(slot_index) + continue if definition.weapon_kind != ItemDefinition.WeaponKind.LASER: _laser_pulse_timers[slot_index] = 0.0 _laser_lines[slot_index].visible = false @@ -121,6 +126,33 @@ func _physics_process(delta: float) -> void: for slot_index in ShipInventory.WEAPON_SLOT_COUNT: _fire_just_pressed[slot_index] = false +func try_deploy_turret(slot_index: int) -> bool: + if slot_index < 0 or slot_index >= ShipInventory.WEAPON_SLOT_COUNT or _shot_timers[slot_index] > 0.0: + return false + if get_tree().paused or bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")): + return false + var item := inventory.get_item("weapon",slot_index) + if item == null or item.definition.weapon_effect != "external_turret" or inventory.ammunition < 50: + return false + var turret := Node2D.new() + turret.set_script(preload("res://scripts/game/deployed_turret.gd")) + turret.set("mother",ship) + turret.set("weapon",item.duplicate(true)) + var direction := _aim_direction_from(ship.global_position) + turret.position = ship.global_position + direction * 70.0 + turret.rotation = direction.angle() + PI * 0.5 + inventory.ammunition -= 50 + inventory.ammunition_changed.emit() + ship.get_parent().add_child(turret) + _shot_timers[slot_index] = item.get_fire_interval() + _break_cloak_on_fire() + var flash := Node2D.new() + flash.set_script(preload("res://scripts/game/deployment_flash.gd")) + flash.position = turret.position + flash.set("color",Color("70dfff")) + ship.get_parent().add_child(flash) + return true + func try_fire_missile(slot_index: int = 2) -> bool: if missile_cooldown_remaining > 0.0 or bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")): return false @@ -156,7 +188,8 @@ func _fire_projectile(item: ItemInstance) -> void: ) ship.get_parent().add_child(projectile) projectile.reset_physics_interpolation() - projectile.set("shield_piercing", item.shield_piercing()) + projectile.set("shield_piercing", item.roll_shield_piercing()) + if definition.weapon_effect == "pulse_laser": projectile.set("source_ship",ship) projectile.set("damage_type_override", item.get_damage_type()) projectile.set("missile_homing", definition.missile_homing) if definition.weapon_effect == "artillery": @@ -180,9 +213,17 @@ func _exit_tree() -> void: func _fire_laser(slot_index: int, item: ItemInstance) -> void: var definition := item.definition var line := _laser_lines[slot_index] + if definition.weapon_effect == "pulse_laser": + line.visible = false + _laser_pulse_timers[slot_index] = 0.0 + if _shot_timers[slot_index] <= 0.0 and _consume_energy(item.get_energy_cost()): + _fire_projectile(item) + _shot_timers[slot_index] = item.get_fire_interval() + return line.set("radiation_beam",item.get_damage_type() == DamageTypes.Type.RADIATION) if _shot_timers[slot_index] > 0.0 or float(ship.get("energy")) < _effective_energy_cost(item.get_energy_cost()): return + var piercing := item.roll_shield_piercing() var muzzle_position := _muzzle_position() var direction := _aim_direction_from(muzzle_position) var endpoint := muzzle_position + direction * definition.weapon_range @@ -193,7 +234,8 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void: [ship.get_rid()] ) var result := ship.get_world_2d().direct_space_state.intersect_ray(query) - result = preload("res://scripts/game/shield_intersections.gd").intercept(result,muzzle_position,endpoint,RAY_COLLISION_MASK,[ship.get_rid()],get_tree()) + if piercing < 1.0: + result = preload("res://scripts/game/shield_intersections.gd").intercept(result,muzzle_position,endpoint,RAY_COLLISION_MASK,[ship.get_rid()],get_tree()) if not result.is_empty(): endpoint = result["position"] line.clear_points() @@ -210,7 +252,7 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void: var target: Node = result.get("collider") var hull_before := float(target.get("hull")) if target.has_method("take_research_weapon_hit") else -1.0 preload("res://scripts/game/combat_vfx.gd").mark_hit(target,endpoint,direction) - _apply_damage(target, float(hit["damage"]), bool(hit["critical"]), item.shield_piercing(), item.get_damage_type()) + _apply_damage(target, float(hit["damage"]), bool(hit["critical"]), piercing, item.get_damage_type()) _apply_laser_radiation(target,item,float(hit.damage)) _spawn_laser_impact(endpoint,item.get_damage_type(),direction) if hull_before >= 0 and float(target.get("hull")) < hull_before and preload("res://scripts/planets/alien_research.gd").learned("hull_a_next_next"): @@ -222,12 +264,13 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void: if is_instance_valid(prior) and prior.get_rid() not in exclusions: exclusions.append(prior.get_rid()) var through := PhysicsRayQueryParameters2D.create(endpoint+direction*2.0,muzzle_position+direction*definition.weapon_range,RAY_COLLISION_MASK,exclusions) var second := ship.get_world_2d().direct_space_state.intersect_ray(through) - second = preload("res://scripts/game/shield_intersections.gd").intercept(second,endpoint+direction*2.0,muzzle_position+direction*definition.weapon_range,RAY_COLLISION_MASK,exclusions,get_tree()) + if piercing < 1.0: + second = preload("res://scripts/game/shield_intersections.gd").intercept(second,endpoint+direction*2.0,muzzle_position+direction*definition.weapon_range,RAY_COLLISION_MASK,exclusions,get_tree()) var second_end: Vector2 = second.get("position",muzzle_position+direction*definition.weapon_range) line.add_point(second_end) if not second.is_empty(): preload("res://scripts/game/combat_vfx.gd").mark_hit(second.get("collider"),second_end,direction) - preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(second.get("collider"),float(hit.damage)*0.5,bool(hit.critical),item.get_damage_type(),item.shield_piercing(),visited,false,-1,0.5) + preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(second.get("collider"),float(hit.damage)*0.5,bool(hit.critical),item.get_damage_type(),piercing,visited,false,-1,0.5) _apply_laser_radiation(second.get("collider"),item,float(hit.damage)*0.5) _spawn_laser_impact(second_end,item.get_damage_type(),direction) _laser_pulse_timers[slot_index] = definition.laser_pulse_duration diff --git a/scripts/game/weapon_projectile.gd b/scripts/game/weapon_projectile.gd index 687f24e..fc673bc 100644 --- a/scripts/game/weapon_projectile.gd +++ b/scripts/game/weapon_projectile.gd @@ -140,7 +140,8 @@ func _physics_process(delta: float) -> void: exclusions ) var result := get_world_2d().direct_space_state.intersect_ray(query) - result = preload("res://scripts/game/shield_intersections.gd").intercept(result,previous_position,next_position,collision_mask,exclusions,get_tree()) + if shield_piercing < 1.0: + result = preload("res://scripts/game/shield_intersections.gd").intercept(result,previous_position,next_position,collision_mask,exclusions,get_tree()) if not result.is_empty(): global_position = result["position"] _hit_target(result.get("collider")) @@ -199,6 +200,14 @@ func _find_enemy_in_acquisition_range() -> Node2D: return nearest_enemy func _draw() -> void: + if weapon_effect == "pulse_laser": + # A single compact bolt: it travels with this projectile, never spans the aim ray. + draw_line(Vector2(-27,0),Vector2(7,0),Color(1.0,0.02,0.06,0.12),13,true) + draw_line(Vector2(-24,0),Vector2(7,0),Color(1.0,0.04,0.08,0.35),7,true) + draw_line(Vector2(-22,0),Vector2(7,0),Color(1.0,0.08,0.12),3.5,true) + draw_line(Vector2(-17,0),Vector2(7,0),Color(1.0,0.7,0.7),1.1,true) + draw_circle(Vector2(7,0),2.3,Color("ffd4d7")) + return if weapon_effect == "artillery": draw_line(Vector2(-55,0),Vector2.ZERO,Color(1,0.4,0.08,0.2),10,true) draw_line(Vector2(-38,0),Vector2(10,0),Color(1,0.8,0.35),4,true) @@ -339,6 +348,7 @@ func _spawn_impact_effect(is_explosion: bool) -> void: is_explosion = true effect.call("configure", is_explosion, effect_color) effect.call("configure_contact",get_damage_type(),direction) + if weapon_effect == "pulse_laser": effect.call("configure",false,Color("ff3448")) func _explode_at_target(impact_target: Node) -> void: if _harmless_intercepted: return diff --git a/scripts/items/crafting_recipes.gd b/scripts/items/crafting_recipes.gd index 990c835..c2baacc 100644 --- a/scripts/items/crafting_recipes.gd +++ b/scripts/items/crafting_recipes.gd @@ -17,3 +17,9 @@ const DATA := { static func icon(id: String) -> Texture2D: var extension := "svg" if id == "energy_bank" else "png" return load("res://assets/icons/resources/%s.%s" % [id, extension]) as Texture2D + +static func output_amount(id: String, captain: Resource) -> int: + var amount := int(DATA[id]["amount"]) + if id in ["ammunition", "irradiated_ammunition"]: + amount = roundi(amount * (1.0 + captain.get_skill_bonus("gunsmith"))) + return amount diff --git a/scripts/items/item_generator.gd b/scripts/items/item_generator.gd index cc130de..fed2120 100644 --- a/scripts/items/item_generator.gd +++ b/scripts/items/item_generator.gd @@ -10,11 +10,14 @@ const DEFINITIONS := [ preload("res://resources/items/mine_layer.tres"), preload("res://resources/items/flares.tres"), preload("res://resources/items/solar_shield_charge.tres"), + preload("res://resources/items/auxiliary_shield.tres"), preload("res://resources/items/gamma_cannon.tres"), preload("res://resources/items/plasma_cannon.tres"), preload("res://resources/items/beam_laser.tres"), + preload("res://resources/items/pulse_laser.tres"), preload("res://resources/items/machine_gun.tres"), + preload("res://resources/items/external_turret.tres"), preload("res://resources/items/missile_launcher.tres"), preload("res://resources/items/unguided_missile.tres"), preload("res://resources/items/reinforced_shield.tres"), @@ -33,8 +36,8 @@ static func generate(level: int, enemy_tier: int = 0) -> ItemInstance: return generate_for_definition(available_definitions().pick_random(), level, enemy_tier) static func available_definitions() -> Array: - var pool := DEFINITIONS.duplicate() var lab := preload("res://scripts/planets/laboratory.gd") + var pool := DEFINITIONS.filter(func(definition): return definition.item_id not in lab.IDS) for id in lab.IDS: if GameState.captain.expedition.laboratory_unlocks.get(id,false): pool.append(lab.definition(id)) return pool diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index c599c5c..e68b913 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -14,6 +14,17 @@ var _instance_ability: ActiveAbility func shield_piercing() -> float: return 0.5 if definition != null and definition.category == ItemDefinition.Category.WEAPON and "piercing" in special_orbs else 0.0 +func roll_shield_piercing(roll: float = -1.0) -> float: + if definition != null and definition.weapon_effect == "pulse_laser": + if (randf() if roll < 0.0 else roll) < 0.5: return 1.0 + return shield_piercing() + +func weapon_damage_scale() -> float: + if definition == null: return 1.0 + if definition.weapon_effect == "external_turret": return 0.5 + if definition.weapon_effect == "pulse_laser": return 0.7 + return 1.0 + func get_damage_type() -> int: if "electricity" in special_orbs: return DamageTypes.Type.ENERGY if "radiation" in special_orbs: return DamageTypes.Type.RADIATION @@ -28,6 +39,9 @@ func get_fire_interval() -> float: return maxf(1.0, maxf(1.0, 5.0 - get_stat_total("missile_cooldown_reduction")) / attack_speed_multiplier()) return maxf(definition.fire_interval, definition.weapon_cooldown) / attack_speed_multiplier() +func get_turret_fire_interval() -> float: + return definition.fire_interval / attack_speed_multiplier() + func is_passive_extension() -> bool: return definition != null and definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and not definition.passive_effect.is_empty() @@ -115,10 +129,14 @@ func get_damage_range() -> Vector2: for modifier in additional_modifiers: if modifier and modifier.stat_key == "damage": flat_bonus += modifier.value - return Vector2(maxf(0.0, minf(lower, upper) + flat_bonus), maxf(0.0, maxf(lower, upper) + flat_bonus)) + var result := Vector2(maxf(0.0, minf(lower, upper) + flat_bonus), maxf(0.0, maxf(lower, upper) + flat_bonus)) + return result * weapon_damage_scale() func roll_damage() -> float: var damage_range := get_damage_range() + var damage_scale := weapon_damage_scale() + if damage_scale != 1.0: + return damage_range.x + randi_range(0, roundi((damage_range.y-damage_range.x)/damage_scale)) * damage_scale # Inclusive integer rolls, preserving fractional flat bonuses on both bounds. var whole_span := floori(damage_range.y - damage_range.x) return damage_range.x + randi_range(0, whole_span) @@ -132,10 +150,14 @@ func get_energy_cost() -> float: var base_lower := roundf((definition.damage_lower_roll_min + definition.damage_lower_roll_max) * 0.5) var base_spread := roundf((definition.damage_spread_roll_min + definition.damage_spread_roll_max) * 0.5) var base_average := maxf(1.0, base_lower + base_spread * 0.5) + base_average *= weapon_damage_scale() var damage_range := get_damage_range() var average := maxf(0.1, (damage_range.x + damage_range.y) * 0.5) var damage_factor := pow(average / base_average, definition.energy_damage_exponent) var addon_factor := 1.0 + get_addon_count() * definition.energy_per_addon + if definition.weapon_effect == "pulse_laser": + # Apply the surcharge after the ordinary laser's rounding, preserving +30%. + return snappedf(maxf(0.0,definition.energy_cost/1.3*damage_factor*addon_factor),0.1)*1.3 return snappedf(maxf(0.0, definition.energy_cost * damage_factor * addon_factor), 0.1) func get_damage_range_text() -> String: @@ -191,8 +213,12 @@ func get_tooltip_text() -> String: lines.append("Regeneracja generatora: +%.1f /s" % ((energy_regeneration_override if energy_regeneration_override >= 0.0 else definition.energy_regeneration_bonus) * ENERGY_REGENERATION_BONUS_MULTIPLIER)) if definition.category == ItemDefinition.Category.WEAPON: lines.append("Typ obrażeń: %s" % DamageTypes.NAMES[get_damage_type()]) + if definition.weapon_effect == "pulse_laser": lines.append("Prędkość lecącej wiązki: %.0f px/s" % definition.projectile_speed) lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.") - if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: + if definition.weapon_effect == "external_turret": + lines.append("Zasięg działka: %.0f px • Magazynek: 50 pocisków • Koszt rozstawienia: 50 amunicji" % definition.weapon_range) + lines.append("Odstęp strzałów działka: %.2f s • Kolejne rozstawienie: %.2f s" % [get_turret_fire_interval(),get_fire_interval()]) + elif definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: lines.append("Zasięg: %.0f px • Amunicja: %d / strzał" % [definition.weapon_range,definition.ammunition_cost]) elif definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: lines.append("Zasięg: %.0f px • Napromieniowana amunicja: 1 / strzał" % definition.weapon_range) @@ -200,7 +226,7 @@ func get_tooltip_text() -> String: lines.append("Amunicja: 1 rakieta / strzał • Cooldown bazowy: 5 s • Bez kosztu energii") else: lines.append("Zasięg: %.0f px • Energia: %.1f / strzał" % [definition.weapon_range, get_energy_cost()]) - lines.append("Odstęp strzałów: %.2f s" % get_fire_interval()) + if definition.weapon_effect != "external_turret": lines.append("Odstęp strzałów: %.2f s" % get_fire_interval()) if not additional_modifiers.is_empty(): lines.append("\nADDONY (aktywne po zamontowaniu)") for modifier in additional_modifiers: diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index 2e0e23e..a6d89ae 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -136,7 +136,7 @@ func craft(recipe_id: String) -> bool: var recipe: Dictionary = RECIPES.DATA[recipe_id] for mineral in recipe["cost"]: GameState.captain.minerals[mineral] = int(GameState.captain.minerals.get(mineral, 0)) - int(recipe["cost"][mineral]) - var amount := int(recipe["amount"]) + var amount := RECIPES.output_amount(recipe_id, GameState.captain) match recipe_id: "parts": GameState.captain.parts += amount "ammunition": ammunition += amount @@ -400,7 +400,7 @@ func get_missile_cooldown(item: ItemInstance) -> float: for equipped in weapon_slots.slice(0,unlocked_weapon_slots) + module_slots: if equipped != null and equipped != item: reduction += equipped.get_stat_total("missile_cooldown_reduction") - return maxf(1.0, maxf(1.0, 5.0 - reduction) / item.attack_speed_multiplier()) + return maxf(1.0, maxf(1.0, 5.0 - reduction) / item.attack_speed_multiplier() * (1.0 - GameState.captain.get_skill_bonus("missile_specialist"))) func consume_missile() -> bool: if missile_ammunition <= 0: @@ -577,8 +577,9 @@ func _weapon_dps(item: ItemInstance, bonuses: Dictionary) -> float: var chance := clampf(GameState.captain.get_skill_bonus("sharp_eye") + float(bonuses.get("critical_chance_percent", 0.0)) / 100.0, 0.0, 0.60) var critical_factor := preload("res://scripts/captain/captain_balance.gd").BASE_CRITICAL_MULTIPLIER * (1.0 + maxf(0.0, float(bonuses.get("critical_damage_percent", 0.0)) / 100.0)) var interval := item.get_fire_interval() + if item.definition.weapon_effect == "external_turret": interval = item.get_turret_fire_interval() if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: - interval = maxf(1.0, maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))) / item.attack_speed_multiplier()) + interval = maxf(1.0, maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))) / item.attack_speed_multiplier() * (1.0 - GameState.captain.get_skill_bonus("missile_specialist"))) var value := item.get_stat_total("damage") * damage_factor * (1.0 + chance * (critical_factor - 1.0)) / maxf(0.001, interval) if item.definition.weapon_effect == "radiation_laser" and item.get_damage_type() == DamageTypes.Type.RADIATION: value *= 1.0+minf(preload("res://scripts/game/radiation_status.gd").DURATION,interval) diff --git a/scripts/levels/exploration_poi.gd b/scripts/levels/exploration_poi.gd index 9e7bd36..004688d 100644 --- a/scripts/levels/exploration_poi.gd +++ b/scripts/levels/exploration_poi.gd @@ -164,6 +164,9 @@ func claim() -> bool: var amount := _rng.randi_range(40, 60) inventory.ammunition += amount summary = "+%d amunicji" % amount + if kind == "wreck" and GameState.captain.get_skill_bonus("scrap_collector") > 0.0: + if GameState.captain.roll_wreck_spare_part(_rng.randf()): + summary += " · +1 część (Zbieracz złomu)" inventory.ammunition_changed.emit() GameState.captain.emit_changed() if kind != "ruin": diff --git a/scripts/planets/expedition_controller.gd b/scripts/planets/expedition_controller.gd index 1b39680..73844cf 100644 --- a/scripts/planets/expedition_controller.gd +++ b/scripts/planets/expedition_controller.gd @@ -227,6 +227,7 @@ func _enter() -> void: _hud_was_visible = game.get_node("HUD/HUDRoot").visible game.get_node("HUD/HUDRoot").hide() player.is_docked = true + player.clear_auxiliary_shield() player.velocity = Vector2.ZERO get_tree().paused = true _prompt.hide() diff --git a/scripts/planets/freighter_screen.gd b/scripts/planets/freighter_screen.gd index e429f99..96bd0ca 100644 --- a/scripts/planets/freighter_screen.gd +++ b/scripts/planets/freighter_screen.gd @@ -518,7 +518,7 @@ func _build_supply_production() -> void: var card := MENU.card(grid) MENU.style(card.get_parent(),6) card.add_theme_constant_override("separation",4) - card.get_parent().tooltip_text = recipe.name+" · partia: %d szt." % recipe.amount + card.get_parent().tooltip_text = recipe.name+" · partia: %d szt." % RECIPES.output_amount(id,captain) var row := MENU.row(card) MENU.icon(row,RECIPES.icon(id),28) var stock := MENU.label(row,str(inventory.get_crafted_stock(id) if is_instance_valid(inventory) else 0),13,MENU.GOLD) @@ -532,7 +532,7 @@ func _build_supply_production() -> void: button.size_flags_horizontal = Control.SIZE_EXPAND_FILL var cost: Dictionary = recipe.cost.duplicate() for key in cost: cost[key] = int(cost[key])*batches - button.tooltip_text = "%s: %d partii = %d szt.\nKoszt: %s" % [recipe.name,batches,int(recipe.amount)*batches,UI.material_text(cost)] + button.tooltip_text = "%s: %d partii = %d szt.\nKoszt: %s" % [recipe.name,batches,RECIPES.output_amount(id,captain)*batches,UI.material_text(cost)] button.disabled = not _can_craft_supply(id,batches) func _can_craft_supply(id: String, batches: int) -> bool: diff --git a/scripts/planets/laboratory.gd b/scripts/planets/laboratory.gd index 9f87aca..6d251ab 100644 --- a/scripts/planets/laboratory.gd +++ b/scripts/planets/laboratory.gd @@ -1,7 +1,33 @@ extends RefCounted const DURATION := 60.0 -const IDS := ["incendiary_missile", "radiation_laser", "artillery_cannon"] +const IDS := [ + "gamma_cannon", "beam_laser", "machine_gun", "missile_launcher", "unguided_missile", + "incendiary_missile", "radiation_laser", "artillery_cannon", "external_turret", "pulse_laser", + "recycler", "virus", "titanium_armor", "combat_drones", "teleport", "cloak", + "mine_layer", "flares", "solar_shield_charge", "auxiliary_shield", + "afterburner", "shield_charge", "repair_drones", +] const COSTS := { + "auxiliary_shield":{"uranium":45,"aluminium":40,"titanium":30,"silicon":15}, + "pulse_laser":{"uranium":40,"aluminium":35,"titanium":25,"copper":15}, + "external_turret":{"iron":50,"titanium":35,"aluminium":25,"sulfur":20}, + "gamma_cannon":{"uranium":45,"titanium":30,"aluminium":20}, + "beam_laser":{"uranium":30,"aluminium":30,"iron":15}, + "machine_gun":{"iron":30,"titanium":20,"sulfur":15}, + "missile_launcher":{"iron":40,"sulfur":30,"uranium":15}, + "unguided_missile":{"iron":30,"sulfur":25,"aluminium":15}, + "recycler":{"iron":25,"aluminium":25,"titanium":15}, + "virus":{"uranium":25,"aluminium":30,"titanium":15}, + "titanium_armor":{"titanium":45,"iron":30}, + "combat_drones":{"iron":35,"titanium":25,"uranium":20}, + "teleport":{"uranium":45,"aluminium":30,"titanium":20}, + "cloak":{"uranium":35,"aluminium":30,"titanium":20}, + "mine_layer":{"iron":30,"sulfur":25,"uranium":15}, + "flares":{"sulfur":25,"aluminium":20,"iron":15}, + "solar_shield_charge":{"uranium":25,"aluminium":35,"titanium":15}, + "afterburner":{"iron":25,"sulfur":30,"aluminium":20}, + "shield_charge":{"uranium":30,"aluminium":25,"titanium":20}, + "repair_drones":{"iron":30,"titanium":25,"aluminium":20}, "incendiary_missile":{"iron":45,"sulfur":60,"uranium":30,"copper":12,"mercury":6}, "radiation_laser":{"uranium":60,"aluminium":45,"silicon":18,"copper":12}, "artillery_cannon":{"iron":75,"titanium":45,"nickel":15,"tungsten":10}, @@ -29,11 +55,15 @@ static func start(captain: Resource, id: String) -> bool: if not can_start(captain,id): return false for key in COSTS[id]: captain.minerals[key] = int(captain.minerals.get(key,0))-int(COSTS[id][key]) captain.expedition.laboratory_project = id - captain.expedition.laboratory_remaining = DURATION + captain.expedition.laboratory_duration = research_duration(captain) + captain.expedition.laboratory_remaining = captain.expedition.laboratory_duration captain.emit_changed() captain.expedition.emit_changed() return true +static func research_duration(captain: Resource) -> float: + return DURATION * (1.0 - captain.get_skill_bonus("scientist")) + static func advance(progress: Resource, delta: float) -> bool: if progress.laboratory_project.is_empty() or delta <= 0.0: return false progress.laboratory_remaining = maxf(0.0,progress.laboratory_remaining-delta) diff --git a/scripts/planets/laboratory_panel.gd b/scripts/planets/laboratory_panel.gd index 1291931..2987bc5 100644 --- a/scripts/planets/laboratory_panel.gd +++ b/scripts/planets/laboratory_panel.gd @@ -12,13 +12,13 @@ func _ready() -> void: _build() func _state() -> String: - return str(captain.expedition.laboratory_project)+str(captain.expedition.laboratory_unlocks) + return str(captain.expedition.laboratory_project)+str(captain.expedition.laboratory_unlocks)+str(captain.get_skill_rank("scientist")) func _process(_delta: float) -> void: if _signature != _state(): _build() if is_instance_valid(_timer): _timer.text = "Pozostało: %d s gry" % ceili(captain.expedition.laboratory_remaining) - _bar.value = LAB.DURATION-captain.expedition.laboratory_remaining + _bar.value = captain.expedition.laboratory_duration-captain.expedition.laboratory_remaining for id in _buttons: if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id) @@ -30,8 +30,8 @@ func _build() -> void: _buttons.clear() _signature = _state() var progress: Resource = captain.expedition - MENU.label(self,"PROJEKTY NOWEGO UZBROJENIA",15,MENU.GOLD) - MENU.label(self,"Wybierz projekt. Badanie trwa 60 sekund gry. Wynalazek odblokowuje nową broń w łupach wrogów.",12,MENU.MUTED) + MENU.label(self,"PROJEKTY BRONI I ROZSZERZEŃ",15,MENU.GOLD) + MENU.label(self,"Wybierz projekt. Nowe badanie trwa %d sekund gry. Odkryta broń lub rozszerzenie zacznie pojawiać się w łupach. Od początku dostępne jest działko plazmowe." % roundi(LAB.research_duration(captain)),12,MENU.MUTED) if not progress.laboratory_project.is_empty(): var item := LAB.definition(progress.laboratory_project) var row := MENU.row(self) @@ -39,7 +39,7 @@ func _build() -> void: MENU.label(row,item.display_name,16,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL _timer = MENU.label(self,"",14) _bar = ProgressBar.new() - _bar.max_value = LAB.DURATION + _bar.max_value = progress.laboratory_duration _bar.custom_minimum_size.y = 18 add_child(_bar) else: @@ -55,7 +55,7 @@ func _build() -> void: var costs: Array[String] = [] for key in LAB.COSTS[id]: costs.append("%s: %d / %d" % [names[key],int(captain.minerals.get(key,0)),int(LAB.COSTS[id][key])]) MENU.label(card,"KOSZT · posiadasz / wymagane\n"+" · ".join(costs),12,MENU.MUTED) - var button := MENU.button(card,"ROZPOCZNIJ BADANIE · 60 s",_start.bind(id),true) + var button := MENU.button(card,"ROZPOCZNIJ BADANIE · %d s" % roundi(LAB.research_duration(captain)),_start.bind(id),true) button.name = "Research_"+id button.disabled = not LAB.can_start(captain,id) _buttons[id] = button diff --git a/scripts/planets/planet_progress.gd b/scripts/planets/planet_progress.gd index 30710f9..513754f 100644 --- a/scripts/planets/planet_progress.gd +++ b/scripts/planets/planet_progress.gd @@ -12,6 +12,7 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd") @export var laboratory_offers: Array[String] = [] @export var laboratory_project: String = "" @export var laboratory_remaining: float = 0.0 +@export var laboratory_duration: float = 60.0 @export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"} @export var mining_stations: Dictionary = {} @export var distant_maps: Dictionary = {} diff --git a/scripts/ui/captain_skills_ui.gd b/scripts/ui/captain_skills_ui.gd index 81996c7..f5fe052 100644 --- a/scripts/ui/captain_skills_ui.gd +++ b/scripts/ui/captain_skills_ui.gd @@ -207,7 +207,8 @@ func _choice_card(skill: String) -> PanelContainer: description.size_flags_vertical = Control.SIZE_EXPAND_FILL body.add_child(description) if data["active"]: - body.add_child(_label("DZIAŁANIE %d s · ODNOWIENIE 30 s" % int(CATALOG.duration(skill)), 11, Color("91aabd"))) + var timing := "NATYCHMIAST" if CATALOG.duration(skill) <= 0.0 else "DZIAŁANIE %d s" % int(CATALOG.duration(skill)) + body.add_child(_label("%s · ODNOWIENIE %d s" % [timing, int(CATALOG.cooldown(skill))], 11, Color("91aabd"))) else: body.add_child(_label("STAŁY EFEKT", 11, Color("91aabd"))) var select := Button.new() @@ -294,8 +295,8 @@ func _process(_delta: float) -> void: var cooldown := float(_captain.skill_cooldowns.get(skill, 0.0)) var reason := _captain.skill_unavailable_reason(skill, player) button.disabled = get_tree().paused or not reason.is_empty() - button.call("set_visual_state", cooldown / CATALOG.COOLDOWN, duration > 0.0, button.disabled) - button.tooltip_text = "%s · poziom %d\n%s\n[%s] · Odnowienie 30 s%s" % [CATALOG.DATA[skill]["name"], _captain.get_skill_rank(skill), CATALOG.description(skill, _captain.get_skill_rank(skill)), entry["binding"], "\n" + reason if not reason.is_empty() else ""] + button.call("set_visual_state", cooldown / CATALOG.cooldown(skill), duration > 0.0, button.disabled) + button.tooltip_text = "%s · poziom %d\n%s\n[%s] · Odnowienie %d s%s" % [CATALOG.DATA[skill]["name"], _captain.get_skill_rank(skill), CATALOG.description(skill, _captain.get_skill_rank(skill)), entry["binding"], int(CATALOG.cooldown(skill)), "\n" + reason if not reason.is_empty() else ""] status.text = "%.1f" % duration if duration > 0.0 else (str(ceili(cooldown)) if cooldown > 0.0 else "") status.modulate = Color("a9ef83") if duration > 0.0 else Color("dae8ef") _bar.visible = not bool(player.get("_is_destroyed")) and not _screen.visible diff --git a/scripts/ui/crafting_recipe_button.gd b/scripts/ui/crafting_recipe_button.gd index 31b3f0d..68efbf1 100644 --- a/scripts/ui/crafting_recipe_button.gd +++ b/scripts/ui/crafting_recipe_button.gd @@ -6,6 +6,7 @@ var inventory: ShipInventory var _stock: Label var _availability: Label var _picture: TextureRect +var _output: Label func _ready() -> void: custom_minimum_size = Vector2(120, 166) @@ -42,10 +43,10 @@ func _ready() -> void: _picture.mouse_filter = Control.MOUSE_FILTER_IGNORE artwork.add_child(_picture) _picture.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - var output := _label("+%d" % RECIPES.DATA[recipe_id]["amount"], 12, Color("efc681")) - output.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT - artwork.add_child(output) - output.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _output = _label("", 12, Color("efc681")) + _output.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + artwork.add_child(_output) + _output.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var title := _label(RECIPES.DATA[recipe_id]["name"], 11, Color("e5edf4")) title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.custom_minimum_size.y = 36 @@ -70,6 +71,7 @@ func refresh() -> void: if _stock == null: return var available := inventory.can_craft(recipe_id) + _output.text = "+%d" % RECIPES.output_amount(recipe_id, GameState.captain) _stock.text = "Zapas: %d" % inventory.get_crafted_stock(recipe_id) _availability.text = "WYTWÓRZ" if available else "BRAK SUROWCÓW" _availability.add_theme_color_override("font_color", Color("85deb1") if available else Color("d08c8e")) @@ -90,7 +92,7 @@ func _make_custom_tooltip(_text: String) -> Object: var contents := VBoxContainer.new() contents.add_theme_constant_override("separation", 8) panel.add_child(contents) - contents.add_child(_label("%s ×%d" % [RECIPES.DATA[recipe_id]["name"], RECIPES.DATA[recipe_id]["amount"]], 13, Color("efc681"))) + contents.add_child(_label("%s ×%d" % [RECIPES.DATA[recipe_id]["name"], RECIPES.output_amount(recipe_id, GameState.captain)], 13, Color("efc681"))) contents.add_child(HSeparator.new()) for mineral in RECIPES.DATA[recipe_id]["cost"]: var needed := int(RECIPES.DATA[recipe_id]["cost"][mineral]) diff --git a/scripts/ui/ship_crafting_panel.gd b/scripts/ui/ship_crafting_panel.gd index 5cbba8c..ccd2ca9 100644 --- a/scripts/ui/ship_crafting_panel.gd +++ b/scripts/ui/ship_crafting_panel.gd @@ -66,7 +66,7 @@ func _heading(caption: String, color: Color, font_size: int = 11) -> Label: func _craft(id: String) -> void: if _inventory.craft(id): - _status.text = "WYTWORZONO: %s ×%d" % [RECIPES.DATA[id]["name"], RECIPES.DATA[id]["amount"]] + _status.text = "WYTWORZONO: %s ×%d" % [RECIPES.DATA[id]["name"], RECIPES.output_amount(id, GameState.captain)] _status.add_theme_color_override("font_color", Color("85deb1")) else: _status.text = "BRAKUJE SUROWCÓW — NAJEDŹ NA RECEPTURĘ, ABY SPRAWDZIĆ KOSZT" diff --git a/scripts/ui/ship_statistics.gd b/scripts/ui/ship_statistics.gd index 293b5ca..0dde30a 100644 --- a/scripts/ui/ship_statistics.gd +++ b/scripts/ui/ship_statistics.gd @@ -215,9 +215,13 @@ func _weapons() -> void: UI.label(card, "Brak zamontowanej broni", 13, UI.MUTED) continue _item_header(card,item) - _metric(card, "weapon_dps_%d" % index, "DPS", func(): return "%.1f" % inventory.get_weapon_dps(index), GOLD) + var external := item.definition.weapon_effect == "external_turret" + _metric(card, "weapon_dps_%d" % index, "DPS jednego działka" if external else "DPS", func(): return "%.1f" % inventory.get_weapon_dps(index), GOLD) _metric(card, "weapon_damage_%d" % index, "Obrażenia trafienia", func(): return "%.1f – %.1f" % [item.get_damage_range().x*player.get_weapon_damage_multiplier(), item.get_damage_range().y*player.get_weapon_damage_multiplier()], GOLD, "Zakres przed krytykiem i obroną przeciwnika.") - _metric(card, "weapon_interval_%d" % index, "Odstęp między strzałami", func(): return "%.2f s" % (inventory.get_missile_cooldown(item) if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE else item.get_fire_interval())) + _metric(card, "weapon_interval_%d" % index, "Odstęp strzałów działka" if external else "Odstęp między strzałami", func(): return "%.2f s" % (item.get_turret_fire_interval() if external else (inventory.get_missile_cooldown(item) if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE else item.get_fire_interval()))) + if external: + _metric(card,"turret_deploy_%d" % index,"Odstęp rozstawiania",func(): return "%.2f s" % item.get_fire_interval()) + UI.label(card,"50 amunicji → 1 działko z 50 pociskami",12,UI.MUTED) _metric(card, "weapon_energy_%d" % index, "Energia na strzał", func(): return "%.1f" % player.get_weapon_energy_cost(item.get_energy_cost()), ENERGY) _metric(card, "weapon_range_%d" % index, "Zasięg", func(): return "%.0f px" % item.definition.weapon_range) UI.label(card,"Typ: " + DamageTypes.NAMES[item.get_damage_type()],12,UI.MUTED) diff --git a/tests/abilities/auxiliary_shield_test.gd b/tests/abilities/auxiliary_shield_test.gd new file mode 100644 index 0000000..101633c --- /dev/null +++ b/tests/abilities/auxiliary_shield_test.gd @@ -0,0 +1,107 @@ +extends Node2D + +const DEFINITION := preload("res://resources/items/auxiliary_shield.tres") +const GEN := preload("res://scripts/items/item_generator.gd") +const LAB := preload("res://scripts/planets/laboratory.gd") + +func _ready() -> void: + GameState.start_new_run() + assert(DEFINITION not in GEN.available_definitions()) + var captain := GameState.captain + var progress: Resource = captain.expedition + progress.freighter_rooms.expansion_1 = "laboratory" + progress.laboratory_offers.assign(["auxiliary_shield"]) + for key in LAB.COSTS.auxiliary_shield: captain.minerals[key] = 100 + assert(LAB.start(captain,"auxiliary_shield")) + LAB.advance(progress,60) + assert(DEFINITION in GEN.available_definitions()) + var item := GEN.generate_for_definition(DEFINITION,30) + assert(item.additional_modifiers.is_empty() and item.definition.icon != null) + item.extension_cooldown = 30 + var ship := preload("res://scenes/game/player_ship.tscn").instantiate() + ship.get_node("Camera2D").enabled = false + add_child(ship) + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + var systems := ship.get_node("ExtensionSystems") + systems.set_physics_process(false) + ship.get_node("ShipInventory").extension_slots[0] = item + ship.max_energy = 200.0 + ship.max_shields = 120.0 + ship.shields = 120.0 + ship.hull = 100.0 + ship.absorption = Vector3.ZERO + ship.energy = 139.99 + assert(not ship.activate_ability(0)) + assert(ship.auxiliary_shields == 0.0 and not systems.cooldowns.has("auxiliary_shield")) + ship.energy = 140.0 + assert(ship.activate_ability(0)) + assert(ship.energy == 0.0 and ship.auxiliary_shields == 60.0 and ship.auxiliary_max_shields == 60.0) + assert(ship.shields == 120.0 and ship.hull == 100.0) + var outer: Node2D = ship.get_node("AuxiliaryShieldVisual") + assert(is_equal_approx(outer.shield_radius,ship.get_node("ShieldVisual").shield_radius*1.45)) + assert(ship.get_active_shield_visual() == outer) + ship.take_damage(25.0) + assert(ship.auxiliary_shields == 35.0 and ship.shields == 120.0 and ship.hull == 100.0) + ship.take_damage(50.0) + assert(ship.auxiliary_shields == 0.0 and ship.shields == 105.0 and ship.hull == 100.0) + ship.take_damage(200.0) + assert(ship.shields == 0.0 and ship.hull == 5.0) + ship.energy = 200.0 + assert(not ship.activate_ability(0),"Normal extension cooldown applies") + systems._physics_process(30.0) + assert(ship.activate_ability(0),"Works with empty ordinary shields") + assert(ship.energy == 60.0 and ship.auxiliary_shields == 60.0) + systems._physics_process(30.0) + ship.energy = 200.0 + assert(not ship.activate_ability(0) and ship.energy == 200.0,"Cannot stack or refill an active outer layer") + ship.max_shields = 200.0 + ship._update_shield_visual() + assert(ship.auxiliary_shields == 60.0 and ship.auxiliary_max_shields == 60.0,"Capacity is captured at activation") + ship.clear_auxiliary_shield() + assert(ship.activate_ability(0) and ship.auxiliary_shields == 100.0) + await get_tree().physics_frame + await get_tree().physics_frame + var bolt := preload("res://scenes/game/weapon_projectile.tscn").instantiate() + bolt.enemy_shot = true + bolt.collision_mask = 2 + bolt.configure(Vector2(-150,0),Vector2.RIGHT,ItemDefinition.WeaponKind.PLASMA,1200.0,20.0,1000.0) + add_child(bolt) + bolt.set_physics_process(false) + bolt._physics_process(0.25) + assert(bolt.is_queued_for_deletion()) + assert(is_equal_approx(bolt.position.length(),outer.shield_radius),"Fast bullets stop on the larger perimeter") + assert(ship.auxiliary_shields == 80.0 and ship.shields == 0.0 and ship.hull == 5.0) + var hit: Dictionary = preload("res://scripts/game/shield_intersections.gd").intercept({},Vector2(-200,0),Vector2(200,0),2,[],get_tree()) + assert(hit.collider == ship and is_equal_approx(hit.position.length(),outer.shield_radius),"Lasers hit the outer shield even with normal shields empty") + systems.reset_combat_state() + assert(ship.auxiliary_shields == 0.0) + ship.energy = 200.0 + assert(ship.activate_ability(0)) + captain.skills.power_redirect = 1 + captain.equip_skill(0,"power_redirect") + ship.shields = ship.max_shields + assert(captain.activate_skill("power_redirect",ship)) + assert(ship.auxiliary_shields == 0.0) + systems.cooldowns.clear() + ship.energy = 200.0 + assert(not ship.activate_ability(0)) + captain.reset_combat_state() + ship.is_docked = true + assert(not ship.activate_ability(0)) + ship.is_docked = false + assert(ship.activate_ability(0)) + ship.is_docked = true + ship._physics_process(0.1) + assert(ship.auxiliary_shields == 0.0) + # Normal-shield emergency technologies must not trigger for absorbed hits. + ship.is_docked = false + ship.energy = 200.0 + ship.shields = ship.max_shields + systems.cooldowns.clear() + assert(ship.activate_ability(0)) + progress.alien_skills.shields_b_next_next = true + ship.take_damage(5.0) + assert(ship._salvo_cooldown == 0.0 and ship.auxiliary_shields == 95.0) + print("AUXILIARY_SHIELD_TEST: PASS (lab unlock, 70% max energy cost, 50% max shield snapshot, no stacking, outer-first damage and overflow, larger bullet/laser collision with empty main shields, cooldown, reset, power redirect and docking)") + get_tree().quit() diff --git a/tests/abilities/auxiliary_shield_test.gd.uid b/tests/abilities/auxiliary_shield_test.gd.uid new file mode 100644 index 0000000..ed2e965 --- /dev/null +++ b/tests/abilities/auxiliary_shield_test.gd.uid @@ -0,0 +1 @@ +uid://boarpjs60rq7x diff --git a/tests/abilities/auxiliary_shield_test.tscn b/tests/abilities/auxiliary_shield_test.tscn new file mode 100644 index 0000000..62a14b0 --- /dev/null +++ b/tests/abilities/auxiliary_shield_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/abilities/auxiliary_shield_test.gd" id="1"] +[node name="AuxiliaryShieldTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/captain/new_skills_test.gd b/tests/captain/new_skills_test.gd new file mode 100644 index 0000000..066a096 --- /dev/null +++ b/tests/captain/new_skills_test.gd @@ -0,0 +1,95 @@ +extends Node2D + +const SKILLS := preload("res://scripts/captain/captain_skills.gd") +const LAB := preload("res://scripts/planets/laboratory.gd") +const RECIPES := preload("res://scripts/items/crafting_recipes.gd") +const NEW_SKILLS := ["scientist", "gunsmith", "bounty_hunter", "missile_specialist", "emergency_procedure"] + +func _ready() -> void: + GameState.start_new_run() + var captain := GameState.captain + var ship := RepairShip.new() + # A real inventory without starting the flight simulation. + var inventory := ShipInventory.new() + inventory.weapon_slots.resize(3) + inventory.module_slots.resize(5) + var missile := ItemInstance.new() + missile.definition = preload("res://resources/items/missile_launcher.tres") + var baseline_money := captain.reward_enemy(1,0,1.0,1.0,false).y + assert(SKILLS.IDS.size() == 16) + for id in NEW_SKILLS: + assert(SKILLS.icon(id) != null) + for rank in range(1,4): + assert(not SKILLS.description(id,rank).is_empty()) + captain.learning_points = 1 + captain.skill_offers.assign([id]) + assert(captain.train_skill(id)) + assert(captain.get_skill_rank(id) == 1) + captain.skills[id] = 0 + for rank in range(1,4): + for id in NEW_SKILLS: captain.skills[id] = rank + var progress: Resource = captain.expedition + progress.freighter_rooms.expansion_1 = "laboratory" + progress.laboratory_project = "" + progress.laboratory_offers.assign(["machine_gun"]) + progress.laboratory_unlocks.clear() + for key in LAB.COSTS.machine_gun: captain.minerals[key] = 100 + assert(LAB.start(captain,"machine_gun")) + assert(is_equal_approx(progress.laboratory_remaining,[54.0,48.0,42.0][rank-1])) + assert(progress.laboratory_duration == progress.laboratory_remaining) + LAB.advance(progress,progress.laboratory_remaining-1.0) + assert(not progress.laboratory_unlocks.has("machine_gun")) + LAB.advance(progress,1.0) + assert(progress.laboratory_unlocks.has("machine_gun")) + for id in ["ammunition","irradiated_ammunition"]: + for key in RECIPES.DATA[id].cost: captain.minerals[key] = 100 + var before := inventory.get_crafted_stock(id) + var iron_before: int = captain.minerals.iron + assert(inventory.craft(id)) + assert(inventory.get_crafted_stock(id)-before == RECIPES.output_amount(id,captain)) + assert(captain.minerals.iron == iron_before-int(RECIPES.DATA[id].cost.get("iron",0))) + assert(RECIPES.output_amount("ammunition",captain) == [11,12,13][rank-1]) + assert(RECIPES.output_amount("irradiated_ammunition",captain) == [55,60,65][rank-1]) + assert(RECIPES.output_amount("missile",captain) == 1) + assert(RECIPES.output_amount("parts",captain) == 1) + var credits_before := captain.credits + var reward := captain.reward_enemy(1,0,1.0,1.0,false) + assert(reward.y == roundi(baseline_money*(1.0+SKILLS.value("bounty_hunter",rank)))) + assert(captain.credits == credits_before,"Enemy credits are collected from pickups") + assert(is_equal_approx(inventory.get_missile_cooldown(missile),[4.5,4.25,4.0][rank-1])) + assert(captain.equip_skill(0,"emergency_procedure")) + captain.reset_combat_state() + ship.max_hull = 200.0 + ship.hull = 100.0 + captain.parts = 0.5 + assert(not captain.activate_skill("emergency_procedure",ship)) + assert(not captain.skill_cooldowns.has("emergency_procedure")) + captain.parts = 2.0 + ship.is_docked = true + assert(not captain.activate_skill("emergency_procedure",ship)) + ship.is_docked = false + # Feedback requires a parent; use a minimal stub for this logic test. + var stub := RepairShip.new() + assert(captain.activate_skill("emergency_procedure",stub)) + assert(is_equal_approx(stub.hull,100.0+[10.0,20.0,30.0][rank-1])) + assert(captain.parts == 1.0 and captain.skill_cooldowns.emergency_procedure == 60.0) + assert(not captain.activate_skill("emergency_procedure",stub)) + captain.tick_skills(60.0) + stub.hull = 199.0 + assert(captain.activate_skill("emergency_procedure",stub)) + assert(stub.hull == 200.0 and captain.parts == 0.0) + captain.reset_combat_state() + captain.parts = 1.0 + assert(not captain.activate_skill("emergency_procedure",stub)) + assert(captain.parts == 1.0 and not captain.skill_cooldowns.has("emergency_procedure")) + stub.free() + ship.free() + inventory.free() + print("NEW_CAPTAIN_SKILLS_TEST: PASS (16 skills, icons, training, all ranks, research duration, ammo output/cost, bounty, missile cooldown, repair conditions/cost/cooldown/cap)") + get_tree().quit() + +class RepairShip extends CharacterBody2D: + var hull := 100.0 + var max_hull := 200.0 + var _is_destroyed := false + var is_docked := false diff --git a/tests/captain/new_skills_test.gd.uid b/tests/captain/new_skills_test.gd.uid new file mode 100644 index 0000000..a91329b --- /dev/null +++ b/tests/captain/new_skills_test.gd.uid @@ -0,0 +1 @@ +uid://b0l117x36bluj diff --git a/tests/captain/new_skills_test.tscn b/tests/captain/new_skills_test.tscn new file mode 100644 index 0000000..819229c --- /dev/null +++ b/tests/captain/new_skills_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/captain/new_skills_test.gd" id="1"] +[node name="NewSkillsTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/captain/salvage_drone_skills_test.gd b/tests/captain/salvage_drone_skills_test.gd new file mode 100644 index 0000000..beea277 --- /dev/null +++ b/tests/captain/salvage_drone_skills_test.gd @@ -0,0 +1,81 @@ +extends Node2D + +const SKILLS := preload("res://scripts/captain/captain_skills.gd") +const POI := preload("res://scripts/levels/exploration_poi.gd") +const PROJECTILE := preload("res://scripts/game/weapon_projectile.gd") + +func _ready() -> void: + GameState.start_new_run() + var captain := GameState.captain + assert(SKILLS.IDS.size() == 16) + for id in ["scrap_collector","drone_commander"]: + assert(SKILLS.icon(id) != null) + captain.learning_points = 1 + captain.skill_offers.assign([id]) + assert(captain.train_skill(id)) + assert(not captain.equip_skill(0,id),"Both new skills are passive") + for rank in range(4): + captain.skills.scrap_collector = rank + captain.parts = 0.0 + for roll in 1000: + captain.roll_wreck_spare_part((roll+0.5)/1000.0) + assert(captain.parts == [0,150,250,400][rank],"Exact probability at each rank") + var threshold: float = [0.0,0.15,0.25,0.40][rank] + var before := captain.parts + assert(not captain.roll_wreck_spare_part(threshold)) + assert(captain.parts == before) + var ship := preload("res://scenes/game/player_ship.tscn").instantiate() + add_child(ship) + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + var inventory: ShipInventory = ship.get_node("ShipInventory") + var systems: Node = ship.get_node("ExtensionSystems") + systems.set_physics_process(false) + for rank in range(4): + captain.skills.scrap_collector = rank + var successes := 0 + for value in range(1,61): + var wreck := StaticBody2D.new() + wreck.set_script(POI) + wreck.kind = "wreck" + wreck.player = ship + wreck.reward_seed = value + wreck.poi_id = "salvage:%d:%d" % [rank,value] + add_child(wreck) + wreck.set_physics_process(false) + var before := captain.parts + assert(wreck.claim()) + var gained := captain.parts-before + assert(gained == 0.0 or gained == 1.0) + assert(not wreck.claim(),"Cannot roll or grant parts twice") + assert(captain.parts == before+gained) + successes += int(gained) + assert(successes == 0 if rank == 0 else successes > 0 and successes < 60) + await get_tree().process_frame + var target := Node2D.new() + target.position = Vector2(300,0) + add_child(target) + var baseline_dps := inventory.get_total_dps() + for rank in range(4): + captain.skills.drone_commander = rank + inventory.ammunition = 100 + systems.activate(5) + assert(inventory.ammunition == 50) + var drones := get_tree().get_nodes_in_group("player_combat_drones") + assert(drones.size() == 3) + for drone in drones: + drone.set_physics_process(false) + drone._fire(target) + var count := 0 + for child in get_children(): + if child.get_script() == PROJECTILE: + assert(is_equal_approx(child.damage,baseline_dps*0.5/3.0*0.25*[1.0,1.1,1.2,1.3][rank])) + count += 1 + child.free() + assert(count == 1) + assert(inventory.ammunition == 50,"No extra cost per drone shot") + assert(is_equal_approx(inventory.get_total_dps(),baseline_dps),"Ship weapons do not gain drone damage") + systems.reset_combat_state() + await get_tree().process_frame + print("SALVAGE_DRONE_SKILLS_TEST: PASS (16 skills, passive training, 0/15/25/40% probabilities, 240 wrecks, no double claims, three drones, actual projectile damage at all ranks, unchanged ammo cost and ship DPS)") + get_tree().quit() diff --git a/tests/captain/salvage_drone_skills_test.gd.uid b/tests/captain/salvage_drone_skills_test.gd.uid new file mode 100644 index 0000000..665e473 --- /dev/null +++ b/tests/captain/salvage_drone_skills_test.gd.uid @@ -0,0 +1 @@ +uid://dyiv2uihif830 diff --git a/tests/captain/salvage_drone_skills_test.tscn b/tests/captain/salvage_drone_skills_test.tscn new file mode 100644 index 0000000..624efa8 --- /dev/null +++ b/tests/captain/salvage_drone_skills_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/captain/salvage_drone_skills_test.gd" id="1"] +[node name="SalvageDroneSkillsTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/external_turret_test.gd b/tests/items/external_turret_test.gd new file mode 100644 index 0000000..c583c72 --- /dev/null +++ b/tests/items/external_turret_test.gd @@ -0,0 +1,119 @@ +extends Node2D + +const DEFINITION := preload("res://resources/items/external_turret.tres") +const MACHINE_GUN := preload("res://resources/items/machine_gun.tres") +const GENERATOR := preload("res://scripts/items/item_generator.gd") +const LAB := preload("res://scripts/planets/laboratory.gd") +const PROJECTILE := preload("res://scripts/game/weapon_projectile.gd") + +func _ready() -> void: + GameState.start_new_run() + assert(DEFINITION not in GENERATOR.available_definitions()) + var progress: Resource = GameState.captain.expedition + progress.freighter_rooms.expansion_1 = "laboratory" + progress.laboratory_offers.assign(["external_turret"]) + for key in LAB.COSTS.external_turret: GameState.captain.minerals[key] = 100 + assert(LAB.start(GameState.captain,"external_turret")) + LAB.advance(progress,60.0) + assert(DEFINITION in GENERATOR.available_definitions()) + for level in [1,5,30,120,1000]: + var turret_item := ItemInstance.new() + turret_item.definition = DEFINITION + var machine_item := ItemInstance.new() + machine_item.definition = MACHINE_GUN + seed(327+level) + GENERATOR.roll_base_values(turret_item,level) + seed(327+level) + GENERATOR.roll_base_values(machine_item,level) + assert(turret_item.get_damage_range().is_equal_approx(machine_item.get_damage_range()*0.5)) + for value in 20: + seed(value) + var turret_damage := turret_item.roll_damage() + seed(value) + assert(is_equal_approx(turret_damage,machine_item.roll_damage()*0.5)) + var ship := preload("res://scenes/game/player_ship.tscn").instantiate() + add_child(ship) + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + ship.get_node("ExtensionSystems").set_physics_process(false) + var controller: Node = ship.get_node("WeaponController") + var inventory: ShipInventory = ship.get_node("ShipInventory") + var item := ItemInstance.new() + item.definition = DEFINITION + item.damage_min = 8.0 + item.damage_max = 12.0 + inventory.weapon_slots[0] = item + GameState.captain.skills.efficient_magazine = 3 + GameState.captain.skills.drone_commander = 3 + inventory.ammunition = 49 + assert(not controller.try_deploy_turret(0)) + assert(inventory.ammunition == 49 and get_tree().get_nodes_in_group("player_external_turrets").is_empty()) + inventory.ammunition = 100 + assert(controller.try_deploy_turret(0)) + assert(inventory.ammunition == 50) + assert(not controller.try_deploy_turret(0),"Deployment cooldown prevents repeated clicks") + var turret: Node2D = get_tree().get_nodes_in_group("player_external_turrets")[0] + turret.set_physics_process(false) + assert(turret.ammunition == 50) + var anchor := turret.global_position + ship.position += Vector2(1000,1000) + turret._physics_process(0.1) + assert(turret.global_position == anchor and turret.ammunition == 50,"Stays in space and holds ammo without targets") + ship.position = Vector2.ZERO + var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() + enemy.position = anchor+Vector2(250,0) + add_child(enemy) + enemy.set_physics_process(false) + enemy.hull = 10000.0 + enemy.max_hull = 10000.0 + enemy.shields = 0.0 + enemy.absorption = Vector3.ZERO + enemy._player = ship + await get_tree().physics_frame + await get_tree().process_frame + assert(turret._find_target() == enemy) + var base_ammo := inventory.ammunition + # The turret retains its own item even if the weapon is modified or unequipped. + item.damage_min = 100.0 + item.damage_max = 100.0 + inventory.weapon_slots[0] = null + var shots := 0 + for index in 50: + turret._fire(enemy) + for child in get_children(): + if child.get_script() == PROJECTILE: + shots += 1 + assert(child.damage >= 4.0 and child.damage <= 6.0) + assert(child.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN) + assert(child.source_ship == ship) + if index == 0: + var hull_before: float = enemy.hull + child._hit_target(enemy) + assert(enemy.hull < hull_before,"Turret bullets really damage enemies") + child.free() + assert(shots == 50 and turret.ammunition == 0) + assert(turret.is_queued_for_deletion()) + assert(inventory.ammunition == base_ammo,"Only prepaid ammunition is used") + turret._fire(enemy) + assert(turret.ammunition == 0,"Never shoots a 51st bullet") + await get_tree().process_frame + inventory.weapon_slots[0] = item + controller._shot_timers[0] = 0.0 + ship.is_docked = true + assert(not controller.try_deploy_turret(0) and inventory.ammunition == base_ammo) + ship.is_docked = false + controller._fire_buttons_down[0] = true + controller._fire_just_pressed[0] = true + controller._physics_process(0.01) + assert(inventory.ammunition == 0) + var second: Node2D = get_tree().get_nodes_in_group("player_external_turrets")[0] + second.set_physics_process(false) + inventory.ammunition = 50 + controller._physics_process(2.0) + assert(inventory.ammunition == 50 and get_tree().get_nodes_in_group("player_external_turrets").size() == 1,"Holding the mouse does not deploy more turrets") + ship.is_docked = true + second._physics_process(0.1) + assert(second.is_queued_for_deletion(),"Docking clears deployed turrets") + await get_tree().create_timer(0.5).timeout + print("EXTERNAL_TURRET_TEST: PASS (laboratory unlock, exact half machine-gun damage at five levels, 50 prepaid ammo despite ammo skill, placement/cooldown/docking, fixed position, target acquisition, 50 real bullets and damage, snapshot, exhaustion cleanup)") + get_tree().quit() diff --git a/tests/items/external_turret_test.gd.uid b/tests/items/external_turret_test.gd.uid new file mode 100644 index 0000000..2202eb5 --- /dev/null +++ b/tests/items/external_turret_test.gd.uid @@ -0,0 +1 @@ +uid://dc154wsfpq5rp diff --git a/tests/items/external_turret_test.tscn b/tests/items/external_turret_test.tscn new file mode 100644 index 0000000..9363e66 --- /dev/null +++ b/tests/items/external_turret_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/external_turret_test.gd" id="1"] +[node name="ExternalTurretTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/pulse_laser_test.gd b/tests/items/pulse_laser_test.gd new file mode 100644 index 0000000..f419b6b --- /dev/null +++ b/tests/items/pulse_laser_test.gd @@ -0,0 +1,110 @@ +extends Node2D + +const PULSE := preload("res://resources/items/pulse_laser.tres") +const BEAM := preload("res://resources/items/beam_laser.tres") +const GEN := preload("res://scripts/items/item_generator.gd") +const LAB := preload("res://scripts/planets/laboratory.gd") + +class StraightLaser extends "res://scripts/game/weapon_controller.gd": + func _aim_direction_from(_origin: Vector2) -> Vector2: return Vector2.RIGHT + +class ForcedPulse extends ItemInstance: + var bypass := false + func roll_shield_piercing(_roll: float = -1.0) -> float: + return 1.0 if bypass else shield_piercing() + +func _ready() -> void: + GameState.start_new_run() + assert(PULSE not in GEN.available_definitions()) + var progress: Resource = GameState.captain.expedition + progress.freighter_rooms.expansion_1 = "laboratory" + progress.laboratory_offers.assign(["pulse_laser"]) + for key in LAB.COSTS.pulse_laser: GameState.captain.minerals[key] = 100 + assert(LAB.start(GameState.captain,"pulse_laser")) + LAB.advance(progress,60) + assert(PULSE in GEN.available_definitions()) + var pulse := ItemInstance.new() + pulse.definition = PULSE + var beam := ItemInstance.new() + beam.definition = BEAM + var successes := 0 + for value in 1000: successes += int(pulse.roll_shield_piercing((value+0.5)/1000.0) == 1.0) + assert(successes == 500) + assert(pulse.roll_shield_piercing(0.5) == 0.0) + assert(beam.roll_shield_piercing(0.0) == 0.0) + pulse.special_orbs.assign(["piercing"]) + assert(pulse.roll_shield_piercing(0.49) == 1.0 and pulse.roll_shield_piercing(0.5) == 0.5) + pulse.special_orbs.clear() + for level in [1,5,30,120,1000]: + seed(457+level) + GEN.roll_base_values(pulse,level) + seed(457+level) + GEN.roll_base_values(beam,level) + assert(pulse.get_damage_range().is_equal_approx(beam.get_damage_range()*0.7)) + assert(is_equal_approx(pulse.get_energy_cost(),beam.get_energy_cost()*1.3)) + for value in 20: + seed(value) + var damage := pulse.roll_damage() + seed(value) + assert(is_equal_approx(damage,beam.roll_damage()*0.7)) + var ship := preload("res://scenes/game/player_ship.tscn").instantiate() + add_child(ship) + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + ship.get_node("ExtensionSystems").set_physics_process(false) + var controller := StraightLaser.new() + ship.add_child(controller) + controller.set_physics_process(false) + var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() + enemy.position = Vector2(240,-28) + add_child(enemy) + enemy.set_physics_process(false) + enemy._player = ship + enemy.max_hull = 10000.0 + enemy.max_shields = 1000.0 + enemy.absorption = Vector3.ZERO + var item := ForcedPulse.new() + item.definition = PULSE + item.damage_min = 10.0 + item.damage_max = 10.0 + await get_tree().physics_frame + await get_tree().process_frame + for bypass in [false,true]: + enemy.hull = 10000.0 + enemy.shields = 1000.0 + item.bypass = bypass + controller._shot_timers[0] = 0.0 + ship.energy = 1000.0 + controller._fire_laser(0,item) + assert(is_equal_approx(ship.energy,1000.0-item.get_energy_cost())) + assert(enemy.hull == 10000.0 and enemy.shields == 1000.0,"A pulse does not hit instantly") + var bolts := get_children().filter(func(child): return child.get_script() == preload("res://scripts/game/weapon_projectile.gd")) + assert(bolts.size() == 1) + var bolt: Area2D = bolts[0] + bolt.set_physics_process(false) + assert(bolt.weapon_effect == "pulse_laser" and bolt.projectile_speed == 1500.0) + assert(bolt.shield_piercing == (1.0 if bypass else 0.0)) + var origin := bolt.global_position + bolt._physics_process(0.04) + assert(is_equal_approx(bolt.global_position.distance_to(origin),60.0)) + assert(enemy.hull == 10000.0 and enemy.shields == 1000.0,"Damage waits for arrival") + bolt._physics_process(0.2) + assert(is_equal_approx(enemy.hull,9993.0 if bypass else 10000.0)) + assert(is_equal_approx(enemy.shields,1000.0 if bypass else 993.0)) + assert(not controller._laser_lines[0].visible and controller._laser_pulse_timers[0] == 0.0) + bolt.free() + # Full bypass also stays full with the alien double-hit technology. + progress.alien_skills.weapons_a_next_next_next = true + enemy.hull = 10000.0 + enemy.shields = 1000.0 + controller._apply_damage(enemy,7.0,false,1.0,DamageTypes.Type.ENERGY) + assert(enemy.shields == 1000.0 and enemy.hull == 9993.0) + progress.alien_skills.clear() + controller._shot_timers[0] = 0.0 + ship.energy = item.get_energy_cost()-0.01 + var before: float = enemy.hull + controller._laser_lines[0].visible = false + controller._fire_laser(0,item) + assert(enemy.hull == before and not controller._laser_lines[0].visible) + print("PULSE_LASER_TEST: PASS (laboratory unlock, 50% full bypass and orb compatibility, -30% damage/+30% energy at five levels, moving 1500px/s red projectiles, no instant damage, shields/hull routing on impact, technology compatibility, insufficient energy)") + get_tree().quit() diff --git a/tests/items/pulse_laser_test.gd.uid b/tests/items/pulse_laser_test.gd.uid new file mode 100644 index 0000000..a19414e --- /dev/null +++ b/tests/items/pulse_laser_test.gd.uid @@ -0,0 +1 @@ +uid://dtugyxs5su4lg diff --git a/tests/items/pulse_laser_test.tscn b/tests/items/pulse_laser_test.tscn new file mode 100644 index 0000000..7070339 --- /dev/null +++ b/tests/items/pulse_laser_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/pulse_laser_test.gd" id="1"] +[node name="PulseLaserTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/items/weapon_icon_transparency_test.gd b/tests/items/weapon_icon_transparency_test.gd new file mode 100644 index 0000000..a7420e0 --- /dev/null +++ b/tests/items/weapon_icon_transparency_test.gd @@ -0,0 +1,27 @@ +extends Node + +func _ready() -> void: + var generator := preload("res://scripts/items/item_generator.gd") + var lab := preload("res://scripts/planets/laboratory.gd") + var definitions: Array = generator.DEFINITIONS.duplicate() + for id in lab.IDS: + var definition := lab.definition(id) + if definition not in definitions: definitions.append(definition) + var count := 0 + for definition in definitions: + if definition.category != ItemDefinition.Category.WEAPON: continue + var image: Image = definition.icon.get_image() + assert(image != null and not image.is_empty()) + var transparent := 0 + var visible := 0 + for y in range(0,image.get_height(),8): + for x in range(0,image.get_width(),8): + var color := image.get_pixel(x,y) + if color.a < 0.01: transparent += 1 + if color.a > 0.5: visible += 1 + assert(transparent > 100,"Weapon icon background must be transparent: "+definition.item_id) + assert(visible > 100,"Weapon artwork must remain visible: "+definition.item_id) + count += 1 + assert(count == 11) + print("WEAPON_ICON_TRANSPARENCY_TEST: PASS (11 weapon icons with transparent backgrounds and visible artwork)") + get_tree().quit() diff --git a/tests/items/weapon_icon_transparency_test.gd.uid b/tests/items/weapon_icon_transparency_test.gd.uid new file mode 100644 index 0000000..b6be3f7 --- /dev/null +++ b/tests/items/weapon_icon_transparency_test.gd.uid @@ -0,0 +1 @@ +uid://og5o1yu4xwxb diff --git a/tests/items/weapon_icon_transparency_test.tscn b/tests/items/weapon_icon_transparency_test.tscn new file mode 100644 index 0000000..10e0c96 --- /dev/null +++ b/tests/items/weapon_icon_transparency_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/weapon_icon_transparency_test.gd" id="1"] +[node name="WeaponIconTransparencyTest" type="Node"] +script = ExtResource("1") diff --git a/tests/previews/auxiliary_shield_live_demo.gd b/tests/previews/auxiliary_shield_live_demo.gd new file mode 100644 index 0000000..bf0e20c --- /dev/null +++ b/tests/previews/auxiliary_shield_live_demo.gd @@ -0,0 +1,96 @@ +extends Node2D + +var _ship: CharacterBody2D +var _game: Node2D +var _status: Label +var _time := 0.0 +var _activated := false +var _shots := 0 +var _next_shot := 4.0 +var _paused := false + +func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS + GameState.start_new_run() + _game = preload("res://scenes/game/game.tscn").instantiate() + _game.process_mode = Node.PROCESS_MODE_PAUSABLE + add_child(_game) + _game.set_physics_process(false) + _ship = _game.player + _ship.set_physics_process(false) + _ship.get_node("WeaponController").set_physics_process(false) + _ship.get_node("ExtensionSystems").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.queue_free() + _ship.position = Vector2(0,1500) + _ship.rotation = 0.0 + _ship.get_node("Camera2D").reset_smoothing() + _ship.max_energy = 100.0 + _ship.max_shields = 100.0 + _ship.max_hull = 100.0 + _ship.absorption = Vector3.ZERO + var item := ItemInstance.new() + item.definition = preload("res://resources/items/auxiliary_shield.tres") + _ship.get_node("ShipInventory").extension_slots[0] = item + _ship.get_node("ShipInventory").inventory_changed.emit() + var overlay := CanvasLayer.new() + overlay.layer = 100 + add_child(overlay) + var panel := PanelContainer.new() + panel.position = Vector2(320,80) + var style := StyleBoxFlat.new() + style.bg_color = Color("100a21ed") + style.border_color = Color("ba8cff") + style.set_border_width_all(1) + style.set_content_margin_all(14) + panel.add_theme_stylebox_override("panel",style) + overlay.add_child(panel) + _status = Label.new() + _status.add_theme_font_size_override("font_size",16) + panel.add_child(_status) + _reset_cycle() + print("AUXILIARY_SHIELD_LIVE_DEMO: running — activation, 8 incoming shots, repeating cycle; SPACE pause, ESC close") + +func _reset_cycle() -> void: + for bullet in get_tree().get_nodes_in_group("auxiliary_demo_bullets"): bullet.queue_free() + _time = 0.0 + _shots = 0 + _next_shot = 4.0 + _activated = false + _ship.get_node("ExtensionSystems").reset_combat_state() + _ship.energy = 100.0 + _ship.shields = 100.0 + _ship.hull = 100.0 + _ship._update_shield_visual() + _update_status() + +func _physics_process(delta: float) -> void: + if _paused or not is_instance_valid(_ship): return + _time += delta + if _time >= 2.0 and not _activated: + _activated = _ship.activate_ability(0) + if _activated and _time >= _next_shot and _shots < 8: + var bullet := preload("res://scenes/game/weapon_projectile.tscn").instantiate() + bullet.enemy_shot = true + bullet.collision_mask = 2 + var origin := _ship.global_position+Vector2(400,0) + bullet.configure(origin,Vector2.LEFT,ItemDefinition.WeaponKind.PLASMA,650.0,10.0,800.0) + bullet.add_to_group("auxiliary_demo_bullets") + _game.add_child(bullet) + _shots += 1 + _next_shot += 0.9 + if _time >= 14.0: _reset_cycle() + _update_status() + +func _update_status() -> void: + var phase := "ZA CHWILĘ AKTYWACJA" if not _activated else ("OSTRZAŁ ZEWNĘTRZNEJ OSŁONY" if _ship.auxiliary_shields > 0.0 else "OSŁONA ZUŻYTA · OBRYWAJĄ ZWYKŁE TARCZE") + _status.text = "TARCZA ZEWNĘTRZNA · %s\n%s\nEnergia: %.0f / 100 · Osłona: %.0f / 50 · Tarcze: %.0f / 100\nOstrzał: 10 obrażeń / pocisk · pokaz powtarza się\nSPACJA: pauza/wznów · ESC: zamknij pokaz" % ["PAUZA" if _paused else "POKAZ",phase,_ship.energy,_ship.auxiliary_shields,_ship.shields] + +func _unhandled_key_input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and not event.echo: + if event.keycode == KEY_SPACE: + _paused = not _paused + get_tree().paused = _paused + _update_status() + get_viewport().set_input_as_handled() + elif event.keycode == KEY_ESCAPE: + get_tree().quit() diff --git a/tests/previews/auxiliary_shield_live_demo.tscn b/tests/previews/auxiliary_shield_live_demo.tscn new file mode 100644 index 0000000..8190bae --- /dev/null +++ b/tests/previews/auxiliary_shield_live_demo.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/auxiliary_shield_live_demo.gd" id="1"] +[node name="AuxiliaryShieldLiveDemo" type="Node2D"] +script = ExtResource("1") diff --git a/tests/previews/auxiliary_shield_preview.gd b/tests/previews/auxiliary_shield_preview.gd new file mode 100644 index 0000000..a048685 --- /dev/null +++ b/tests/previews/auxiliary_shield_preview.gd @@ -0,0 +1,56 @@ +extends Node2D + +func _ready() -> void: + GameState.start_new_run() + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + var ship: CharacterBody2D = game.player + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + ship.get_node("ExtensionSystems").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.queue_free() + ship.position = Vector2(0,1500) + ship.rotation = 0.0 + ship.get_node("Camera2D").reset_smoothing() + ship.max_energy = 100.0 + ship.energy = 100.0 + ship.max_shields = 100.0 + ship.shields = 100.0 + var item := ItemInstance.new() + item.definition = preload("res://resources/items/auxiliary_shield.tres") + var inventory: ShipInventory = ship.get_node("ShipInventory") + inventory.extension_slots[0] = item + inventory.inventory_changed.emit() + assert(ship.activate_ability(0)) + var layer := CanvasLayer.new() + layer.layer = 100 + add_child(layer) + var panel := PanelContainer.new() + panel.position = Vector2(330,80) + var style := StyleBoxFlat.new() + style.bg_color = Color("100a21ec") + style.border_color = Color("ba8cff") + style.set_border_width_all(1) + style.set_corner_radius_all(8) + style.set_content_margin_all(14) + panel.add_theme_stylebox_override("panel",style) + layer.add_child(panel) + var row := HBoxContainer.new() + row.add_theme_constant_override("separation",14) + panel.add_child(row) + var icon := TextureRect.new() + icon.texture = item.definition.icon + icon.custom_minimum_size = Vector2(80,80) + icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + row.add_child(icon) + var text := Label.new() + text.text = "TARCZA ZEWNĘTRZNA\n70% maks. energii → osłona 50% maks. tarcz\nOsobna większa warstwa · pochłania obrażenia jako pierwsza" + text.add_theme_font_size_override("font_size",15) + row.add_child(text) + for frame in 15: await get_tree().process_frame + await RenderingServer.frame_post_draw + assert(get_viewport().get_texture().get_image().save_png("res://artifacts/auxiliary_shield_preview.png") == OK) + print("AUXILIARY_SHIELD_PREVIEW: PASS") + get_tree().quit() diff --git a/tests/previews/auxiliary_shield_preview.tscn b/tests/previews/auxiliary_shield_preview.tscn new file mode 100644 index 0000000..5872cd9 --- /dev/null +++ b/tests/previews/auxiliary_shield_preview.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/auxiliary_shield_preview.gd" id="1"] +[node name="AuxiliaryShieldPreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/previews/external_turret_live_demo.gd b/tests/previews/external_turret_live_demo.gd new file mode 100644 index 0000000..d24498b --- /dev/null +++ b/tests/previews/external_turret_live_demo.gd @@ -0,0 +1,95 @@ +extends Node2D + +class DeploymentController extends "res://scripts/game/weapon_controller.gd": + var launch_direction := Vector2.RIGHT + func _aim_direction_from(_origin: Vector2) -> Vector2: return launch_direction + +var _ship: CharacterBody2D +var _inventory: ShipInventory +var _controller: Node +var _status: Label +var _automatic := true +var _next_deployment := 1.5 +var _deployed := 0 + +func _ready() -> void: + GameState.start_new_run() + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + _ship = game.player + _ship.set_physics_process(false) + _ship.get_node("WeaponController").set_physics_process(false) + _ship.get_node("ExtensionSystems").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.queue_free() + _ship.position = Vector2(0,1500) + _ship.rotation = PI*0.5 + _ship.get_node("Camera2D").reset_smoothing() + _inventory = _ship.get_node("ShipInventory") + var item := ItemInstance.new() + item.definition = preload("res://resources/items/external_turret.tres") + _inventory.weapon_slots[0] = item + _inventory.ammunition = 150 + _inventory.inventory_changed.emit() + _controller = DeploymentController.new() + _ship.add_child(_controller) + _controller.set_physics_process(false) + var target := preload("res://scenes/game/enemy_ship.tscn").instantiate() + target.position = _ship.position+Vector2(430,0) + game.add_child(target) + target.set_physics_process(false) + target.max_hull = 1000000.0 + target.hull = target.max_hull + target.max_shields = 0.0 + target.shields = 0.0 + target._player = _ship + target.get_node("Sprite2D").texture = preload("res://assets/Ships/Spaceship_0.png") + target.get_node("Sprite2D").scale = Vector2.ONE*0.18 + target.get_node("Sprite2D").rotation = -PI*0.5 + var overlay := CanvasLayer.new() + overlay.layer = 100 + add_child(overlay) + var panel := PanelContainer.new() + panel.position = Vector2(350,90) + var style := StyleBoxFlat.new() + style.bg_color = Color("07131eec") + style.border_color = Color("70dfff") + style.set_border_width_all(1) + style.set_content_margin_all(14) + panel.add_theme_stylebox_override("panel",style) + overlay.add_child(panel) + _status = Label.new() + _status.add_theme_font_size_override("font_size",16) + panel.add_child(_status) + _update_status() + print("EXTERNAL_TURRET_LIVE_DEMO: running — SPACE toggles deployment, ESC closes demo") + +func _physics_process(delta: float) -> void: + if not is_instance_valid(_controller): return + _controller._shot_timers[0] = maxf(0.0,_controller._shot_timers[0]-delta) + _next_deployment = maxf(0.0,_next_deployment-delta) + if not _automatic or _next_deployment > 0.0: return + if _deployed < 3: + _controller.launch_direction = Vector2.RIGHT.rotated([-1.2,0.0,1.2][_deployed]) + if _controller.try_deploy_turret(0): + _deployed += 1 + _next_deployment = 1.2 + _update_status() + elif get_tree().get_nodes_in_group("player_external_turrets").is_empty(): + _deployed = 0 + _inventory.ammunition = 150 + _inventory.ammunition_changed.emit() + _next_deployment = 2.0 + _update_status() + +func _unhandled_key_input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and not event.echo: + if event.keycode == KEY_SPACE: + _automatic = not _automatic + _update_status() + get_viewport().set_input_as_handled() + elif event.keycode == KEY_ESCAPE: + get_tree().quit() + +func _update_status() -> void: + _status.text = "DZIAŁKA ZEWNĘTRZNE · %s\nWypuszczone: %d / 3 · 50 amunicji na działko\nKażde oddaje 50 strzałów i znika · pokaz powtarza się\nSPACJA: włącz/wyłącz wypuszczanie · ESC: zamknij pokaz" % ["POKAZ" if _automatic else "WSTRZYMANE",_deployed] diff --git a/tests/previews/external_turret_live_demo.gd.uid b/tests/previews/external_turret_live_demo.gd.uid new file mode 100644 index 0000000..3ca2dfe --- /dev/null +++ b/tests/previews/external_turret_live_demo.gd.uid @@ -0,0 +1 @@ +uid://il3v3vkm078y diff --git a/tests/previews/external_turret_live_demo.tscn b/tests/previews/external_turret_live_demo.tscn new file mode 100644 index 0000000..e19d50f --- /dev/null +++ b/tests/previews/external_turret_live_demo.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/external_turret_live_demo.gd" id="1"] +[node name="ExternalTurretLiveDemo" type="Node2D"] +script = ExtResource("1") diff --git a/tests/previews/external_turret_preview.gd b/tests/previews/external_turret_preview.gd new file mode 100644 index 0000000..cfb86bb --- /dev/null +++ b/tests/previews/external_turret_preview.gd @@ -0,0 +1,84 @@ +extends Node2D + +const DEFINITION := preload("res://resources/items/external_turret.tres") + +func _ready() -> void: + GameState.start_new_run() + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + var ship: CharacterBody2D = game.player + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + ship.get_node("ExtensionSystems").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.queue_free() + ship.position = Vector2(0,1500) + ship.rotation = 0.0 + ship.get_node("Camera2D").reset_smoothing() + var inventory: ShipInventory = ship.get_node("ShipInventory") + var item := ItemInstance.new() + item.definition = DEFINITION + inventory.weapon_slots[0] = item + inventory.ammunition = 300 + inventory.inventory_changed.emit() + var targets: Array[Node2D] = [] + for point in [Vector2(-260,-160),Vector2(260,-160)]: + var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() + enemy.position = ship.position+point + game.add_child(enemy) + enemy.set_physics_process(false) + enemy._player = ship + enemy.max_hull = 10000.0 + enemy.hull = 10000.0 + targets.append(enemy) + for point in [Vector2(-120,-70),Vector2(0,-120),Vector2(120,-70)]: + var turret := Node2D.new() + turret.set_script(preload("res://scripts/game/deployed_turret.gd")) + turret.mother = ship + turret.weapon = item + turret.position = ship.position+point + game.add_child(turret) + turret.set_physics_process(false) + turret._age = 1.0 + turret._fire_timer = 0.0 + turret._physics_process(0.1) + var layer := CanvasLayer.new() + layer.layer = 100 + add_child(layer) + var panel := PanelContainer.new() + panel.position = Vector2(340,80) + var style := StyleBoxFlat.new() + style.bg_color = Color("07121ee8") + style.border_color = Color("619ab9") + style.set_border_width_all(1) + style.set_corner_radius_all(8) + style.set_content_margin_all(14) + panel.add_theme_stylebox_override("panel",style) + layer.add_child(panel) + var row := HBoxContainer.new() + row.add_theme_constant_override("separation",14) + panel.add_child(row) + var icon := TextureRect.new() + icon.texture = DEFINITION.icon + icon.custom_minimum_size = Vector2(72,72) + icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + row.add_child(icon) + var text := Label.new() + text.text = "DZIAŁKA ZEWNĘTRZNE\n50 amunicji → 1 działko • 50 pocisków\nPołowa obrażeń działka maszynowego • Zasięg 550" + text.add_theme_font_size_override("font_size",15) + row.add_child(text) + for frame in 12: await get_tree().process_frame + for turret in get_tree().get_nodes_in_group("player_external_turrets"): + turret.rotation = (targets[0].position-turret.position).angle()+PI*0.5 + turret._fire(targets[0]) + for child in game.get_children(): + if child.get_script() == preload("res://scripts/game/weapon_projectile.gd"): + child.set_physics_process(false) + child.position += child.direction*55.0 + turret.queue_redraw() + await get_tree().process_frame + await RenderingServer.frame_post_draw + assert(get_viewport().get_texture().get_image().save_png("res://artifacts/external_turret_preview.png") == OK) + print("EXTERNAL_TURRET_PREVIEW: PASS") + get_tree().quit() diff --git a/tests/previews/external_turret_preview.gd.uid b/tests/previews/external_turret_preview.gd.uid new file mode 100644 index 0000000..88e4ed6 --- /dev/null +++ b/tests/previews/external_turret_preview.gd.uid @@ -0,0 +1 @@ +uid://b4103wy1uqe8i diff --git a/tests/previews/external_turret_preview.tscn b/tests/previews/external_turret_preview.tscn new file mode 100644 index 0000000..3eb0fa7 --- /dev/null +++ b/tests/previews/external_turret_preview.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/external_turret_preview.gd" id="1"] +[node name="ExternalTurretPreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/previews/new_captain_skills_preview.gd b/tests/previews/new_captain_skills_preview.gd new file mode 100644 index 0000000..4705b91 --- /dev/null +++ b/tests/previews/new_captain_skills_preview.gd @@ -0,0 +1,32 @@ +extends Node2D + +func _ready() -> void: + GameState.start_new_run() + var captain := GameState.captain + var catalog := preload("res://scripts/captain/captain_skills.gd") + for id in catalog.IDS: captain.skills[id] = 1 + captain.parts = 5.0 + captain.equip_skill(0,"emergency_procedure") + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + game.player.set_physics_process(false) + game.player.get_node("WeaponController").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false) + var ui: CanvasLayer = game.find_child("CaptainSkills",true,false) + captain.learning_points = 1 + captain.skill_offers.assign(["scientist","gunsmith","bounty_hunter"]) + ui.open_management() + await _snapshot("res://artifacts/new_captain_skills_economy.png") + captain.skill_offers.assign(["missile_specialist","emergency_procedure"]) + ui.open_management() + await _snapshot("res://artifacts/new_captain_skills_combat.png") + game.player.hull = game.player.max_hull * 0.5 + assert(captain.activate_skill("emergency_procedure",game.player)) + print("NEW_CAPTAIN_SKILLS_PREVIEW: PASS") + get_tree().quit() + +func _snapshot(path: String) -> void: + for index in 12: await get_tree().process_frame + await RenderingServer.frame_post_draw + assert(get_viewport().get_texture().get_image().save_png(path) == OK) diff --git a/tests/previews/new_captain_skills_preview.gd.uid b/tests/previews/new_captain_skills_preview.gd.uid new file mode 100644 index 0000000..7fc1034 --- /dev/null +++ b/tests/previews/new_captain_skills_preview.gd.uid @@ -0,0 +1 @@ +uid://w0teqs6kqxgg diff --git a/tests/previews/new_captain_skills_preview.tscn b/tests/previews/new_captain_skills_preview.tscn new file mode 100644 index 0000000..9727e61 --- /dev/null +++ b/tests/previews/new_captain_skills_preview.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/new_captain_skills_preview.gd" id="1"] +[node name="NewCaptainSkillsPreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/previews/pulse_laser_live_demo.gd b/tests/previews/pulse_laser_live_demo.gd new file mode 100644 index 0000000..02c21b6 --- /dev/null +++ b/tests/previews/pulse_laser_live_demo.gd @@ -0,0 +1,74 @@ +extends Node2D + +class DemoLaser extends "res://scripts/game/weapon_controller.gd": + func _aim_direction_from(_origin: Vector2) -> Vector2: return Vector2.RIGHT + +var _ship: CharacterBody2D +var _controller: Node +var _item: ItemInstance +var _automatic := true +var _status: Label +var _delay := 0.0 + +func _ready() -> void: + GameState.start_new_run() + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + _ship = game.player + _ship.set_physics_process(false) + _ship.get_node("WeaponController").set_physics_process(false) + _ship.get_node("ExtensionSystems").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.queue_free() + _ship.position = Vector2(0,1500) + _ship.rotation = PI*0.5 + _ship.get_node("Camera2D").reset_smoothing() + _item = ItemInstance.new() + _item.definition = preload("res://resources/items/pulse_laser.tres") + var inventory: ShipInventory = _ship.get_node("ShipInventory") + inventory.weapon_slots[0] = _item + inventory.inventory_changed.emit() + _controller = DemoLaser.new() + _ship.add_child(_controller) + _controller.set_physics_process(false) + var target := preload("res://scenes/game/weapon_test_target.tscn").instantiate() + target.position = _ship.position+Vector2(450,0) + game.add_child(target) + target.get_node("Sprite2D").rotation = -PI*0.5 + var overlay := CanvasLayer.new() + overlay.layer = 100 + add_child(overlay) + var panel := PanelContainer.new() + panel.position = Vector2(350,90) + var style := StyleBoxFlat.new() + style.bg_color = Color("130913ec") + style.border_color = Color("d75f6c") + style.set_border_width_all(1) + style.set_content_margin_all(14) + panel.add_theme_stylebox_override("panel",style) + overlay.add_child(panel) + _status = Label.new() + _status.add_theme_font_size_override("font_size",16) + panel.add_child(_status) + _update_status() + print("PULSE_LASER_LIVE_DEMO: running — SPACE toggles fire, ESC closes demo") + +func _physics_process(delta: float) -> void: + if not is_instance_valid(_controller): return + _controller._shot_timers[0] = maxf(0.0,_controller._shot_timers[0]-delta) + _delay += delta + if _automatic and _delay >= 1.0: + _ship.energy = _ship.max_energy + _controller._fire_laser(0,_item) + +func _unhandled_key_input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and not event.echo: + if event.keycode == KEY_SPACE: + _automatic = not _automatic + _update_status() + get_viewport().set_input_as_handled() + elif event.keycode == KEY_ESCAPE: + get_tree().quit() + +func _update_status() -> void: + _status.text = "LASER PULSACYJNY · %s\nPojedyncze czerwone impulsy lecą do celu · 1500 px/s\nSPACJA: włącz/wyłącz strzelanie · ESC: zamknij pokaz" % ("STRZELA" if _automatic else "WSTRZYMANY") diff --git a/tests/previews/pulse_laser_live_demo.gd.uid b/tests/previews/pulse_laser_live_demo.gd.uid new file mode 100644 index 0000000..0702d9b --- /dev/null +++ b/tests/previews/pulse_laser_live_demo.gd.uid @@ -0,0 +1 @@ +uid://cm2kcugob60p diff --git a/tests/previews/pulse_laser_live_demo.tscn b/tests/previews/pulse_laser_live_demo.tscn new file mode 100644 index 0000000..ff0a250 --- /dev/null +++ b/tests/previews/pulse_laser_live_demo.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/pulse_laser_live_demo.gd" id="1"] +[node name="PulseLaserLiveDemo" type="Node2D"] +script = ExtResource("1") diff --git a/tests/previews/pulse_laser_preview.gd b/tests/previews/pulse_laser_preview.gd new file mode 100644 index 0000000..fd7afb5 --- /dev/null +++ b/tests/previews/pulse_laser_preview.gd @@ -0,0 +1,65 @@ +extends Node2D + +class StraightLaser extends "res://scripts/game/weapon_controller.gd": + func _aim_direction_from(_origin: Vector2) -> Vector2: return Vector2.RIGHT + +func _ready() -> void: + GameState.start_new_run() + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + var ship: CharacterBody2D = game.player + ship.set_physics_process(false) + ship.get_node("WeaponController").set_physics_process(false) + ship.get_node("ExtensionSystems").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.queue_free() + ship.position = Vector2(0,1500) + ship.rotation = PI*0.5 + ship.get_node("Camera2D").reset_smoothing() + var item := ItemInstance.new() + item.definition = preload("res://resources/items/pulse_laser.tres") + var inventory: ShipInventory = ship.get_node("ShipInventory") + inventory.weapon_slots[0] = item + inventory.inventory_changed.emit() + var controller := StraightLaser.new() + ship.add_child(controller) + controller.set_physics_process(false) + var layer := CanvasLayer.new() + layer.layer = 100 + add_child(layer) + var panel := PanelContainer.new() + panel.position = Vector2(340,80) + var style := StyleBoxFlat.new() + style.bg_color = Color("130913ec") + style.border_color = Color("c55965") + style.set_border_width_all(1) + style.set_corner_radius_all(8) + style.set_content_margin_all(14) + panel.add_theme_stylebox_override("panel",style) + layer.add_child(panel) + var row := HBoxContainer.new() + row.add_theme_constant_override("separation",14) + panel.add_child(row) + var icon := TextureRect.new() + icon.texture = item.definition.icon + icon.custom_minimum_size = Vector2(72,72) + icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + row.add_child(icon) + var text := Label.new() + text.text = "LASER PULSACYJNY\n50% impulsów omija tarcze\n−30% obrażeń • +30% zużycia energii" + text.add_theme_font_size_override("font_size",15) + row.add_child(text) + for frame in 12: await get_tree().process_frame + for distance in [120.0,420.0]: + controller._shot_timers[0] = 0.0 + controller._fire_laser(0,item) + for bolt in game.get_children(): + if bolt.get_script() == preload("res://scripts/game/weapon_projectile.gd") and bolt.is_physics_processing(): + bolt.set_physics_process(false) + bolt._physics_process(distance/item.definition.projectile_speed) + await get_tree().process_frame + await RenderingServer.frame_post_draw + assert(get_viewport().get_texture().get_image().save_png("res://artifacts/pulse_laser_flying_preview.png") == OK) + print("PULSE_LASER_PREVIEW: PASS") + get_tree().quit() diff --git a/tests/previews/pulse_laser_preview.gd.uid b/tests/previews/pulse_laser_preview.gd.uid new file mode 100644 index 0000000..7090f06 --- /dev/null +++ b/tests/previews/pulse_laser_preview.gd.uid @@ -0,0 +1 @@ +uid://bnulop1yqa1y4 diff --git a/tests/previews/pulse_laser_preview.tscn b/tests/previews/pulse_laser_preview.tscn new file mode 100644 index 0000000..d72ed1d --- /dev/null +++ b/tests/previews/pulse_laser_preview.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/pulse_laser_preview.gd" id="1"] +[node name="PulseLaserPreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/previews/salvage_drone_skills_preview.gd b/tests/previews/salvage_drone_skills_preview.gd new file mode 100644 index 0000000..ffd3ece --- /dev/null +++ b/tests/previews/salvage_drone_skills_preview.gd @@ -0,0 +1,23 @@ +extends Node2D + +func _ready() -> void: + GameState.start_new_run() + var captain := GameState.captain + var catalog := preload("res://scripts/captain/captain_skills.gd") + for id in catalog.IDS: + if id not in ["scrap_collector","drone_commander"]: captain.skills[id] = 1 + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + game.player.set_physics_process(false) + game.player.get_node("WeaponController").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false) + var ui: CanvasLayer = game.find_child("CaptainSkills",true,false) + captain.learning_points = 1 + captain.skill_offers.assign(["scrap_collector","drone_commander"]) + ui.open_management() + for index in 12: await get_tree().process_frame + await RenderingServer.frame_post_draw + assert(get_viewport().get_texture().get_image().save_png("res://artifacts/salvage_drone_skills.png") == OK) + print("SALVAGE_DRONE_SKILLS_PREVIEW: PASS") + get_tree().quit() diff --git a/tests/previews/salvage_drone_skills_preview.gd.uid b/tests/previews/salvage_drone_skills_preview.gd.uid new file mode 100644 index 0000000..b125e0a --- /dev/null +++ b/tests/previews/salvage_drone_skills_preview.gd.uid @@ -0,0 +1 @@ +uid://bkoqreceoove6 diff --git a/tests/previews/salvage_drone_skills_preview.tscn b/tests/previews/salvage_drone_skills_preview.tscn new file mode 100644 index 0000000..b063afe --- /dev/null +++ b/tests/previews/salvage_drone_skills_preview.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/salvage_drone_skills_preview.gd" id="1"] +[node name="SalvageDroneSkillsPreview" type="Node2D"] +script = ExtResource("1") diff --git a/tests/previews/weapon_icons_transparent_preview.gd b/tests/previews/weapon_icons_transparent_preview.gd new file mode 100644 index 0000000..4ec740f --- /dev/null +++ b/tests/previews/weapon_icons_transparent_preview.gd @@ -0,0 +1,55 @@ +extends Node + +class Checker extends Control: + func _draw() -> void: + for y in range(0,int(size.y),12): + for x in range(0,int(size.x),12): + var tint := Color("25323e") if (x/12+y/12)%2 == 0 else Color("17222e") + draw_rect(Rect2(x,y,12,12),tint) + +func _ready() -> void: + var layer := CanvasLayer.new() + add_child(layer) + var root := ColorRect.new() + root.color = Color("08111d") + root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + layer.add_child(root) + var title := Label.new() + title.text = "UZBROJENIE · IKONY BEZ TŁA" + title.position = Vector2(38,20) + title.add_theme_font_size_override("font_size",24) + root.add_child(title) + var generator := preload("res://scripts/items/item_generator.gd") + var lab := preload("res://scripts/planets/laboratory.gd") + var definitions: Array = generator.DEFINITIONS.duplicate() + for id in lab.IDS: + if lab.definition(id) not in definitions: definitions.append(lab.definition(id)) + var index := 0 + for definition in definitions: + if definition.category != ItemDefinition.Category.WEAPON: continue + var tile := VBoxContainer.new() + tile.position = Vector2(38+(index%4)*304,72+(index/4)*206) + tile.size = Vector2(286,194) + root.add_child(tile) + var checker := Checker.new() + checker.custom_minimum_size = Vector2(286,164) + tile.add_child(checker) + var icon := TextureRect.new() + icon.texture = definition.icon + icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS + checker.add_child(icon) + icon.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + var label := Label.new() + label.text = definition.display_name + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + label.add_theme_font_size_override("font_size",14) + tile.add_child(label) + index += 1 + for frame in 6: await get_tree().process_frame + await RenderingServer.frame_post_draw + assert(get_viewport().get_texture().get_image().save_png("res://artifacts/weapon_icons_transparent.png") == OK) + print("WEAPON_ICONS_PREVIEW: PASS") + get_tree().quit() diff --git a/tests/previews/weapon_icons_transparent_preview.gd.uid b/tests/previews/weapon_icons_transparent_preview.gd.uid new file mode 100644 index 0000000..ff90db3 --- /dev/null +++ b/tests/previews/weapon_icons_transparent_preview.gd.uid @@ -0,0 +1 @@ +uid://bh21rkpvul2ug diff --git a/tests/previews/weapon_icons_transparent_preview.tscn b/tests/previews/weapon_icons_transparent_preview.tscn new file mode 100644 index 0000000..9da53e7 --- /dev/null +++ b/tests/previews/weapon_icons_transparent_preview.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/previews/weapon_icons_transparent_preview.gd" id="1"] +[node name="WeaponIconsPreview" type="Node"] +script = ExtResource("1") diff --git a/tests/ships/laboratory_loot_test.gd b/tests/ships/laboratory_loot_test.gd new file mode 100644 index 0000000..171f660 --- /dev/null +++ b/tests/ships/laboratory_loot_test.gd @@ -0,0 +1,37 @@ +extends Node + +const LAB := preload("res://scripts/planets/laboratory.gd") +const GEN := preload("res://scripts/items/item_generator.gd") + +func _ready() -> void: + call_deferred("_run") + +func _run() -> void: + var state := get_node("/root/GameState") + state.start_new_run() + var captain: Resource = state.captain + var progress: Resource = captain.expedition + var initial := GEN.available_definitions() + for definition in initial: + assert(definition.category != ItemDefinition.Category.ACTIVE_EXTENSION) + assert(definition.category != ItemDefinition.Category.WEAPON or definition.item_id == "plasma_cannon") + for definition in GEN.DEFINITIONS: + if definition.category == ItemDefinition.Category.ACTIVE_EXTENSION or (definition.category == ItemDefinition.Category.WEAPON and definition.item_id != "plasma_cannon"): + assert(definition.item_id in LAB.IDS) + progress.freighter_rooms.expansion_1 = "laboratory" + for cost in LAB.COSTS.values(): + for key in cost: captain.minerals[key] = 10000 + LAB.ensure_offers(progress) + while not progress.laboratory_offers.is_empty(): + var id: String = progress.laboratory_offers[0] + assert(not GEN.available_definitions().has(LAB.definition(id))) + var count := GEN.available_definitions().size() + assert(LAB.start(captain,id)) + assert(not GEN.available_definitions().has(LAB.definition(id))) + LAB.advance(progress,LAB.DURATION) + assert(GEN.available_definitions().has(LAB.definition(id))) + assert(GEN.available_definitions().size() == count + 1) + assert(progress.laboratory_unlocks.size() == LAB.IDS.size()) + assert(GEN.available_definitions().size() == GEN.DEFINITIONS.size() + 3) + print("LABORATORY_LOOT_TEST: PASS (plasma-only starting weapon, locked extensions, all research unlocks, no duplicate loot)") + get_tree().quit() diff --git a/tests/ships/laboratory_loot_test.gd.uid b/tests/ships/laboratory_loot_test.gd.uid new file mode 100644 index 0000000..53839fa --- /dev/null +++ b/tests/ships/laboratory_loot_test.gd.uid @@ -0,0 +1 @@ +uid://dykj1pqmcwuc5 diff --git a/tests/ships/laboratory_loot_test.tscn b/tests/ships/laboratory_loot_test.tscn new file mode 100644 index 0000000..0456c5d --- /dev/null +++ b/tests/ships/laboratory_loot_test.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tests/ships/laboratory_loot_test.gd" id="1"] + +[node name="LaboratoryLootTest" type="Node"] +script = ExtResource("1") diff --git a/tests/ships/laboratory_test.gd b/tests/ships/laboratory_test.gd index ab41ad4..f752a5f 100644 --- a/tests/ships/laboratory_test.gd +++ b/tests/ships/laboratory_test.gd @@ -33,12 +33,22 @@ func _ready() -> void: var offers: Array = progress.laboratory_offers.duplicate() LAB.ensure_offers(progress) assert(progress.laboratory_offers==offers,"Opening laboratory does not reroll offers") - assert(GEN.available_definitions().size()==GEN.DEFINITIONS.size()) + for definition in GEN.available_definitions(): + assert(definition.category != ItemDefinition.Category.ACTIVE_EXTENSION) + assert(definition.category != ItemDefinition.Category.WEAPON or definition.item_id == "plasma_cannon") + for definition in GEN.DEFINITIONS: + if definition.category == ItemDefinition.Category.ACTIVE_EXTENSION or (definition.category == ItemDefinition.Category.WEAPON and definition.item_id != "plasma_cannon"): + assert(definition.item_id in LAB.IDS) + for id in LAB.IDS: + assert(LAB.definition(id) != null and LAB.COSTS.has(id)) assert(not LAB.start(captain,offers[0]),"Laboratory must be built") progress.freighter_rooms.expansion_1 = "laboratory" assert(not LAB.start(captain,offers[0]),"Cannot research without materials") for cost in LAB.COSTS.values(): for key in cost: captain.minerals[key] = 1000 + # Fix the first choice for the weapon integration checks below. + progress.laboratory_offers.assign(["incendiary_missile", "radiation_laser", "artillery_cannon"]) + offers = progress.laboratory_offers.duplicate() var game := preload("res://scenes/game/game.tscn").instantiate() add_child(game) game.set_physics_process(false) @@ -70,12 +80,15 @@ func _ready() -> void: GameState.advance_play_time(29.9,game) assert(not progress.laboratory_unlocks.has("incendiary_missile")) GameState.advance_play_time(0.1,game) - assert(progress.laboratory_unlocks.has("incendiary_missile") and progress.laboratory_offers.size()==2) + assert(progress.laboratory_unlocks.has("incendiary_missile") and progress.laboratory_offers.size()==3) assert(GEN.available_definitions().has(LAB.definition("incendiary_missile"))) - for id in ["radiation_laser","artillery_cannon"]: + while not progress.laboratory_offers.is_empty(): + var id: String = progress.laboratory_offers[0] + assert(not GEN.available_definitions().has(LAB.definition(id))) assert(LAB.start(captain,id)) LAB.advance(progress,60) - assert(progress.laboratory_offers.is_empty() and progress.laboratory_unlocks.size()==3) + assert(GEN.available_definitions().has(LAB.definition(id))) + assert(progress.laboratory_offers.is_empty() and progress.laboratory_unlocks.size()==LAB.IDS.size()) assert(GEN.available_definitions().size()==GEN.DEFINITIONS.size()+3) var inventory: ShipInventory = player.get_node("ShipInventory") var cannon := ItemInstance.new()