This commit is contained in:
Tomasz Boruc 2026-10-26 19:33:45 +01:00
parent 0665110c1a
commit 23ebb4ad4c
131 changed files with 2573 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

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

View File

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

View File

@ -10,7 +10,7 @@ icon = ExtResource("icon")
item_id = "beam_laser" item_id = "beam_laser"
display_name = "Laser wiązkowy" display_name = "Laser wiązkowy"
category = 0 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_stat = "damage"
primary_value = 3.0 primary_value = 3.0
stat_suffix = " DMG" stat_suffix = " DMG"

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,12 @@ func get_skill_rank(skill: String) -> int:
func get_skill_bonus(skill: String) -> float: func get_skill_bonus(skill: String) -> float:
return SKILLS.value(skill, get_skill_rank(skill)) 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: func get_energy_bank_fraction() -> float:
return maxf(0.10, get_skill_bonus("electrician")) 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]) 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): 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" 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 "" return ""
func activate_skill(skill: String, ship: CharacterBody2D) -> bool: func activate_skill(skill: String, ship: CharacterBody2D) -> bool:
if not skill_unavailable_reason(skill, ship).is_empty(): if not skill_unavailable_reason(skill, ship).is_empty():
return false return false
skill_cooldowns[skill] = SKILLS.COOLDOWN skill_cooldowns[skill] = SKILLS.cooldown(skill)
skill_durations[skill] = SKILLS.duration(skill) skill_durations[skill] = SKILLS.duration(skill)
if skill == "power_redirect": if skill == "power_redirect":
ship.set("shields", 0.0) ship.set("shields", 0.0)
ship.call("_update_shield_visual") 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() emit_changed()
return true 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 index := clampi(tier, 0, 2)
var scale_factor := preload("res://scripts/levels/level_balance.gd").item_stat_scale(clampi(system_level, 1, 1000)) 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 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) add_rewards(xp, money if grant_credits else 0)
return Vector2i(xp, money) return Vector2i(xp, money)

View File

@ -4,8 +4,15 @@ const MAX_RANK := 3
static var _icon_cache: Dictionary = {} static var _icon_cache: Dictionary = {}
const COOLDOWN := 30.0 const COOLDOWN := 30.0
const DURATION := 5.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 := { 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"}, "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"}, "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"}, "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]) return float(DATA[skill]["values"][clampi(rank, 1, MAX_RANK) - 1])
static func duration(skill: String) -> float: 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) 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: static func icon(skill: String) -> Texture2D:
if skill not in IDS: return null if skill not in IDS: return null
if not _icon_cache.has(skill): 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 atlas: Texture2D = load("res://assets/icons/captain/unified_skills.png")
var cell := atlas.get_size()/3.0 var cell := atlas.get_size()/3.0
var index := IDS.find(skill) var index := IDS.find(skill)
@ -44,6 +62,20 @@ static func icon(skill: String) -> Texture2D:
static func description(skill: String, rank: int) -> String: static func description(skill: String, rank: int) -> String:
var amount := roundi(value(skill, rank) * 100.0) var amount := roundi(value(skill, rank) * 100.0)
match skill: 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": "electrician":
return "Powerbank przywraca %d%% maksymalnej energii zamiast 10%%. Umiejętność pasywna; premie rang nie sumują się." % amount return "Powerbank przywraca %d%% maksymalnej energii zamiast 10%%. Umiejętność pasywna; premie rang nie sumują się." % amount
"lucky": "lucky":

View File

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

View File

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

View File

@ -90,7 +90,8 @@ func _clear_shot(target: Node2D) -> bool:
func _fire(target: Node2D) -> void: func _fire(target: Node2D) -> void:
var direction := (target.global_position - global_position).normalized() var direction := (target.global_position - global_position).normalized()
var bullet := PROJECTILE.instantiate() 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) get_parent().add_child(bullet)
_flash = 0.06 _flash = 0.06
mother.get_node("ExtensionSystems").call("break_cloak_on_fire") mother.get_node("ExtensionSystems").call("break_cloak_on_fire")

View File

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

View File

@ -0,0 +1 @@
uid://3w0qsvkh8rob

View File

@ -367,7 +367,7 @@ func _fire_plasma(direction: Vector2) -> void:
WEAPON_AUDIO_SCRIPT.play(get_parent(), global_position, weapon_kind, -4.0) 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: 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) take_weapon_hit(amount,critical,damage_type)
_weapon_piercing = 0.0 _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: 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 _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) _take_player_hit(amount,critical,damage_type,true,disabled_system)
_weapon_piercing = 0.0 _weapon_piercing = 0.0
_research_damage_scale = 1.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 piercing_damage := actual * _weapon_piercing if _research_player_hit else 0.0
var shield_damage := minf(shields, (actual - piercing_damage) * shield_multiplier) var shield_damage := minf(shields, (actual - piercing_damage) * shield_multiplier)
var double_hit := _research_direct_hit and research.learned("weapons_a_next_next_next") 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 remaining := actual if double_hit else maxf(0.0, actual - shield_damage / shield_multiplier)
var shields_before := shields var shields_before := shields
shields -= shield_damage shields -= shield_damage

View File

@ -17,6 +17,7 @@ var _has_destination := false
func reset_combat_state() -> void: func reset_combat_state() -> void:
cooldowns.clear() cooldowns.clear()
ship.clear_auxiliary_shield()
_clear_drones() _clear_drones()
solar_remaining = 0.0 solar_remaining = 0.0
if cloak_remaining > 0.0: if cloak_remaining > 0.0:
@ -167,7 +168,7 @@ func _start_cloak() -> void:
if shadow != null: if shadow != null:
_shadow_was_visible = shadow.visible _shadow_was_visible = shadow.visible
shadow.hide() 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 var visual := ship.get_node_or_null(name) as CanvasItem
if visual != null: if visual != null:
_visual_colors[name] = visual.modulate _visual_colors[name] = visual.modulate

View File

@ -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"): for target in tree.get_nodes_in_group("shielded_combatants"):
if not is_instance_valid(target) or target.is_queued_for_deletion(): continue 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 (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 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 if shield == null: continue
var radius := float(shield.shield_radius)*shield.global_transform.x.length() var radius := float(shield.shield_radius)*shield.global_transform.x.length()
var offset := start-shield.global_position var offset := start-shield.global_position

View File

@ -34,6 +34,9 @@ var scanner_range := 1000.0
@export var max_energy := 100.0 @export var max_energy := 100.0
var hull := 100.0 var hull := 100.0
var shields := 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 := 100.0
var energy_regeneration := 5.0 var energy_regeneration := 5.0
var shield_regeneration := 0.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_script(preload("res://scripts/game/extension_systems.gd"))
extension_systems.set("cooldowns", $ShipInventory.extension_cooldowns) extension_systems.set("cooldowns", $ShipInventory.extension_cooldowns)
add_child(extension_systems) add_child(extension_systems)
_ensure_auxiliary_shield_visual()
for index in ShipInventory.EXTENSION_SLOT_COUNT: for index in ShipInventory.EXTENSION_SLOT_COUNT:
_ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key) _ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key)
_ensure_input_action(&"use_energy_bank", KEY_R) _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) get_parent().add_child(popup)
return return
var remaining_damage: float = hit["damage"] 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 research := preload("res://scripts/planets/alien_research.gd")
var shields_before := shields 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: if research.learned("shields_b_next_next") and _salvo_cooldown <= 0.0:
remaining_damage *= 0.1 remaining_damage *= 0.1
_salvo_cooldown = 60.0 _salvo_cooldown = 60.0
@ -385,6 +394,38 @@ func _spawn_hit_effect(at_position: Vector2, effect_color: Color) -> void:
func _update_shield_visual() -> void: func _update_shield_visual() -> void:
if is_instance_valid(_shield_visual): if is_instance_valid(_shield_visual):
_shield_visual.call("set_shield_values", shields, max_shields) _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: func _destroy_ship() -> void:
if _is_destroyed: if _is_destroyed:
@ -463,6 +504,7 @@ func _physics_process(delta: float) -> void:
if GameState.captain.is_skill_active("power_redirect"): if GameState.captain.is_skill_active("power_redirect"):
shields = 0.0 shields = 0.0
if _is_destroyed or is_docked: if _is_destroyed or is_docked:
if auxiliary_shields > 0.0: clear_auxiliary_shield()
_mini_boost_active = false _mini_boost_active = false
return return
_refill_recovery_budgets(delta) _refill_recovery_budgets(delta)

View File

@ -104,6 +104,11 @@ func _physics_process(delta: float) -> void:
continue continue
var definition := item.definition 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: if definition.weapon_kind != ItemDefinition.WeaponKind.LASER:
_laser_pulse_timers[slot_index] = 0.0 _laser_pulse_timers[slot_index] = 0.0
_laser_lines[slot_index].visible = false _laser_lines[slot_index].visible = false
@ -121,6 +126,33 @@ func _physics_process(delta: float) -> void:
for slot_index in ShipInventory.WEAPON_SLOT_COUNT: for slot_index in ShipInventory.WEAPON_SLOT_COUNT:
_fire_just_pressed[slot_index] = false _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: 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")): if missile_cooldown_remaining > 0.0 or bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")):
return false return false
@ -156,7 +188,8 @@ func _fire_projectile(item: ItemInstance) -> void:
) )
ship.get_parent().add_child(projectile) ship.get_parent().add_child(projectile)
projectile.reset_physics_interpolation() 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("damage_type_override", item.get_damage_type())
projectile.set("missile_homing", definition.missile_homing) projectile.set("missile_homing", definition.missile_homing)
if definition.weapon_effect == "artillery": if definition.weapon_effect == "artillery":
@ -180,9 +213,17 @@ func _exit_tree() -> void:
func _fire_laser(slot_index: int, item: ItemInstance) -> void: func _fire_laser(slot_index: int, item: ItemInstance) -> void:
var definition := item.definition var definition := item.definition
var line := _laser_lines[slot_index] 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) 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()): if _shot_timers[slot_index] > 0.0 or float(ship.get("energy")) < _effective_energy_cost(item.get_energy_cost()):
return return
var piercing := item.roll_shield_piercing()
var muzzle_position := _muzzle_position() var muzzle_position := _muzzle_position()
var direction := _aim_direction_from(muzzle_position) var direction := _aim_direction_from(muzzle_position)
var endpoint := muzzle_position + direction * definition.weapon_range var endpoint := muzzle_position + direction * definition.weapon_range
@ -193,6 +234,7 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void:
[ship.get_rid()] [ship.get_rid()]
) )
var result := ship.get_world_2d().direct_space_state.intersect_ray(query) var result := ship.get_world_2d().direct_space_state.intersect_ray(query)
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()) 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(): if not result.is_empty():
endpoint = result["position"] endpoint = result["position"]
@ -210,7 +252,7 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void:
var target: Node = result.get("collider") var target: Node = result.get("collider")
var hull_before := float(target.get("hull")) if target.has_method("take_research_weapon_hit") else -1.0 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) 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)) _apply_laser_radiation(target,item,float(hit.damage))
_spawn_laser_impact(endpoint,item.get_damage_type(),direction) _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"): 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()) 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 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) var second := ship.get_world_2d().direct_space_state.intersect_ray(through)
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()) 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) var second_end: Vector2 = second.get("position",muzzle_position+direction*definition.weapon_range)
line.add_point(second_end) line.add_point(second_end)
if not second.is_empty(): if not second.is_empty():
preload("res://scripts/game/combat_vfx.gd").mark_hit(second.get("collider"),second_end,direction) 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) _apply_laser_radiation(second.get("collider"),item,float(hit.damage)*0.5)
_spawn_laser_impact(second_end,item.get_damage_type(),direction) _spawn_laser_impact(second_end,item.get_damage_type(),direction)
_laser_pulse_timers[slot_index] = definition.laser_pulse_duration _laser_pulse_timers[slot_index] = definition.laser_pulse_duration

View File

@ -140,6 +140,7 @@ func _physics_process(delta: float) -> void:
exclusions exclusions
) )
var result := get_world_2d().direct_space_state.intersect_ray(query) var result := get_world_2d().direct_space_state.intersect_ray(query)
if shield_piercing < 1.0:
result = preload("res://scripts/game/shield_intersections.gd").intercept(result,previous_position,next_position,collision_mask,exclusions,get_tree()) result = preload("res://scripts/game/shield_intersections.gd").intercept(result,previous_position,next_position,collision_mask,exclusions,get_tree())
if not result.is_empty(): if not result.is_empty():
global_position = result["position"] global_position = result["position"]
@ -199,6 +200,14 @@ func _find_enemy_in_acquisition_range() -> Node2D:
return nearest_enemy return nearest_enemy
func _draw() -> void: 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": if weapon_effect == "artillery":
draw_line(Vector2(-55,0),Vector2.ZERO,Color(1,0.4,0.08,0.2),10,true) 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) 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 is_explosion = true
effect.call("configure", is_explosion, effect_color) effect.call("configure", is_explosion, effect_color)
effect.call("configure_contact",get_damage_type(),direction) 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: func _explode_at_target(impact_target: Node) -> void:
if _harmless_intercepted: return if _harmless_intercepted: return

View File

@ -17,3 +17,9 @@ const DATA := {
static func icon(id: String) -> Texture2D: static func icon(id: String) -> Texture2D:
var extension := "svg" if id == "energy_bank" else "png" var extension := "svg" if id == "energy_bank" else "png"
return load("res://assets/icons/resources/%s.%s" % [id, extension]) as Texture2D 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

View File

@ -10,11 +10,14 @@ const DEFINITIONS := [
preload("res://resources/items/mine_layer.tres"), preload("res://resources/items/mine_layer.tres"),
preload("res://resources/items/flares.tres"), preload("res://resources/items/flares.tres"),
preload("res://resources/items/solar_shield_charge.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/gamma_cannon.tres"),
preload("res://resources/items/plasma_cannon.tres"), preload("res://resources/items/plasma_cannon.tres"),
preload("res://resources/items/beam_laser.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/machine_gun.tres"),
preload("res://resources/items/external_turret.tres"),
preload("res://resources/items/missile_launcher.tres"), preload("res://resources/items/missile_launcher.tres"),
preload("res://resources/items/unguided_missile.tres"), preload("res://resources/items/unguided_missile.tres"),
preload("res://resources/items/reinforced_shield.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) return generate_for_definition(available_definitions().pick_random(), level, enemy_tier)
static func available_definitions() -> Array: static func available_definitions() -> Array:
var pool := DEFINITIONS.duplicate()
var lab := preload("res://scripts/planets/laboratory.gd") 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: for id in lab.IDS:
if GameState.captain.expedition.laboratory_unlocks.get(id,false): pool.append(lab.definition(id)) if GameState.captain.expedition.laboratory_unlocks.get(id,false): pool.append(lab.definition(id))
return pool return pool

View File

@ -14,6 +14,17 @@ var _instance_ability: ActiveAbility
func shield_piercing() -> float: func shield_piercing() -> float:
return 0.5 if definition != null and definition.category == ItemDefinition.Category.WEAPON and "piercing" in special_orbs else 0.0 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: func get_damage_type() -> int:
if "electricity" in special_orbs: return DamageTypes.Type.ENERGY if "electricity" in special_orbs: return DamageTypes.Type.ENERGY
if "radiation" in special_orbs: return DamageTypes.Type.RADIATION 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(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() 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: func is_passive_extension() -> bool:
return definition != null and definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and not definition.passive_effect.is_empty() 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: for modifier in additional_modifiers:
if modifier and modifier.stat_key == "damage": if modifier and modifier.stat_key == "damage":
flat_bonus += modifier.value 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: func roll_damage() -> float:
var damage_range := get_damage_range() 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. # Inclusive integer rolls, preserving fractional flat bonuses on both bounds.
var whole_span := floori(damage_range.y - damage_range.x) var whole_span := floori(damage_range.y - damage_range.x)
return damage_range.x + randi_range(0, whole_span) 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_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_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) var base_average := maxf(1.0, base_lower + base_spread * 0.5)
base_average *= weapon_damage_scale()
var damage_range := get_damage_range() var damage_range := get_damage_range()
var average := maxf(0.1, (damage_range.x + damage_range.y) * 0.5) 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 damage_factor := pow(average / base_average, definition.energy_damage_exponent)
var addon_factor := 1.0 + get_addon_count() * definition.energy_per_addon 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) return snappedf(maxf(0.0, definition.energy_cost * damage_factor * addon_factor), 0.1)
func get_damage_range_text() -> String: 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)) 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: if definition.category == ItemDefinition.Category.WEAPON:
lines.append("Typ obrażeń: %s" % DamageTypes.NAMES[get_damage_type()]) 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.") 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]) lines.append("Zasięg: %.0f px • Amunicja: %d / strzał" % [definition.weapon_range,definition.ammunition_cost])
elif definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: elif definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA:
lines.append("Zasięg: %.0f px • Napromieniowana amunicja: 1 / strzał" % definition.weapon_range) 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") lines.append("Amunicja: 1 rakieta / strzał • Cooldown bazowy: 5 s • Bez kosztu energii")
else: else:
lines.append("Zasięg: %.0f px • Energia: %.1f / strzał" % [definition.weapon_range, get_energy_cost()]) 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(): if not additional_modifiers.is_empty():
lines.append("\nADDONY (aktywne po zamontowaniu)") lines.append("\nADDONY (aktywne po zamontowaniu)")
for modifier in additional_modifiers: for modifier in additional_modifiers:

View File

@ -136,7 +136,7 @@ func craft(recipe_id: String) -> bool:
var recipe: Dictionary = RECIPES.DATA[recipe_id] var recipe: Dictionary = RECIPES.DATA[recipe_id]
for mineral in recipe["cost"]: for mineral in recipe["cost"]:
GameState.captain.minerals[mineral] = int(GameState.captain.minerals.get(mineral, 0)) - int(recipe["cost"][mineral]) 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: match recipe_id:
"parts": GameState.captain.parts += amount "parts": GameState.captain.parts += amount
"ammunition": ammunition += 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: for equipped in weapon_slots.slice(0,unlocked_weapon_slots) + module_slots:
if equipped != null and equipped != item: if equipped != null and equipped != item:
reduction += equipped.get_stat_total("missile_cooldown_reduction") 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: func consume_missile() -> bool:
if missile_ammunition <= 0: 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 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 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() 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: 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) 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: 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) value *= 1.0+minf(preload("res://scripts/game/radiation_status.gd").DURATION,interval)

View File

@ -164,6 +164,9 @@ func claim() -> bool:
var amount := _rng.randi_range(40, 60) var amount := _rng.randi_range(40, 60)
inventory.ammunition += amount inventory.ammunition += amount
summary = "+%d amunicji" % 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() inventory.ammunition_changed.emit()
GameState.captain.emit_changed() GameState.captain.emit_changed()
if kind != "ruin": if kind != "ruin":

View File

@ -227,6 +227,7 @@ func _enter() -> void:
_hud_was_visible = game.get_node("HUD/HUDRoot").visible _hud_was_visible = game.get_node("HUD/HUDRoot").visible
game.get_node("HUD/HUDRoot").hide() game.get_node("HUD/HUDRoot").hide()
player.is_docked = true player.is_docked = true
player.clear_auxiliary_shield()
player.velocity = Vector2.ZERO player.velocity = Vector2.ZERO
get_tree().paused = true get_tree().paused = true
_prompt.hide() _prompt.hide()

View File

@ -518,7 +518,7 @@ func _build_supply_production() -> void:
var card := MENU.card(grid) var card := MENU.card(grid)
MENU.style(card.get_parent(),6) MENU.style(card.get_parent(),6)
card.add_theme_constant_override("separation",4) 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) var row := MENU.row(card)
MENU.icon(row,RECIPES.icon(id),28) 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) 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 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var cost: Dictionary = recipe.cost.duplicate() var cost: Dictionary = recipe.cost.duplicate()
for key in cost: cost[key] = int(cost[key])*batches 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) button.disabled = not _can_craft_supply(id,batches)
func _can_craft_supply(id: String, batches: int) -> bool: func _can_craft_supply(id: String, batches: int) -> bool:

View File

@ -1,7 +1,33 @@
extends RefCounted extends RefCounted
const DURATION := 60.0 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 := { 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}, "incendiary_missile":{"iron":45,"sulfur":60,"uranium":30,"copper":12,"mercury":6},
"radiation_laser":{"uranium":60,"aluminium":45,"silicon":18,"copper":12}, "radiation_laser":{"uranium":60,"aluminium":45,"silicon":18,"copper":12},
"artillery_cannon":{"iron":75,"titanium":45,"nickel":15,"tungsten":10}, "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 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]) 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_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.emit_changed()
captain.expedition.emit_changed() captain.expedition.emit_changed()
return true 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: static func advance(progress: Resource, delta: float) -> bool:
if progress.laboratory_project.is_empty() or delta <= 0.0: return false if progress.laboratory_project.is_empty() or delta <= 0.0: return false
progress.laboratory_remaining = maxf(0.0,progress.laboratory_remaining-delta) progress.laboratory_remaining = maxf(0.0,progress.laboratory_remaining-delta)

View File

@ -12,13 +12,13 @@ func _ready() -> void:
_build() _build()
func _state() -> String: 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: func _process(_delta: float) -> void:
if _signature != _state(): _build() if _signature != _state(): _build()
if is_instance_valid(_timer): if is_instance_valid(_timer):
_timer.text = "Pozostało: %d s gry" % ceili(captain.expedition.laboratory_remaining) _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: for id in _buttons:
if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id) if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id)
@ -30,8 +30,8 @@ func _build() -> void:
_buttons.clear() _buttons.clear()
_signature = _state() _signature = _state()
var progress: Resource = captain.expedition var progress: Resource = captain.expedition
MENU.label(self,"PROJEKTY NOWEGO UZBROJENIA",15,MENU.GOLD) MENU.label(self,"PROJEKTY BRONI I ROZSZERZEŃ",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,"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(): if not progress.laboratory_project.is_empty():
var item := LAB.definition(progress.laboratory_project) var item := LAB.definition(progress.laboratory_project)
var row := MENU.row(self) 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 MENU.label(row,item.display_name,16,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL
_timer = MENU.label(self,"",14) _timer = MENU.label(self,"",14)
_bar = ProgressBar.new() _bar = ProgressBar.new()
_bar.max_value = LAB.DURATION _bar.max_value = progress.laboratory_duration
_bar.custom_minimum_size.y = 18 _bar.custom_minimum_size.y = 18
add_child(_bar) add_child(_bar)
else: else:
@ -55,7 +55,7 @@ func _build() -> void:
var costs: Array[String] = [] 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])]) 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) 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.name = "Research_"+id
button.disabled = not LAB.can_start(captain,id) button.disabled = not LAB.can_start(captain,id)
_buttons[id] = button _buttons[id] = button

View File

@ -12,6 +12,7 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd")
@export var laboratory_offers: Array[String] = [] @export var laboratory_offers: Array[String] = []
@export var laboratory_project: String = "" @export var laboratory_project: String = ""
@export var laboratory_remaining: float = 0.0 @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 freighter_rooms: Dictionary = {"expansion_3":"module_storage"}
@export var mining_stations: Dictionary = {} @export var mining_stations: Dictionary = {}
@export var distant_maps: Dictionary = {} @export var distant_maps: Dictionary = {}

View File

@ -207,7 +207,8 @@ func _choice_card(skill: String) -> PanelContainer:
description.size_flags_vertical = Control.SIZE_EXPAND_FILL description.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_child(description) body.add_child(description)
if data["active"]: 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: else:
body.add_child(_label("STAŁY EFEKT", 11, Color("91aabd"))) body.add_child(_label("STAŁY EFEKT", 11, Color("91aabd")))
var select := Button.new() var select := Button.new()
@ -294,8 +295,8 @@ func _process(_delta: float) -> void:
var cooldown := float(_captain.skill_cooldowns.get(skill, 0.0)) var cooldown := float(_captain.skill_cooldowns.get(skill, 0.0))
var reason := _captain.skill_unavailable_reason(skill, player) var reason := _captain.skill_unavailable_reason(skill, player)
button.disabled = get_tree().paused or not reason.is_empty() button.disabled = get_tree().paused or not reason.is_empty()
button.call("set_visual_state", cooldown / CATALOG.COOLDOWN, duration > 0.0, button.disabled) 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 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.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.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") status.modulate = Color("a9ef83") if duration > 0.0 else Color("dae8ef")
_bar.visible = not bool(player.get("_is_destroyed")) and not _screen.visible _bar.visible = not bool(player.get("_is_destroyed")) and not _screen.visible

View File

@ -6,6 +6,7 @@ var inventory: ShipInventory
var _stock: Label var _stock: Label
var _availability: Label var _availability: Label
var _picture: TextureRect var _picture: TextureRect
var _output: Label
func _ready() -> void: func _ready() -> void:
custom_minimum_size = Vector2(120, 166) custom_minimum_size = Vector2(120, 166)
@ -42,10 +43,10 @@ func _ready() -> void:
_picture.mouse_filter = Control.MOUSE_FILTER_IGNORE _picture.mouse_filter = Control.MOUSE_FILTER_IGNORE
artwork.add_child(_picture) artwork.add_child(_picture)
_picture.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _picture.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var output := _label("+%d" % RECIPES.DATA[recipe_id]["amount"], 12, Color("efc681")) _output = _label("", 12, Color("efc681"))
output.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT _output.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
artwork.add_child(output) artwork.add_child(_output)
output.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _output.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var title := _label(RECIPES.DATA[recipe_id]["name"], 11, Color("e5edf4")) var title := _label(RECIPES.DATA[recipe_id]["name"], 11, Color("e5edf4"))
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title.custom_minimum_size.y = 36 title.custom_minimum_size.y = 36
@ -70,6 +71,7 @@ func refresh() -> void:
if _stock == null: if _stock == null:
return return
var available := inventory.can_craft(recipe_id) 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) _stock.text = "Zapas: %d" % inventory.get_crafted_stock(recipe_id)
_availability.text = "WYTWÓRZ" if available else "BRAK SUROWCÓW" _availability.text = "WYTWÓRZ" if available else "BRAK SUROWCÓW"
_availability.add_theme_color_override("font_color", Color("85deb1") if available else Color("d08c8e")) _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() var contents := VBoxContainer.new()
contents.add_theme_constant_override("separation", 8) contents.add_theme_constant_override("separation", 8)
panel.add_child(contents) 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()) contents.add_child(HSeparator.new())
for mineral in RECIPES.DATA[recipe_id]["cost"]: for mineral in RECIPES.DATA[recipe_id]["cost"]:
var needed := int(RECIPES.DATA[recipe_id]["cost"][mineral]) var needed := int(RECIPES.DATA[recipe_id]["cost"][mineral])

View File

@ -66,7 +66,7 @@ func _heading(caption: String, color: Color, font_size: int = 11) -> Label:
func _craft(id: String) -> void: func _craft(id: String) -> void:
if _inventory.craft(id): 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")) _status.add_theme_color_override("font_color", Color("85deb1"))
else: else:
_status.text = "BRAKUJE SUROWCÓW — NAJEDŹ NA RECEPTURĘ, ABY SPRAWDZIĆ KOSZT" _status.text = "BRAKUJE SUROWCÓW — NAJEDŹ NA RECEPTURĘ, ABY SPRAWDZIĆ KOSZT"

View File

@ -215,9 +215,13 @@ func _weapons() -> void:
UI.label(card, "Brak zamontowanej broni", 13, UI.MUTED) UI.label(card, "Brak zamontowanej broni", 13, UI.MUTED)
continue continue
_item_header(card,item) _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_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_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) _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) UI.label(card,"Typ: " + DamageTypes.NAMES[item.get_damage_type()],12,UI.MUTED)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More