Duzo zmian

This commit is contained in:
Tomasz Boruc 2026-10-16 22:04:36 +02:00
parent 85ba677339
commit e843d76c68
112 changed files with 2173 additions and 69 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="metal" x2=".8" y2="1"><stop stop-color="#e4edf3"/><stop offset=".48" stop-color="#5b7587"/><stop offset="1" stop-color="#172635"/></linearGradient><linearGradient id="gold" x2=".8" y2="1"><stop stop-color="#fff1b1"/><stop offset=".5" stop-color="#eac26a"/><stop offset="1" stop-color="#876028"/></linearGradient><radialGradient id="bg"><stop stop-color="#203449"/><stop offset="1" stop-color="#09121e"/></radialGradient></defs><rect width="128" height="128" rx="14" fill="url(#bg)"/><circle cx="64" cy="64" r="43" fill="#112933" stroke="#80d7ca" stroke-width="3"/><circle cx="64" cy="64" r="34" fill="none" stroke="#4c7a83" stroke-width="2"/><path d="M64 12v14m0 76v14M12 64h14m76 0h14M29 29l9 9m52 52 9 9M29 99l9-9m52-52 9-9" stroke="#c5eae6" stroke-width="2"/><path d="M91 32L72 72l-37 22 20-40z" fill="url(#metal)" stroke="#d2f5ed" stroke-width="2"/><path d="M91 32L55 54l17 18z" fill="#79dacb"/><path d="M35 94l37-22-17-18z" fill="#294d60"/><circle cx="64" cy="64" r="6" fill="url(#gold)"/><path d="M89 94l11-3 6 7-4 10-13 1-5-7z" fill="#879fb1" stroke="#dce9ee"/><path d="M88 100l10 2m-4-7-2 11" stroke="#244252" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://lqmq10ghoawn"
path="res://.godot/imported/explorer.svg-c2c25b150817ef3cb23b5aec41dcac40.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/explorer.svg"
dest_files=["res://.godot/imported/explorer.svg-c2c25b150817ef3cb23b5aec41dcac40.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="metal" x2=".8" y2="1"><stop stop-color="#e4edf3"/><stop offset=".48" stop-color="#5b7587"/><stop offset="1" stop-color="#172635"/></linearGradient><linearGradient id="gold" x2=".8" y2="1"><stop stop-color="#fff1b1"/><stop offset=".5" stop-color="#eac26a"/><stop offset="1" stop-color="#876028"/></linearGradient><radialGradient id="bg"><stop stop-color="#203449"/><stop offset="1" stop-color="#09121e"/></radialGradient></defs><rect width="128" height="128" rx="14" fill="url(#bg)"/><circle cx="64" cy="64" r="42" fill="#bb934c" opacity=".08"/><path d="M37 28l47-7 25 32-12 48-49 8-26-33z" fill="url(#metal)" stroke="#b9c7ce" stroke-width="2"/><path d="M37 28l25 32 47-7M62 60l-14 49" fill="none" stroke="#d4e0e6" stroke-width="2"/><path d="M42 40l16-2m13-3 11-2" stroke="#fff1bc" stroke-width="4" stroke-linecap="round"/><g fill="url(#gold)"><circle cx="78" cy="69" r="5"/><circle cx="96" cy="63" r="5"/><circle cx="73" cy="90" r="5"/><circle cx="91" cy="84" r="5"/><circle cx="36" cy="66" r="5"/><circle cx="46" cy="82" r="5"/></g><path d="M24 15l3 8 8 3-8 3-3 8-3-8-8-3 8-3zm83 76 3 8 8 3-8 3-3 8-3-8-8-3 8-3z" fill="#ffdc8a"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cmog75c28nh0v"
path="res://.godot/imported/lucky.svg-72d8243f6a27dec9318a407bb2dfb6e5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/lucky.svg"
dest_files=["res://.godot/imported/lucky.svg-72d8243f6a27dec9318a407bb2dfb6e5.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,20 @@
{
"tool": "built-in image_gen",
"assets": [
{
"key": "teleport_art",
"transparent": false,
"prompt": "Use case: stylized-concept. One finished premium square ability icon for a dark realistic space action RPG, no text or frame. A sleek small titanium spacecraft phasing through a circular mechanical hyperspace gate, brilliant violet cyan portal rim, fragmented translucent afterimages and luminous particles suggesting teleportation, a clearly legible ship centered inside the gate. Realistic painted 3D sci-fi inventory art, polished metal, rich deep navy background, restrained cinematic light, strong simple silhouette readable at 36 pixels. 12% safe margins on all sides, dark corners. No words, lettering, numbers, UI, logos, or multiple icons."
},
{
"key": "flares_art",
"transparent": false,
"prompt": "Use case: stylized-concept. One finished premium square ability icon for a dark realistic space action RPG, no text or frame. Three compact gunmetal countermeasure flare cartridges fanning outward from one small launcher, each ejecting a brilliant orange gold white burning flare, elegant arcing orange spark trails, warm reflected highlights on detailed metal. Dramatic but clean and highly readable at 36 pixels, strong simple triangular silhouette. Realistic painted 3D sci-fi inventory art, deep navy backdrop, 12% safe margins, dark corners, no words, lettering, numbers, UI, logos, or multiple icons."
},
{
"key": "mine_art",
"transparent": true,
"prompt": "Use case: game sprite. Create a single premium realistic sci-fi proximity mine seen strictly from directly above, orthographic top-down, suitable for a 2D space combat game AND an inventory icon. Compact circular dark gunmetal mechanical body with six short robust radial trigger arms arranged symmetrically, beveled titanium armor panels, copper accents, one glowing orange-red central sensor lens and several tiny amber indicator LEDs. Highly polished realistic painted 3D game asset with crisp readable silhouette at 32 pixels, restrained lighting from upper left, no perspective tilt. Mine fills 80% of square with safe transparent padding. Genuinely transparent background, no ground or cast shadow, no smoke, no explosion, no halo baked outside the object. No text, lettering, symbols, digits, UI, frame, or multiple objects."
}
]
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="metal" x2=".8" y2="1"><stop stop-color="#b3ccd6"/><stop offset=".5" stop-color="#35505f"/><stop offset="1" stop-color="#142332"/></linearGradient><linearGradient id="accent" x2=".4" y2="1"><stop stop-color="#f1ffff"/><stop offset=".5" stop-color="#78e8eb"/><stop offset="1" stop-color="#78e8eb" stop-opacity=".3"/></linearGradient><linearGradient id="dark" x2="1" y2="1"><stop stop-color="#263b50"/><stop offset="1" stop-color="#0b1726"/></linearGradient></defs><path d="M64 10l43 23-7 50-36 31-36-31-7-50z" fill="url(#dark)" stroke="#4a8198" stroke-width="2"/><path d="M64 24L36 89l28-13 28 13z" fill="url(#metal)" opacity=".25" stroke="#b4f9ff" stroke-width="2"/><path d="M64 24L36 89l28-13 28 13" fill="none" stroke="url(#accent)" stroke-width="2" stroke-dasharray="7 5"/><path d="M26 43h76M24 56h80M27 69h74M32 82h64" stroke="#85e9f0" stroke-width="2" opacity=".28"/><path d="M31 105L101 22" stroke="#bcfcff" stroke-width="3"/><path d="M35 108L105 25" stroke="#439bba" opacity=".6"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c2y3r2bcd76j3"
path="res://.godot/imported/cloak.svg-1b5aa796b5986a3b2eadf42c7bc6c8f5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/cloak.svg"
dest_files=["res://.godot/imported/cloak.svg-1b5aa796b5986a3b2eadf42c7bc6c8f5.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ddqhcasuuecgc"
path="res://.godot/imported/cloak_art.png-86e932fd023e9e2d1cf22befbd492f1b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/cloak_art.png"
dest_files=["res://.godot/imported/cloak_art.png-86e932fd023e9e2d1cf22befbd492f1b.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,3 @@
{
"tool": "built-in image_gen"
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="metal" x2=".8" y2="1"><stop stop-color="#b3ccd6"/><stop offset=".5" stop-color="#35505f"/><stop offset="1" stop-color="#142332"/></linearGradient><linearGradient id="accent" x2=".4" y2="1"><stop stop-color="#f1ffff"/><stop offset=".5" stop-color="#ff9e63"/><stop offset="1" stop-color="#ff9e63" stop-opacity=".3"/></linearGradient><linearGradient id="dark" x2="1" y2="1"><stop stop-color="#263b50"/><stop offset="1" stop-color="#0b1726"/></linearGradient></defs><g fill="none" stroke-linecap="round"><path d="M35 106Q25 65 42 31" stroke="#e0783f" stroke-width="5"/><path d="M64 110V22" stroke="#f6a35c" stroke-width="6"/><path d="M93 106Q103 65 86 31" stroke="#e0783f" stroke-width="5"/><path d="M34 103Q29 69 42 37m22 69V29m30 74Q99 69 86 37" stroke="#ffdb9c" stroke-width="2"/></g><g fill="#ffba71" opacity=".2"><circle cx="42" cy="30" r="19"/><circle cx="64" cy="21" r="19"/><circle cx="86" cy="30" r="19"/></g><g fill="#fff2ca"><path d="M42 12l4 13 12 5-12 4-4 13-4-13-12-4 12-5z"/><path d="M64 3l4 13 12 5-12 4-4 13-4-13-12-4 12-5z"/><path d="M86 12l4 13 12 5-12 4-4 13-4-13-12-4 12-5z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cujt4m4cehoqv"
path="res://.godot/imported/flares.svg-837a92111af09adbbac44fda02b8653b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/flares.svg"
dest_files=["res://.godot/imported/flares.svg-837a92111af09adbbac44fda02b8653b.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ccdxl4xyjwyg3"
path="res://.godot/imported/flares_art.png-8257018c1bf2262aa2bd54553a07d479.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/flares_art.png"
dest_files=["res://.godot/imported/flares_art.png-8257018c1bf2262aa2bd54553a07d479.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.6 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dwv0fw3dyupj3"
path="res://.godot/imported/mine_art.png-d9855ade10eaece77e81644f714c4fca.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/mine_art.png"
dest_files=["res://.godot/imported/mine_art.png-d9855ade10eaece77e81644f714c4fca.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 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="metal" x2=".8" y2="1"><stop stop-color="#b3ccd6"/><stop offset=".5" stop-color="#35505f"/><stop offset="1" stop-color="#142332"/></linearGradient><linearGradient id="accent" x2=".4" y2="1"><stop stop-color="#f1ffff"/><stop offset=".5" stop-color="#ffb76f"/><stop offset="1" stop-color="#ffb76f" stop-opacity=".3"/></linearGradient><linearGradient id="dark" x2="1" y2="1"><stop stop-color="#263b50"/><stop offset="1" stop-color="#0b1726"/></linearGradient></defs><g stroke="#ecb76f" stroke-width="5" stroke-linecap="round"><path d="M64 14v19m0 62v19M14 64h19m62 0h19M29 29l14 14m42 42 14 14M29 99l14-14m42-42 14-14"/></g><circle cx="64" cy="64" r="34" fill="url(#metal)" stroke="#ecb76f" stroke-width="3"/><circle cx="64" cy="64" r="24" fill="url(#dark)" stroke="#a77e53" stroke-width="2"/><circle cx="64" cy="64" r="12" fill="url(#accent)"/><path d="M42 64h8m28 0h8M64 42v8m0 28v8" stroke="#fff0cf" stroke-width="3"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://beubewxxfj0nk"
path="res://.godot/imported/mine_layer.svg-01b12ba6f174bb2a7cfd0b9cfeb7da19.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/mine_layer.svg"
dest_files=["res://.godot/imported/mine_layer.svg-01b12ba6f174bb2a7cfd0b9cfeb7da19.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="metal" x2=".8" y2="1"><stop stop-color="#b3ccd6"/><stop offset=".5" stop-color="#35505f"/><stop offset="1" stop-color="#142332"/></linearGradient><linearGradient id="accent" x2=".4" y2="1"><stop stop-color="#f1ffff"/><stop offset=".5" stop-color="#72dcff"/><stop offset="1" stop-color="#72dcff" stop-opacity=".3"/></linearGradient><linearGradient id="dark" x2="1" y2="1"><stop stop-color="#263b50"/><stop offset="1" stop-color="#0b1726"/></linearGradient></defs><path d="M64 25l37 15-5 37q-5 20-32 36-27-16-32-36l-5-37z" fill="url(#dark)" stroke="url(#accent)" stroke-width="4"/><path d="M64 39l23 10-3 25q-4 13-20 23-16-10-20-23l-3-25z" fill="#2886aa" opacity=".25" stroke="#b6f5ff" stroke-width="2"/><path d="M68 44L50 72h13l-4 19 22-31H67z" fill="#bdf8ff"/><circle cx="97" cy="25" r="13" fill="#ffce6c"/><g stroke="#ffe3a0" stroke-width="3" stroke-linecap="round"><path d="M97 3v5m0 34v5M75 25h5m34 0h5M81 9l4 4m24 24 4 4M81 41l4-4m24-24 4-4"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://y31gpw240xo0"
path="res://.godot/imported/solar_shield_charge.svg-b342289bf0e77d2c650ef1077b9b60d0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/solar_shield_charge.svg"
dest_files=["res://.godot/imported/solar_shield_charge.svg-b342289bf0e77d2c650ef1077b9b60d0.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://jemfbex10v14"
path="res://.godot/imported/solar_shield_charge_art.png-4a285d20cee948e9860ce0789735a22e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/solar_shield_charge_art.png"
dest_files=["res://.godot/imported/solar_shield_charge_art.png-4a285d20cee948e9860ce0789735a22e.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 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><defs><linearGradient id="metal" x2=".8" y2="1"><stop stop-color="#b3ccd6"/><stop offset=".5" stop-color="#35505f"/><stop offset="1" stop-color="#142332"/></linearGradient><linearGradient id="accent" x2=".4" y2="1"><stop stop-color="#f1ffff"/><stop offset=".5" stop-color="#a69bff"/><stop offset="1" stop-color="#a69bff" stop-opacity=".3"/></linearGradient><linearGradient id="dark" x2="1" y2="1"><stop stop-color="#263b50"/><stop offset="1" stop-color="#0b1726"/></linearGradient></defs><ellipse cx="64" cy="79" rx="42" ry="19" fill="none" stroke="url(#accent)" stroke-width="5"/><ellipse cx="64" cy="79" rx="30" ry="12" fill="#5e42ac" opacity=".35"/><path d="M27 65V30m74 35V30M40 52V14m48 38V14" stroke="url(#accent)" stroke-width="3" stroke-linecap="round" opacity=".7"/><path d="M64 25L42 76l22-9 22 9z" fill="url(#metal)" stroke="#e0d8ff" stroke-width="2"/><path d="M64 44v20m-7 17 7 18 7-18" fill="none" stroke="#d7b7ff" stroke-width="3"/><path d="M25 103h78" stroke="#8064cf" stroke-width="2" opacity=".5"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bdgnd1s2jd1l6"
path="res://.godot/imported/teleport.svg-42397f6d0e76594345e1c58a2997a16e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/teleport.svg"
dest_files=["res://.godot/imported/teleport.svg-42397f6d0e76594345e1c58a2997a16e.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
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c8emovr704ky4"
path="res://.godot/imported/teleport_art.png-96d2eb9efda7c053a9325458f6b883ae.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/extensions/teleport_art.png"
dest_files=["res://.godot/imported/teleport_art.png-96d2eb9efda7c053a9325458f6b883ae.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: 1.4 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -0,0 +1,6 @@
{
"tool": "built-in image_gen",
"asset": "assets/icons/resources/aluminium.png",
"transparent_background": true,
"prompt": "Use case: game asset. Generate one premium realistic painted 3D game sprite of raw ALUMINIUM ORE for a sci-fi space mining game, usable both as an inventory resource icon and as an asteroid in top-down 2D gameplay. One rugged irregular charcoal-gray rock with abundant pale SILVERY WHITE mineral seams and light pearl-gray crystalline facets embedded naturally in dark stone. Clearly a raw mined ore nugget, NOT smelted bars, NOT ingots, NOT a manufactured object. Distinct from rusty orange iron, blue titanium, green uranium and yellow sulfur. Match a high-quality resource icon set of chunky dark rocks with colored mineral veins. Compact approximately rounded asymmetric silhouette, rich rock texture, controlled highlights from upper left, crisp and legible at 48 pixels. Object fills about 80 percent of square with safe padding, entirely inside the frame. Genuine transparent background; no scene, no floor, no drop shadow, no frame, no text, letters, numbers or other objects."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cmtfsckdh2f35"
path="res://.godot/imported/captain_skills_art.png-b442619ccaaf8a841989cf1910d4fb60.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/ui/captain_skills_art.png"
dest_files=["res://.godot/imported/captain_skills_art.png-b442619ccaaf8a841989cf1910d4fb60.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://bfpmuonivd2r4"
path="res://.godot/imported/crafting_art.png-30b85c072a19ade651aee4403a07600c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/ui/crafting_art.png"
dest_files=["res://.godot/imported/crafting_art.png-30b85c072a19ade651aee4403a07600c.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 @@
{
"tool": "built-in image_gen",
"prompts": [
{
"asset": "assets/icons/ui/crafting_art.png",
"prompt": "Use case: stylized-concept. Create ONE finished premium square UI button artwork for a dark realistic sci-fi space RPG. Subject: a compact futuristic engineering workbench emblem, a heavy brushed titanium wrench crossing a precise copper mechanical gear, a tiny luminous amber fusion core at the junction. Bold single iconic silhouette, tasteful warm amber light and metallic highlights on dark charcoal navy, realistic painted 3D game icon matching high quality RPG ability icons. Centered object, 12% safe padding, dark navy corners, no border or UI frame. Must be unmistakable and legible when displayed at 54px. No text, letters, digits, labels or multiple icons. Opaque square background."
},
{
"asset": "assets/icons/ui/captain_skills_art.png",
"prompt": "Use case: stylized-concept. Create ONE finished premium square UI button artwork for a dark realistic sci-fi space RPG. Subject: a commanding futuristic captain's insignia, a sculpted brushed titanium five-point star with a small violet crystalline core, flanked by two elegant metallic wing/laurel shapes, subtle radiant violet and cyan light. Bold single iconic emblem, realistic painted 3D game icon matching high quality RPG ability icons, deep charcoal navy backdrop, centered composition with 12% safe padding, dark navy corners, no border or UI frame. Must remain unmistakable and legible at 54px. No text, letters, digits, labels or multiple icons. Opaque square background."
}
]
}

View File

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

View File

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=18 format=3]
[gd_resource type="Resource" script_class="ItemAffixDatabase" load_steps=19 format=3]
[ext_resource type="Script" path="res://scripts/items/item_affix_database.gd" id="1_database"]
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="2_type"]
@ -21,6 +21,8 @@
[ext_resource type="Resource" path="res://resources/items/affixes/energy_absorption.tres" id="energy"]
[ext_resource type="Resource" path="res://resources/items/affixes/radiation_absorption.tres" id="radiation"]
[ext_resource type="Resource" path="res://resources/items/affixes/luck_percent.tres" id="luck"]
[resource]
script = ExtResource("1_database")
affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix"), ExtResource("14_affix"), ExtResource("kinetic"), ExtResource("energy"), ExtResource("radiation")])
affixes = Array[ExtResource("2_type")]([ExtResource("3_affix"), ExtResource("4_affix"), ExtResource("5_affix"), ExtResource("6_affix"), ExtResource("7_affix"), ExtResource("8_affix"), ExtResource("9_affix"), ExtResource("10_affix"), ExtResource("11_affix"), ExtResource("12_affix"), ExtResource("13_affix"), ExtResource("14_affix"), ExtResource("kinetic"), ExtResource("energy"), ExtResource("radiation"), ExtResource("luck")])

View File

@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="ItemAffixDefinition" load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/items/item_affix_definition.gd" id="1"]
[resource]
script = ExtResource("1")
affix_id = "luck_percent"
display_name = "Szczęście"
description = "Zwiększa szansę na przedmiot o 1 poziom wyższy. Sumuje się z Farciarzem, do wspólnego limitu 60%."
stat_key = "luck_percent"
unit = "%"
selection_weight = 0.7
category_mask = 95
minimum_value_level_1 = 0.5
maximum_value_level_1 = 1.0
minimum_value_level_1000 = 3.0
maximum_value_level_1000 = 6.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
shader_type canvas_item;
uniform float elapsed = 0.0;
void fragment() {
vec4 tex = texture(TEXTURE, UV);
float edge = abs(tex.a - texture(TEXTURE, UV + vec2(TEXTURE_PIXEL_SIZE.x * 2.0, 0.0)).a);
edge += abs(tex.a - texture(TEXTURE, UV + vec2(0.0, TEXTURE_PIXEL_SIZE.y * 2.0)).a);
float scan = pow(0.5 + 0.5 * sin(UV.y * 140.0 - elapsed * 4.0), 14.0);
COLOR = vec4(mix(tex.rgb * vec3(0.3, 0.65, 0.9), vec3(0.4, 0.95, 1.0), clamp(edge + scan * 0.5, 0.0, 1.0)), tex.a * (0.10 + edge * 0.25 + scan * 0.1));
}

View File

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

View File

@ -7,7 +7,7 @@ radius = 7.0
[node name="EnemyPlasmaProjectile" type="Area2D"]
collision_layer = 0
collision_mask = 3
collision_mask = 51
monitoring = true
monitorable = false
script = ExtResource("1_script")

View File

@ -26,7 +26,7 @@ script = ExtResource("1_game")
[node name="GameMusic" type="AudioStreamPlayer" parent="." unique_id=784369202]
script = ExtResource("8_scene_music")
track_path = "res://assets/sounds/music/game_music.mp3"
track_path = "res://assets/sounds/music/music2.mp3"
playback_volume_db = -17.0
[node name="SpaceBackground" type="Node2D" parent="." unique_id=300719476]
@ -55,6 +55,7 @@ shape = SubResource("RightWall")
[node name="PlayerShip" parent="." unique_id=1837545573 instance=ExtResource("3_player")]
[node name="HUD" type="CanvasLayer" parent="." unique_id=733012261]
layer = 20
script = ExtResource("4_hud")
[node name="HUDRoot" type="Control" parent="HUD" unique_id=539413585]

View File

@ -1,6 +1,8 @@
extends Resource
class_name ActiveAbility
@export var ability_id: String = ""
@export var cooldown: float = 0.0
@export var display_name: String = ""
@export_multiline var description: String = ""

View File

@ -0,0 +1,25 @@
extends ActiveAbility
@export_enum("Teleport", "Cloak", "Mine", "Flares", "Solar shields") var kind := 0
func unavailable_reason(ship: CharacterBody2D) -> String:
match kind:
0:
if float(ship.get("energy")) <= 0.0:
return "Brak energii"
if not ship.get_node("ExtensionSystems").call("has_teleport_destination"):
return "Brak bezpiecznego miejsca na teleport"
1:
if ship.call("is_cloaked"):
return "Kamuflaż już działa"
4:
if GameState.captain.is_skill_active("power_redirect"):
return "Tarcze wyłączone przez Przekierowanie zasilania"
if float(ship.call("get_solar_regeneration")) <= 0.0:
return "Wymagany zasięg gwiazdy"
if float(ship.get("shields")) >= float(ship.get("max_shields")):
return "Tarcze są pełne"
return ""
func activate(ship: CharacterBody2D) -> void:
ship.get_node("ExtensionSystems").call("activate", kind)

View File

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

View File

@ -4,6 +4,7 @@ class_name CaptainProgress
const BALANCE := preload("res://scripts/captain/captain_balance.gd")
const SKILLS := preload("res://scripts/captain/captain_skills.gd")
const ACTIVE_SKILL_SLOTS := 5
const MAX_LUCK_CHANCE := 0.60
@export var level: int = 1
@export var experience: int = 0
@ -111,6 +112,17 @@ func prepare_skill_offers() -> void:
skill_offers.append(pool[index])
pool.remove_at(index)
func get_luck_chance(equipment_bonus_percent: float = 0.0) -> float:
return clampf(get_skill_bonus("lucky") + equipment_bonus_percent / 100.0, 0.0, MAX_LUCK_CHANCE)
func roll_loot_level(base_level: int, roll: float = -1.0, equipment_bonus_percent: float = 0.0) -> int:
var level := clampi(base_level, 1, 1000)
var chance := get_luck_chance(equipment_bonus_percent)
if chance <= 0.0 or level >= 1000:
return level
var luck_roll := randf() if roll < 0.0 else roll
return mini(1000, level + 1) if luck_roll < chance else level
func discounted_price(base_price: int) -> int:
return maxi(1, roundi(base_price * (1.0 - get_skill_bonus("business_shark"))))
@ -121,6 +133,11 @@ func should_consume_ammunition(ammo_kind: String) -> bool:
_ammo_credit[ammo_kind] = maxf(0.0, credit - 1.0) if saved else credit
return not saved
func reset_combat_state() -> void:
skill_cooldowns.clear()
skill_durations.clear()
emit_changed()
func tick_skills(delta: float) -> void:
for skill in skill_cooldowns:
skill_cooldowns[skill] = maxf(0.0, float(skill_cooldowns[skill]) - delta)

View File

@ -3,8 +3,10 @@ extends RefCounted
const MAX_RANK := 3
const COOLDOWN := 30.0
const DURATION := 5.0
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect"]
const IDS := ["business_shark", "reactor_overload", "tactical_maneuver", "efficient_magazine", "miner", "power_redirect", "lucky", "explorer"]
const DATA := {
"lucky": {"name": "Farciarz", "active": false, "values": [0.05, 0.10, 0.25], "color": "e8c76f"},
"explorer": {"name": "Eksplorator", "active": false, "values": [1, 3, 6], "color": "76d9cc"},
"business_shark": {"name": "Rekin biznesu", "active": false, "values": [0.10, 0.30, 0.60], "color": "efb568"},
"reactor_overload": {"name": "Przeciążenie reaktora", "active": true, "values": [0.10, 0.20, 0.70], "color": "63d9f5"},
"tactical_maneuver": {"name": "Manewr taktyczny", "active": true, "values": [0.30, 0.60, 1.00], "color": "b9a0ef"},
@ -19,11 +21,16 @@ static func value(skill: String, rank: int) -> float:
return float(DATA[skill]["values"][clampi(rank, 1, MAX_RANK) - 1])
static func icon(skill: String) -> Texture2D:
return load("res://assets/icons/captain/%s.png" % skill) as Texture2D
var extension := "svg" if skill in ["lucky", "explorer"] else "png"
return load("res://assets/icons/captain/%s.%s" % [skill, extension]) as Texture2D
static func description(skill: String, rank: int) -> String:
var amount := roundi(value(skill, rank) * 100.0)
match skill:
"lucky":
return "Szczęście: %d%% szans na przedmiot o 1 poziom wyższy niż bazowy loot planszy. Dotyczy lootu z wrogów i znalezisk. Bazowo: 0%%. Sumuje się ze sprzętem do limitu 60%%. Premie rang nie sumują się." % amount
"explorer":
return "+%d wraków na nowej planszy. Premia ustalana przy rozpoczęciu systemu; nie zmienia bieżącej planszy. Premie rang nie sumują się." % roundi(value(skill, rank))
"business_shark":
return "Zakupy za kredyty tańsze o %d%%." % amount
"reactor_overload":

View File

@ -40,6 +40,14 @@ func configure(sprite: Sprite2D) -> void:
outline.append_array(_edge_vertices(vertical[y * 5 + x + 1], false))
outline.append_array(_edge_vertices(horizontal[(y + 1) * 4 + x], true))
outline.append_array(_edge_vertices(vertical[y * 5 + x], true))
# Rare jagged cracks can cross at a corner. Repair the silhouette once,
# rather than submitting an invalid polygon every rendered frame.
if Geometry2D.triangulate_polygon(outline).is_empty():
outline = Geometry2D.convex_hull(outline)
if outline.size() > 1 and outline[0].is_equal_approx(outline[outline.size() - 1]):
outline.remove_at(outline.size() - 1)
if outline.size() < 3 or Geometry2D.triangulate_polygon(outline).is_empty():
continue
var center := Vector2.ZERO
for vertex in outline:
center += vertex
@ -54,7 +62,8 @@ func configure(sprite: Sprite2D) -> void:
_fragments.append({"points": points, "uv": uv, "position": origin,
"velocity": direction.rotated(_rng.randf_range(-0.35, 0.35)) * _rng.randf_range(40.0, 95.0),
"angle": 0.0, "spin": _rng.randf_range(-4.0, 4.0)})
_dust_color = Color("a1c7df") if "ice" in _texture.resource_path else Color("b4a99a")
var mineral := _texture.resource_path.get_file().get_basename()
_dust_color = {"iron": Color("be9b75"), "titanium": Color("9ecbe9"), "uranium": Color("a9db71"), "aluminium": Color("d3dce0"), "sulfur": Color("e8cd65")}.get(mineral, Color("b4a99a"))
for index in 28:
_dust.append({"direction": Vector2.RIGHT.rotated(_rng.randf_range(0.0, TAU)),
"speed": _rng.randf_range(45.0, 210.0), "size": _rng.randf_range(1.0, 3.0)})

View File

@ -0,0 +1,71 @@
extends StaticBody2D
const VFX := preload("res://scripts/game/extension_vfx.gd")
const LIFETIME := 10.0
const MAX_PARTICLES := 80
var drift := Vector2.ZERO
var _age := 0.0
var _emission := 0.0
var _particles: Array[Dictionary] = []
var _rng := RandomNumberGenerator.new()
var _launch_direction := Vector2.DOWN
func _ready() -> void:
add_to_group("player_flares")
collision_layer = 16
collision_mask = 0
z_index = 23
_rng.randomize()
_launch_direction = drift.normalized() if drift.length_squared() > 0.01 else Vector2.DOWN
var collision := CollisionShape2D.new()
var shape := CircleShape2D.new()
shape.radius = 9.0
collision.shape = shape
add_child(collision)
func _physics_process(delta: float) -> void:
_age += delta
position += drift * delta
drift = drift.move_toward(Vector2.ZERO, 35.0 * delta)
queue_redraw()
if _age >= LIFETIME:
queue_free()
func _process(delta: float) -> void:
for index in range(_particles.size() - 1, -1, -1):
var particle := _particles[index]
particle["age"] = float(particle["age"]) + delta
particle["position"] = Vector2(particle["position"]) + Vector2(particle["velocity"]) * delta
particle["velocity"] = Vector2(particle["velocity"]) * exp(-1.8 * delta)
if float(particle["age"]) >= float(particle["life"]):
_particles.remove_at(index)
_emission += delta
while _emission >= 0.018 and _particles.size() < MAX_PARTICLES:
_emission -= 0.018
var side := Vector2(-_launch_direction.y, _launch_direction.x)
_particles.append({"position": global_position, "velocity": -_launch_direction * _rng.randf_range(15, 55) + side * _rng.randf_range(-18, 18), "age": 0.0, "life": _rng.randf_range(0.35, 0.85), "size": _rng.randf_range(1, 2.4)})
_emission = minf(_emission, 0.018)
queue_redraw()
func take_damage(_amount: float) -> void:
# Countermeasure burns for the full ten seconds even when hit.
pass
func _draw() -> void:
var fade := clampf(LIFETIME - _age, 0.0, 1.0)
var flicker := 0.90 + 0.06 * sin(_age * 29.0) + 0.04 * sin(_age * 47.0)
var glow := VFX.radial_texture()
for particle in _particles:
var remaining := 1.0 - float(particle["age"]) / float(particle["life"])
var point := to_local(Vector2(particle["position"]))
var radius := (5.0 + float(particle["age"]) * 10.0) * remaining
draw_texture_rect(glow, Rect2(point - Vector2.ONE * radius, Vector2.ONE * radius * 2), false, Color(0.9, 0.25, 0.04, remaining * 0.16 * fade))
var spark_color := Color(1.0, lerpf(0.24, 0.85, remaining), lerpf(0.02, 0.4, remaining), remaining * fade)
draw_line(point, point - Vector2(particle["velocity"]).normalized() * 5.0 * remaining, spark_color, float(particle["size"]) * remaining, true)
var radius := 48.0 * flicker
draw_texture_rect(glow, Rect2(-radius, -radius, radius * 2, radius * 2), false, Color(1.0, 0.28, 0.025, 0.52 * fade))
draw_texture_rect(glow, Rect2(-22, -22, 44, 44), false, Color(1.0, 0.66, 0.18, 0.9 * fade))
draw_texture_rect(glow, Rect2(-8, -8, 16, 16), false, Color(1.0, 0.97, 0.82, fade))
draw_circle(Vector2.ZERO, 2.4 * flicker, Color(1.0, 0.99, 0.9, fade))
draw_line(Vector2(-15, 0), Vector2(15, 0), Color(1.0, 0.74, 0.32, fade * 0.34), 1.0, true)
draw_line(Vector2(0, -10), Vector2(0, 10), Color(1.0, 0.8, 0.4, fade * 0.22), 1.0, true)

View File

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

View File

@ -0,0 +1,61 @@
extends Node2D
const VFX := preload("res://scripts/game/extension_vfx.gd")
const MINE_TEXTURE := preload("res://assets/icons/extensions/mine_art.png")
const LIFETIME := 60.0
const ARM_TIME := 0.7
const TRIGGER_RADIUS := 34.0
var drift := Vector2.ZERO
var _age := 0.0
var _triggered := false
func _ready() -> void:
add_to_group("player_mines")
z_index = 12
func _physics_process(delta: float) -> void:
_age += delta
position += drift * delta
drift = drift.move_toward(Vector2.ZERO, 100.0 * delta)
if _age >= LIFETIME:
queue_free()
return
queue_redraw()
if _triggered or _age < ARM_TIME:
return
for enemy in get_tree().get_nodes_in_group("enemies"):
if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0:
continue
if global_position.distance_to(enemy.global_position) <= TRIGGER_RADIUS + float(enemy.call("get_visual_size")) * 0.35:
_triggered = true
enemy.call("apply_mine_hit")
var explosion := Node2D.new()
explosion.set_script(preload("res://scripts/game/deployment_flash.gd"))
explosion.set("color", Color("ffbc75"))
explosion.set("blast", true)
explosion.position = position
get_parent().add_child(explosion)
queue_free()
return
func _draw() -> void:
var armed := _age >= ARM_TIME
var deployment := smoothstep(0.0, 0.35, _age)
var fade := clampf(LIFETIME - _age, 0.0, 1.0)
var accent := Color("ff9957") if armed else Color("8ad7e7")
var pulse := 0.65 + 0.35 * sin(_age * 5.0)
var glow_radius := 28.0 + pulse * 4.0
draw_texture_rect(VFX.radial_texture(), Rect2(-glow_radius, -glow_radius, glow_radius * 2, glow_radius * 2), false, Color(accent, 0.22 * fade))
if armed:
for segment in 3:
var angle := _age * 0.35 + float(segment) * TAU / 3.0
draw_arc(Vector2.ZERO, 27.0, angle, angle + 0.6, 16, Color(accent, 0.22 * fade), 1.0, true)
else:
draw_arc(Vector2.ZERO, 23.0, -PI * 0.5, -PI * 0.5 + TAU * clampf(_age / ARM_TIME, 0.0, 1.0), 48, Color(accent, fade * 0.7), 1.5, true)
var scale_factor := lerpf(0.35, 1.0, deployment)
var spin := (1.0 - deployment) * -0.85
draw_set_transform(Vector2.ZERO, spin, Vector2.ONE * scale_factor)
draw_texture_rect(MINE_TEXTURE, Rect2(-21, -21, 42, 42), false, Color(1, 1, 1, fade))
draw_set_transform(Vector2.ZERO)
var sensor_radius := 7.0 + pulse * 2.0
draw_texture_rect(VFX.radial_texture(), Rect2(-sensor_radius, -sensor_radius, sensor_radius * 2, sensor_radius * 2), false, Color(accent, (0.55 + pulse * 0.35) * fade))

View File

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

View File

@ -0,0 +1,36 @@
extends Node2D
const VFX := preload("res://scripts/game/extension_vfx.gd")
var color := Color("ffb15c")
var direction := Vector2.DOWN
var blast := false
var _age := 0.0
var _particles: Array[Dictionary] = []
func _ready() -> void:
z_index = 26
var rng := RandomNumberGenerator.new()
rng.randomize()
for index in (28 if blast else 14):
var ray := Vector2.RIGHT.rotated(rng.randf_range(0, TAU)) if blast else direction.rotated(rng.randf_range(-1.0, 1.0))
_particles.append({"velocity": ray * rng.randf_range(45, 155), "size": rng.randf_range(1.0, 2.3)})
func _process(delta: float) -> void:
_age += delta
queue_redraw()
if _age >= (0.8 if blast else 0.4):
queue_free()
func _draw() -> void:
var life := 0.8 if blast else 0.4
var progress := clampf(_age / life, 0, 1)
var fade := pow(1.0 - progress, 2)
var radius := (65.0 if blast else 35.0) * (0.4 + progress)
draw_texture_rect(VFX.radial_texture(), Rect2(-radius, -radius, radius * 2, radius * 2), false, Color(color, fade * 0.9))
if blast:
draw_arc(Vector2.ZERO, 8.0 + progress * 80.0, 0, TAU, 64, Color(color, fade * 0.6), 2.0, true)
draw_arc(Vector2.ZERO, 5.0 + progress * 55.0, 0, TAU, 64, Color("fff2d3", fade * 0.35), 1.0, true)
for particle in _particles:
var velocity: Vector2 = particle["velocity"]
var point := velocity * _age * (1.0 - progress * 0.35)
draw_line(point, point - velocity.normalized() * (3.0 + 7.0 * fade), Color(color, fade), float(particle["size"]), true)

View File

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

View File

@ -48,7 +48,7 @@ func _hit_target(target: Variant) -> void:
effect.global_position = global_position
get_parent().add_child(effect)
effect.call("configure", false, Color(1.0, 0.32, 0.38))
if target is Node and target.has_method("take_damage"):
if target is Node and target.has_method("take_damage") and not target.is_in_group("asteroids"):
if target is CharacterBody2D:
target.call("take_damage", damage, false, DamageTypes.Type.ENERGY)
else:

View File

@ -33,6 +33,8 @@ var _player: CharacterBody2D
var _activated := false
var _sprite: Sprite2D
var _weapon_timer := 0.0
var _slow_remaining := 0.0
var _combat_target: Node2D
func _ready() -> void:
hull = max_hull
@ -89,20 +91,26 @@ func get_visual_size() -> float:
return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0)
func _physics_process(delta: float) -> void:
_slow_remaining = maxf(0.0, _slow_remaining - delta)
_weapon_timer = maxf(0.0, _weapon_timer - delta)
if not is_instance_valid(_player) or bool(_player.get("_is_destroyed")):
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
move_and_slide()
return
var to_player := _player.global_position - global_position
_combat_target = select_combat_target()
if _combat_target == null:
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
move_and_slide()
return
var to_player := _combat_target.global_position - global_position
if to_player.length() <= activation_range:
_activated = true
if _activated and to_player.length_squared() > 1.0:
var direction := to_player.normalized()
var desired_rotation := direction.angle() + PI * 0.5
rotation = rotate_toward(rotation, desired_rotation, turn_speed * delta)
velocity = velocity.move_toward(direction * approach_speed, approach_acceleration * delta)
_weapon_timer = maxf(0.0, _weapon_timer - delta)
velocity = velocity.move_toward(direction * approach_speed * get_slow_multiplier(), approach_acceleration * get_slow_multiplier() * delta)
if to_player.length() <= weapon_range and _weapon_timer <= 0.0:
_fire_plasma(direction)
_weapon_timer = get_fire_interval()
@ -112,6 +120,32 @@ func _physics_process(delta: float) -> void:
move_and_slide()
_check_player_collision()
func get_slow_multiplier() -> float:
return 0.5 if _slow_remaining > 0.0 else 1.0
func select_combat_target() -> Node2D:
var closest: Node2D
var closest_distance := INF
for flare in get_tree().get_nodes_in_group("player_flares"):
if flare.is_queued_for_deletion():
continue
var distance := global_position.distance_to(flare.global_position)
if distance <= maxf(activation_range, weapon_range) and distance < closest_distance:
closest = flare
closest_distance = distance
if closest != null:
return closest
if is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked")):
return _player
return null
func apply_mine_hit() -> void:
if is_queued_for_deletion() or hull <= 0.0:
return
_slow_remaining = 5.0
velocity = velocity.limit_length(approach_speed * 0.5)
take_damage(max_hull * 0.05, false, DamageTypes.Type.KINETIC, true)
func _check_player_collision() -> void:
for collision_index in get_slide_collision_count():
var collision := get_slide_collision(collision_index)
@ -122,7 +156,7 @@ func _fire_plasma(direction: Vector2) -> void:
var muzzle := global_position + direction * (get_visual_size() * 0.5 + 8.0)
if weapon_kind == ItemDefinition.WeaponKind.LASER:
var endpoint := muzzle + _laser_aim_direction(direction) * weapon_range
var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, 3, [get_rid()])
var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, 51, [get_rid()])
var hit := get_world_2d().direct_space_state.intersect_ray(query)
if not hit.is_empty():
endpoint = hit["position"]
@ -140,7 +174,7 @@ func _fire_plasma(direction: Vector2) -> void:
else:
var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate()
projectile.enemy_shot = true
projectile.collision_mask = 3
projectile.collision_mask = 51
projectile.missile_homing = false
var speed := 430.0 if weapon_kind == ItemDefinition.WeaponKind.GAMMA else (950.0 if weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN else 600.0)
projectile.configure(muzzle, direction, weapon_kind, speed, weapon_damage, weapon_range)
@ -155,10 +189,10 @@ func take_weapon_hit(amount: float, critical: bool, damage_type: int = DamageTyp
if is_instance_valid(_player):
_player.call("restore_shields_from_damage", maxf(0.0, before - hull - shields))
func take_damage(amount: float, critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC) -> void:
func take_damage(amount: float, critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC, bypass_absorption: bool = false) -> void:
if is_queued_for_deletion() or hull <= 0.0:
return
var actual := maxf(0.0, amount) * (1.0 - absorption[damage_type] / 100.0)
var actual := maxf(0.0, amount) * (1.0 if bypass_absorption else 1.0 - absorption[damage_type] / 100.0)
var shield_damage := minf(shields, actual)
shields -= shield_damage
hull = maxf(0.0, hull - actual + shield_damage)
@ -181,7 +215,7 @@ func get_fire_interval() -> float:
func _laser_aim_direction(direction: Vector2) -> Vector2:
if randf() < LASER_MISS_CHANCE:
var side := Vector2(-direction.y, direction.x)
var distance := global_position.distance_to(_player.global_position) if is_instance_valid(_player) else weapon_range
var distance := global_position.distance_to(_combat_target.global_position) if is_instance_valid(_combat_target) else weapon_range
var offset := randf_range(60.0, 110.0) * (-1.0 if randf() < 0.5 else 1.0)
return (direction * maxf(1.0, distance) + side * offset).normalized()
return direction.rotated(randf_range(-0.02, 0.02))
@ -216,7 +250,7 @@ func _drop_loot() -> void:
return
var pickup := Node2D.new()
pickup.set_script(LOOT_PICKUP)
pickup.set("item", ITEM_GENERATOR.generate(GameState.system_level, loot_tier))
pickup.set("item", ITEM_GENERATOR.generate_loot(GameState.system_level, loot_tier, null, float(_player.get("luck_percent")) if is_instance_valid(_player) else 0.0))
pickup.position = position + LOOT_OFFSET
get_parent().call_deferred("add_child", pickup)

View File

@ -0,0 +1,23 @@
extends Node2D
var color := Color("9be7ff")
var radius := 50.0
var _time := 0.0
func _ready() -> void:
z_index = 28
func _process(delta: float) -> void:
_time += delta
queue_redraw()
if _time >= 0.7:
queue_free()
func _draw() -> void:
var progress := _time / 0.7
var alpha := 1.0 - progress
for ring in 3:
draw_arc(Vector2.ZERO, 8.0 + radius * progress + float(ring) * 5.0, 0, TAU, 64, Color(color, alpha * (0.5 - float(ring) * 0.12)), 2.0, true)
for index in 12:
var direction := Vector2.RIGHT.rotated(float(index) * TAU / 12.0)
draw_line(direction * (10.0 + radius * progress), direction * (20.0 + radius * progress), Color(color, alpha * 0.6), 1.5, true)

View File

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

View File

@ -0,0 +1,160 @@
extends Node
const WORLD_SIZE := preload("res://scripts/levels/system_generator.gd").WORLD_SIZE
const PULSE := preload("res://scripts/game/extension_pulse.gd")
var cooldowns: Dictionary = {}
var cloak_remaining := 0.0
var solar_remaining := 0.0
var _cloak_material: ShaderMaterial
var _old_material: Material
var _shadow_was_visible := false
var _visual_colors: Dictionary = {}
var _destination := Vector2.ZERO
var _has_destination := false
@onready var ship: CharacterBody2D = get_parent()
func reset_combat_state() -> void:
cooldowns.clear()
solar_remaining = 0.0
if cloak_remaining > 0.0:
_end_cloak()
func _physics_process(delta: float) -> void:
for id in cooldowns.keys():
cooldowns[id] = maxf(0.0, float(cooldowns[id]) - delta)
if cloak_remaining > 0.0:
cloak_remaining = maxf(0.0, cloak_remaining - delta)
if _cloak_material != null:
_cloak_material.set_shader_parameter("elapsed", 10.0 - cloak_remaining)
if cloak_remaining <= 0.0 or bool(ship.get("_is_destroyed")):
_end_cloak()
if solar_remaining > 0.0:
var available_time := minf(delta, solar_remaining)
solar_remaining = maxf(0.0, solar_remaining - delta)
if bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")) or float(ship.call("get_solar_regeneration")) <= 0.0 or GameState.captain.is_skill_active("power_redirect"):
solar_remaining = 0.0
return
var maximum := float(ship.get("max_shields"))
ship.set("shields", minf(maximum, float(ship.get("shields")) + maximum * available_time / 5.0))
ship.call("_update_shield_visual")
ship.get_node("ShieldVisual").call("flash_hit")
if float(ship.get("shields")) >= maximum:
solar_remaining = 0.0
func _safe_destination(point: Vector2) -> bool:
var query := PhysicsShapeQueryParameters2D.new()
var shape := CircleShape2D.new()
shape.radius = 65.0
query.shape = shape
query.transform = Transform2D(0.0, point)
query.collision_mask = 5
query.exclude = [ship.get_rid()]
return ship.get_world_2d().direct_space_state.intersect_shape(query, 1).is_empty()
func has_teleport_destination() -> bool:
if _has_destination and _destination.distance_to(ship.global_position) >= 400.0 and _safe_destination(_destination):
return true
var half := WORLD_SIZE * 0.5 - Vector2(160, 160)
for attempt in 48:
var candidate := Vector2(randf_range(-half.x, half.x), randf_range(-half.y, half.y))
if candidate.distance_to(ship.global_position) >= 400.0 and _safe_destination(candidate):
_destination = candidate
_has_destination = true
return true
_has_destination = false
return false
func activate(kind: int) -> void:
match kind:
0:
if not has_teleport_destination():
return
_teleport_effect(ship.global_position, false)
ship.global_position = _destination
_has_destination = false
ship.velocity = Vector2.ZERO
ship.set("angular_velocity", 0.0)
ship.set("_boost_target", 0.0)
ship.set("_boost_speed", 0.0)
ship.set("_mini_boost_coast_speed", 0.0)
ship.set("energy", 0.0)
ship.reset_physics_interpolation()
ship.get_node("Camera2D").reset_smoothing()
_teleport_effect(ship.global_position, true)
1: _start_cloak()
2:
var mine := Node2D.new()
mine.set_script(preload("res://scripts/game/deployed_mine.gd"))
var drop_direction := Vector2.DOWN.rotated(ship.rotation)
mine.position = ship.position + drop_direction * 42.0
mine.set("drift", drop_direction * 36.0)
_deployment_effect(mine.position, drop_direction, Color("8ad7e7"))
ship.get_parent().add_child(mine)
3:
_deployment_effect(ship.position + Vector2.DOWN.rotated(ship.rotation) * 28.0, Vector2.DOWN.rotated(ship.rotation), Color("ffb15c"))
for index in 3:
var flare := StaticBody2D.new()
flare.set_script(preload("res://scripts/game/deployed_flare.gd"))
var direction := Vector2.DOWN.rotated(ship.rotation + (float(index) - 1.0) * 0.8)
flare.position = ship.position + direction * 48.0
flare.set("drift", direction * 90.0 + ship.velocity * 0.25)
ship.get_parent().add_child(flare)
4:
solar_remaining = 5.0
_pulse(ship.global_position, Color("70dfff"), 50.0)
func _teleport_effect(point: Vector2, arriving: bool) -> void:
var effect := Node2D.new()
effect.set_script(preload("res://scripts/game/teleport_vortex.gd"))
effect.position = point
effect.set("arriving", arriving)
effect.set("ship_texture", ship.get_node("Sprite2D").get("texture"))
effect.set("ship_rotation", ship.rotation)
ship.get_parent().add_child(effect)
func _deployment_effect(point: Vector2, direction: Vector2, color: Color) -> void:
var effect := Node2D.new()
effect.set_script(preload("res://scripts/game/deployment_flash.gd"))
effect.position = point
effect.set("direction", direction)
effect.set("color", color)
ship.get_parent().add_child(effect)
func _start_cloak() -> void:
cloak_remaining = 10.0
var sprite := ship.get_node("Sprite2D") as Sprite2D
_old_material = sprite.material
_cloak_material = ShaderMaterial.new()
_cloak_material.shader = preload("res://resources/ui/cloak.gdshader")
sprite.material = _cloak_material
var shadow := sprite.get_node_or_null("ShipShadow") as CanvasItem
if shadow != null:
_shadow_was_visible = shadow.visible
shadow.hide()
for name in ["ShieldVisual", "EngineVisual", "SolarEnergyEffect", "EnergyIndicator"]:
var visual := ship.get_node_or_null(name) as CanvasItem
if visual != null:
_visual_colors[name] = visual.modulate
visual.modulate = Color(visual.modulate, 0.08)
_pulse(ship.global_position, Color("81e4ef"), 40.0)
func _end_cloak() -> void:
cloak_remaining = 0.0
ship.get_node("Sprite2D").set("material", _old_material)
_cloak_material = null
var shadow := ship.get_node_or_null("Sprite2D/ShipShadow") as CanvasItem
if shadow != null:
shadow.visible = _shadow_was_visible
for name in _visual_colors:
var visual := ship.get_node_or_null(NodePath(name)) as CanvasItem
if visual != null:
visual.modulate = _visual_colors[name]
_visual_colors.clear()
func _pulse(point: Vector2, color: Color, radius: float) -> void:
var pulse := Node2D.new()
pulse.set_script(PULSE)
pulse.position = point
pulse.set("color", color)
pulse.set("radius", radius)
ship.get_parent().add_child(pulse)

View File

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

View File

@ -0,0 +1,17 @@
extends RefCounted
static var _radial: GradientTexture2D
static func radial_texture() -> GradientTexture2D:
if _radial == null:
var gradient := Gradient.new()
gradient.offsets = PackedFloat32Array([0.0, 0.12, 0.35, 0.7, 1.0])
gradient.colors = PackedColorArray([Color(1, 1, 1, 1), Color(1, 1, 1, 0.85), Color(1, 1, 1, 0.3), Color(1, 1, 1, 0.04), Color(1, 1, 1, 0)])
_radial = GradientTexture2D.new()
_radial.gradient = gradient
_radial.width = 128
_radial.height = 128
_radial.fill = GradientTexture2D.FILL_RADIAL
_radial.fill_from = Vector2(0.5, 0.5)
_radial.fill_to = Vector2(1.0, 0.5)
return _radial

View File

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

View File

@ -1,5 +1,7 @@
extends CanvasLayer
const RENDER_LAYERS := preload("res://scripts/ui/render_layers.gd")
@onready var debug_toggle: Button = $HUDRoot/DebugToggle
@onready var debug_info: Label = $HUDRoot/DebugInfo
@onready var player: CharacterBody2D = get_parent().get_node("PlayerShip")
@ -32,11 +34,11 @@ func _create_navigation() -> void:
navigation.name = "TopNavigation"
$HUDRoot.add_child(navigation)
navigation.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
navigation.offset_left = -60
navigation.offset_right = 60
navigation.offset_left = -70
navigation.offset_right = 70
navigation.offset_top = 12
navigation.offset_bottom = 64
navigation.add_theme_constant_override("separation", 8)
navigation.offset_bottom = 92
navigation.add_theme_constant_override("separation", 10)
_crafting_window = _create_window("CraftingWindow", "WARSZTAT", Vector2(704, 420))
var crafting := PanelContainer.new()
crafting.name = "CraftingPanel"
@ -47,20 +49,11 @@ func _create_navigation() -> void:
_captain_skills_ui.set_script(preload("res://scripts/ui/captain_skills_ui.gd"))
_captain_skills_ui.set("player", player)
add_child(_captain_skills_ui)
for entry in [["Twórz", _crafting_window, "res://assets/icons/ui/crafting.svg", Color("efb568")], ["Umiejętności kapitana", null, "res://assets/icons/ui/captain_skills.svg", Color("b9a0ef")]]:
for entry in [["Twórz", _crafting_window, "res://assets/icons/ui/crafting_art.png", Color("efb568")], ["Umiejętności kapitana", null, "res://assets/icons/ui/captain_skills_art.png", Color("b9a0ef")]]:
var button := Button.new()
button.tooltip_text = entry[0]
button.icon = load(entry[2]) as Texture2D
button.expand_icon = true
button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
button.custom_minimum_size = Vector2(56, 52)
button.add_theme_constant_override("icon_max_width", 36)
preload("res://scripts/ui/hud_button_style.gd").apply(button, entry[3])
button.add_theme_constant_override("h_separation", 0)
for state in ["normal", "hover", "pressed", "disabled"]:
var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat
style.set_content_margin_all(8)
button.add_theme_stylebox_override(state, style)
button.set_script(preload("res://scripts/ui/navigation_button.gd"))
button.call("configure_navigation", load(entry[2]) as Texture2D, entry[3], "TWÓRZ" if entry[1] != null else "KAPITAN")
navigation.add_child(button)
if entry[1] == null:
button.name = "OpenCaptainSkills"
@ -74,9 +67,9 @@ func _create_navigation() -> void:
func _create_window(node_name: String, title: String, dimensions: Vector2) -> PanelContainer:
var window := PanelContainer.new()
window.name = node_name
window.z_index = 100
window.z_index = RENDER_LAYERS.WINDOW_Z
var style := StyleBoxFlat.new()
style.bg_color = Color("0b1520fa")
style.bg_color = Color("0b1520")
style.border_color = Color("376779")
style.set_border_width_all(1)
style.set_corner_radius_all(8)
@ -115,6 +108,7 @@ func _toggle_window(window: PanelContainer) -> void:
window.visible = opening
func _ready() -> void:
layer = RENDER_LAYERS.HUD
_sidebar_open = bool(GameState.hud_panel_state.get("ship_open", false))
_debug_expanded = bool(GameState.hud_panel_state.get("debug_expanded", false))
debug_info.visible = _debug_expanded
@ -156,7 +150,7 @@ func _create_sidebar() -> void:
_captain_panel = $HUDRoot/CaptainPanel
_captain_panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
_captain_panel.position = Vector2(12, 12)
_captain_panel.size = Vector2(320, _captain_panel.get_combined_minimum_size().y)
_captain_panel.size = Vector2(244, _captain_panel.get_combined_minimum_size().y)
_captain_panel.resized.connect(_layout_debug)
_passive_skills = VBoxContainer.new()
_passive_skills.name = "PassiveCaptainSkills"
@ -193,7 +187,7 @@ func _create_sidebar() -> void:
_sidebar = PanelContainer.new()
_sidebar.name = "ShipDrawer"
var style := StyleBoxFlat.new()
style.bg_color = Color(0.025, 0.042, 0.064, 0.97)
style.bg_color = Color(0.025, 0.042, 0.064, 1.0)
style.border_color = Color("376779")
style.set_border_width_all(1)
style.set_corner_radius_all(8)
@ -427,7 +421,8 @@ Energia: %+.1f pkt
Prędkość maks.: %+.1f%%
Skrócenie cooldownu rakiet: %.2f s
Doświadczenie: %+.1f%%
Kredyty: %+.1f%%""".replace("\r", "") % [
Kredyty: %+.1f%%
Szczęście: %.1f%% / 60%%""".replace("\r", "") % [
(float(player.call("get_weapon_damage_multiplier")) - 1.0) * 100.0,
float(player.call("get_critical_chance")) * 100.0,
float(player.get("critical_damage_bonus")) * 100.0,
@ -443,6 +438,7 @@ Kredyty: %+.1f%%""".replace("\r", "") % [
cooldown_bonus,
(float(player.get("experience_multiplier")) - 1.0) * 100.0,
(float(player.get("credits_multiplier")) - 1.0) * 100.0,
float(player.call("get_luck_chance")) * 100.0,
]
func _create_ability_bar() -> void:
@ -476,6 +472,17 @@ func _create_ability_bar() -> void:
button.add_child(shortcut)
button.add_theme_font_size_override("font_size", 11)
button.pressed.connect(func() -> void: player.call("activate_ability", index))
var timer := Label.new()
timer.name = "Cooldown"
timer.mouse_filter = Control.MOUSE_FILTER_IGNORE
timer.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
timer.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
timer.add_theme_font_size_override("font_size", 13)
timer.add_theme_font_override("font", ThemeDB.fallback_font)
timer.add_theme_color_override("font_outline_color", Color("030810"))
timer.add_theme_constant_override("outline_size", 4)
button.add_child(timer)
timer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_ability_bar.add_child(button)
_ability_buttons[index] = button
_update_ability_bar()
@ -493,7 +500,9 @@ func _update_ability_bar() -> void:
button.disabled = not reason.is_empty()
var item: ItemInstance = player.get_node("ShipInventory").get_item("extension", index)
button.call("configure", item.definition.icon, Color("91dbea"))
button.call("set_visual_state", 0.0, false, not reason.is_empty())
var cooldown := float(player.call("get_ability_cooldown", index))
button.call("set_visual_state", cooldown / maxf(0.01, ability.cooldown), false, not reason.is_empty())
(button.get_node("Cooldown") as Label).text = str(ceili(cooldown)) if cooldown > 0.0 else ""
button.tooltip_text = "[%d] %s\n" % [index + 1, ability.display_name] + ability.description + ("\n" + reason if not reason.is_empty() else "\nGotowe")
_ability_bar.get_parent().visible = any_equipped

View File

@ -8,6 +8,7 @@ var restart_inventory: Dictionary = {}
var system_level: int = 1
var system_seed: int = 0
var explored_pois: Dictionary = {}
var exploration_wreck_bonuses: Dictionary = {}
var hud_panel_state: Dictionary = {}
func create_system_seed() -> int:
@ -15,12 +16,20 @@ func create_system_seed() -> int:
rng.randomize()
return maxi(1, rng.randi())
func reset_current_system_exploration() -> void:
# Retrying after death starts a fresh attempt at this board while keeping rewards.
var prefix := "%d:" % system_seed
for id in explored_pois.keys():
if str(id).begins_with(prefix):
explored_pois.erase(id)
func start_new_run() -> void:
captain = CAPTAIN_PROGRESS.new()
system_level = 1
system_seed = create_system_seed()
restart_inventory.clear()
explored_pois.clear()
exploration_wreck_bonuses.clear()
hud_panel_state.clear()
func _ready() -> void:

View File

@ -14,8 +14,8 @@ const DESTRUCTION_EFFECT := preload("res://scripts/game/player_destruction_effec
@export var reverse_max_speed := 210.0
@export var max_strafe_speed := 210.0
@export var thrust_acceleration := 90.0
@export var brake_acceleration := 540.0
@export var space_drag := 24.0
@export var brake_acceleration := 180.0
@export var space_drag := 16.0
@export var max_turn_speed := 2.8
@export var turn_acceleration := 7.0
@export var turn_damping := 4.0
@ -40,6 +40,7 @@ var absorption := Vector3.ZERO
var damage_multiplier := 1.0
var experience_multiplier := 1.0
var credits_multiplier := 1.0
var luck_percent := 0.0
var shield_on_damage_percent := 0.0
var critical_chance_bonus := 0.0
var critical_damage_bonus := 0.0
@ -63,6 +64,7 @@ var _mini_boost_coast_speed := 0.0
@onready var _shield_visual: Node2D = $ShieldVisual
func _enter_tree() -> void:
GameState.captain.reset_combat_state()
# Set the chassis before inventory children calculate equipment bonuses.
var chassis := SHIP_CATALOG.for_path(GameState.selected_ship_path)
base_max_hull = float(chassis["hull"])
@ -80,6 +82,7 @@ func apply_equipment_bonuses(bonuses: Dictionary) -> void:
damage_multiplier = 1.0 + float(bonuses.get("damage_percent", 0.0)) / 100.0
experience_multiplier = 1.0 + float(bonuses.get("experience_percent", 0.0)) / 100.0
credits_multiplier = 1.0 + float(bonuses.get("credits_percent", 0.0)) / 100.0
luck_percent = maxf(0.0, float(bonuses.get("luck_percent", 0.0)))
shield_on_damage_percent = maxf(0.0, float(bonuses.get("shield_on_damage_percent", 0.0)))
critical_chance_bonus = float(bonuses.get("critical_chance_percent", 0.0)) / 100.0
critical_damage_bonus = float(bonuses.get("critical_damage_percent", 0.0)) / 100.0
@ -93,6 +96,11 @@ func apply_equipment_bonuses(bonuses: Dictionary) -> void:
_update_shield_visual()
func _ready() -> void:
var extension_systems := Node.new()
extension_systems.name = "ExtensionSystems"
extension_systems.set_script(preload("res://scripts/game/extension_systems.gd"))
extension_systems.set("cooldowns", $ShipInventory.extension_cooldowns)
add_child(extension_systems)
for index in ShipInventory.EXTENSION_SLOT_COUNT:
_ensure_input_action(StringName("ability_%d" % (index + 1)), (KEY_1 + index) as Key)
_ensure_input_action(&"use_energy_bank", KEY_R)
@ -106,7 +114,7 @@ func _ready() -> void:
var scanner_layer := CanvasLayer.new()
scanner_layer.name = "ScannerOverlay"
scanner_layer.layer = 8
scanner_layer.layer = preload("res://scripts/ui/render_layers.gd").SCANNER
add_child(scanner_layer)
var scanner := Control.new()
scanner.set_script(preload("res://scripts/ui/scanner_overlay.gd"))
@ -220,6 +228,8 @@ func _destroy_ship() -> void:
if _is_destroyed:
return
_is_destroyed = true
$ExtensionSystems.reset_combat_state()
GameState.captain.reset_combat_state()
var death_velocity := velocity
velocity = Vector2.ZERO
collision_layer = 0
@ -255,6 +265,7 @@ func _show_defeat_screen() -> void:
func _restart_level_after_destruction() -> void:
if is_inside_tree():
$ShipInventory.preserve_for_restart()
GameState.reset_current_system_exploration()
get_tree().reload_current_scene.call_deferred()
func _physics_process(delta: float) -> void:
@ -391,16 +402,35 @@ func get_active_ability(slot_index: int) -> ActiveAbility:
var item: ItemInstance = $ShipInventory.get_item("extension", slot_index)
return item.definition.active_ability if item != null and item.definition != null else null
func get_luck_chance() -> float:
return GameState.captain.get_luck_chance(luck_percent)
func is_cloaked() -> bool:
var systems := get_node_or_null("ExtensionSystems")
return systems != null and float(systems.get("cloak_remaining")) > 0.0 and not _is_destroyed
func get_ability_cooldown(slot_index: int) -> float:
var ability := get_active_ability(slot_index)
if ability == null or ability.ability_id.is_empty():
return 0.0
return float($ExtensionSystems.cooldowns.get(ability.ability_id, 0.0))
func ability_unavailable_reason(slot_index: int) -> String:
if _is_destroyed or is_docked:
if _is_destroyed or is_docked or get_tree().paused:
return "Statek niedostępny"
var remaining := get_ability_cooldown(slot_index)
if remaining > 0.0:
return "Odnowienie: %d s" % ceili(remaining)
var ability := get_active_ability(slot_index)
return ability.unavailable_reason(self) if ability != null else "Brak rozszerzenia"
func activate_ability(slot_index: int) -> bool:
if not ability_unavailable_reason(slot_index).is_empty():
return false
get_active_ability(slot_index).activate(self)
var ability := get_active_ability(slot_index)
ability.activate(self)
if ability.cooldown > 0.0 and not ability.ability_id.is_empty():
$ExtensionSystems.cooldowns[ability.ability_id] = ability.cooldown
return true
func use_energy_bank() -> void:

View File

@ -0,0 +1,44 @@
extends Node2D
const VFX := preload("res://scripts/game/extension_vfx.gd")
var arriving := true
var ship_texture: Texture2D
var ship_rotation := 0.0
var _age := 0.0
var _shards: Array[Dictionary] = []
func _ready() -> void:
z_index = 27
var rng := RandomNumberGenerator.new()
rng.randomize()
for index in 28:
_shards.append({"angle": rng.randf_range(0, TAU), "distance": rng.randf_range(25, 85), "size": rng.randf_range(1, 3)})
func _process(delta: float) -> void:
_age += delta
queue_redraw()
if _age >= 1.0:
queue_free()
func _draw() -> void:
var progress := clampf(_age, 0, 1)
var fade := pow(1.0 - progress, 2)
var radius := 28.0 + (70.0 * (1.0 - progress) if arriving else progress * 70.0)
draw_texture_rect(VFX.radial_texture(), Rect2(-90, -90, 180, 180), false, Color(0.42, 0.35, 1.0, fade * 0.65))
for ring in 3:
var r := radius + float(ring) * 6.0
var color := Color(0.45 + float(ring) * 0.15, 0.55 + float(ring) * 0.1, 1.0, fade * 0.6)
for segment in 4:
var start := float(segment) * TAU / 4.0 + progress * (2.0 if ring % 2 == 0 else -2.0)
draw_arc(Vector2.ZERO, r, start, start + 1.05, 24, color, 1.5, true)
for shard in _shards:
var angle := float(shard["angle"]) + progress * 1.2
var distance := float(shard["distance"]) * (1.0 - progress if arriving else progress)
var point := Vector2.RIGHT.rotated(angle) * distance
draw_line(point, point + Vector2.RIGHT.rotated(angle + 0.7) * 6.0, Color(0.7, 0.85, 1.0, fade), float(shard["size"]), true)
if ship_texture != null:
draw_set_transform(Vector2.ZERO, ship_rotation, Vector2.ONE * (1.0 + progress * 0.6))
var dimensions := ship_texture.get_size()
dimensions *= 64.0 / maxf(dimensions.x, dimensions.y)
draw_texture_rect(ship_texture, Rect2(-dimensions * 0.5, dimensions), false, Color(0.5, 0.7, 1.0, fade * 0.45))
draw_set_transform(Vector2.ZERO)

View File

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

View File

@ -253,6 +253,9 @@ func _hit_target(target: Node) -> void:
_impact_handled = true
var is_explosion := weapon_kind == ItemDefinition.WeaponKind.MISSILE
_spawn_impact_effect(is_explosion)
if enemy_shot and target.is_in_group("asteroids"):
queue_free()
return
if weapon_kind == ItemDefinition.WeaponKind.MISSILE:
_explode_at_target(target)
else:
@ -283,6 +286,8 @@ func _explode_at_target(impact_target: Node) -> void:
func _apply_damage_once(target: Variant) -> void:
if not (target is Node):
return
if enemy_shot and target.is_in_group("asteroids"):
return
var instance_id: int = target.get_instance_id()
if _already_hit.has(instance_id):
return

View File

@ -1,6 +1,12 @@
extends RefCounted
const DEFINITIONS := [
preload("res://resources/items/teleport.tres"),
preload("res://resources/items/cloak.tres"),
preload("res://resources/items/mine_layer.tres"),
preload("res://resources/items/flares.tres"),
preload("res://resources/items/solar_shield_charge.tres"),
preload("res://resources/items/gamma_cannon.tres"),
preload("res://resources/items/plasma_cannon.tres"),
preload("res://resources/items/beam_laser.tres"),
@ -22,6 +28,10 @@ const AFFIX_DATABASE := preload("res://resources/items/affix_database.tres")
static func generate(level: int, enemy_tier: int = 0) -> ItemInstance:
return generate_for_definition(DEFINITIONS.pick_random(), level, enemy_tier)
static func generate_loot(level: int, enemy_tier: int = 0, definition: ItemDefinition = null, equipment_luck_percent: float = 0.0) -> ItemInstance:
var loot_level := GameState.captain.roll_loot_level(level, -1.0, equipment_luck_percent)
return generate_for_definition(definition if definition != null else DEFINITIONS.pick_random(), loot_level, enemy_tier)
static func generate_for_definition(definition: ItemDefinition, level: int, enemy_tier: int = 0) -> ItemInstance:
var item := ItemInstance.new()
item.definition = definition

View File

@ -135,4 +135,4 @@ func get_tooltip_text() -> String:
return "\n".join(lines)
static func stat_name(key: String) -> String:
return str({"kinetic_absorption": "Absorpcja kinetyczna", "energy_absorption": "Absorpcja energetyczna", "radiation_absorption": "Absorpcja radiacyjna", "missile_cooldown_reduction": "Skrócenie cooldownu rakiet", "scanner_range": "Zasięg skanera", "shield_on_damage_percent": "Odzysk tarcz z obrażeń", "damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key))
return str({"luck_percent": "Szczęście", "kinetic_absorption": "Absorpcja kinetyczna", "energy_absorption": "Absorpcja energetyczna", "radiation_absorption": "Absorpcja radiacyjna", "missile_cooldown_reduction": "Skrócenie cooldownu rakiet", "scanner_range": "Zasięg skanera", "shield_on_damage_percent": "Odzysk tarcz z obrażeń", "damage": "Obrażenia", "max_hull": "Kadłub", "max_shields": "Tarcze", "max_energy": "Energia", "max_speed": "Prędkość", "energy_regeneration": "Regeneracja energii", "shield_regeneration": "Regeneracja tarcz", "hull_percent": "Kadłub", "shield_percent": "Tarcze", "damage_percent": "Obrażenia broni", "speed_percent": "Prędkość statku", "experience_percent": "Doświadczenie (EXP)", "credits_percent": "Kredyty", "critical_chance_percent": "Szansa na trafienie krytyczne", "critical_damage_percent": "Obrażenia krytyczne"}.get(key, key))

View File

@ -19,6 +19,7 @@ const AMMUNITION_CRAFT_AMOUNT := 10
const AMMUNITION_BUY_AMOUNT := 100
const AMMUNITION_BUY_BASE_COST := 20
var extension_cooldowns: Dictionary = {}
var energy_banks := 0
var irradiated_ammunition := 0
@ -287,7 +288,7 @@ func _recalculate_ship_stats() -> void:
for item in weapon_slots + module_slots:
if item == null or item.definition == null:
continue
for stat_key in ["kinetic_absorption", "energy_absorption", "radiation_absorption", "shield_on_damage_percent", "scanner_range", "max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent", "critical_chance_percent", "critical_damage_percent"]:
for stat_key in ["kinetic_absorption", "energy_absorption", "radiation_absorption", "shield_on_damage_percent", "scanner_range", "max_hull", "max_shields", "max_energy", "max_speed", "energy_regeneration", "shield_regeneration", "hull_percent", "shield_percent", "damage_percent", "speed_percent", "experience_percent", "credits_percent", "luck_percent", "critical_chance_percent", "critical_damage_percent"]:
bonuses[stat_key] = float(bonuses.get(stat_key, 0.0)) + item.get_stat_total(stat_key)
ship.call("apply_equipment_bonuses", bonuses)
@ -344,7 +345,7 @@ func _comparison_bonuses(incoming: ItemInstance = null, outgoing: ItemInstance =
return bonuses
func _add_comparison_bonuses(bonuses: Dictionary, item: ItemInstance) -> void:
for key in ["kinetic_absorption", "energy_absorption", "radiation_absorption", "max_hull", "max_shields", "max_energy", "max_speed", "scanner_range", "hull_percent", "shield_percent", "speed_percent", "damage_percent", "critical_damage_percent", "critical_chance_percent", "shield_on_damage_percent", "energy_regeneration", "shield_regeneration", "missile_cooldown_reduction", "experience_percent", "credits_percent"]:
for key in ["kinetic_absorption", "energy_absorption", "radiation_absorption", "max_hull", "max_shields", "max_energy", "max_speed", "scanner_range", "hull_percent", "shield_percent", "speed_percent", "damage_percent", "critical_damage_percent", "critical_chance_percent", "shield_on_damage_percent", "energy_regeneration", "shield_regeneration", "missile_cooldown_reduction", "experience_percent", "credits_percent", "luck_percent"]:
bonuses[key] = float(bonuses.get(key, 0.0)) + item.get_stat_total(key)
func _comparison_stats(bonuses: Dictionary) -> Dictionary:
@ -357,6 +358,7 @@ func _comparison_stats(bonuses: Dictionary) -> Dictionary:
# These stats are bonuses in their own right, not rewards or hit scenarios.
for data in [["kinetic_absorption", "Absorpcja kinetyczna"], ["energy_absorption", "Absorpcja energetyczna"], ["radiation_absorption", "Absorpcja radiacyjna"], ["damage_percent", "Obrażenia broni"], ["critical_damage_percent", "Obrażenia krytyczne"], ["critical_chance_percent", "Szansa na krytyk"], ["shield_on_damage_percent", "Odzysk tarcz z obrażeń"], ["experience_percent", "Doświadczenie"], ["credits_percent", "Kredyty"]]:
stats[data[0]] = {"value": float(bonuses.get(data[0], 0.0)), "name": data[1], "unit": "%"}
stats["luck_percent"] = {"value": GameState.captain.get_luck_chance(float(bonuses.get("luck_percent", 0.0))) * 100.0, "name": "Szczęście (limit 60%)", "unit": "%"}
stats["missile_cooldown_reduction"] = {"value": float(bonuses.get("missile_cooldown_reduction", 0.0)), "name": "Skrócenie cooldownu rakiet", "unit": "s"}
var absorption_values := DamageTypes.capped(Vector3(float(bonuses.get("kinetic_absorption", 0.0)), float(bonuses.get("energy_absorption", 0.0)), float(bonuses.get("radiation_absorption", 0.0))))
for index in 3:

View File

@ -3,6 +3,10 @@ extends RefCounted
const POI := preload("res://scripts/levels/exploration_poi.gd")
static func spawn(game: Node2D, data: Dictionary, player: CharacterBody2D) -> void:
# Snapshot once per system. Learning a rank later must not add wrecks on reload.
if not GameState.exploration_wreck_bonuses.has(GameState.system_seed):
GameState.exploration_wreck_bonuses[GameState.system_seed] = roundi(GameState.captain.get_skill_bonus("explorer"))
var wreck_bonus := int(GameState.exploration_wreck_bonuses[GameState.system_seed])
var rng := RandomNumberGenerator.new()
rng.seed = GameState.system_seed ^ 0x504f49
var occupied: Array[Vector2] = []
@ -39,6 +43,26 @@ static func spawn(game: Node2D, data: Dictionary, player: CharacterBody2D) -> vo
var planet := game.get_node("CelestialBodies/Planet%d" % (index + 1))
_add(game, player, "ruin", planet.position, 100, rng.randi(), float(planet.get("radius")) + 100.0)
# Separate stream and IDs preserve the base POIs, asteroid count and ruin chance.
var bonus_rng := RandomNumberGenerator.new()
bonus_rng.seed = GameState.system_seed ^ 0x4558504c
for index in wreck_bonus:
for attempt in 1000:
var candidate := Vector2(bonus_rng.randf_range(-2300, 2300), bonus_rng.randf_range(-1500, 1500))
if candidate.length() < 650 or candidate.distance_to(player.position) < 450:
continue
var clear := true
for planet in data["planets"]:
if candidate.distance_to(planet["position"]) < float(planet["radius"]) + 220:
clear = false
for point in occupied:
if candidate.distance_to(point) < 180:
clear = false
if clear:
occupied.append(candidate)
_add(game, player, "wreck", candidate, 1000 + index, bonus_rng.randi())
break
static func _add(game: Node2D, player: CharacterBody2D, kind: String, position: Vector2, index: int, reward_seed: int, approach_radius: float = 115.0) -> void:
var id := "%d:%d" % [GameState.system_seed, index]
if GameState.explored_pois.has(id):

View File

@ -1,7 +1,13 @@
extends StaticBody2D
const GENERATOR := preload("res://scripts/items/item_generator.gd")
const ASTEROID_TEXTURES := [preload("res://assets/asteroids/asteroid_rock.png"), preload("res://assets/asteroids/asteroid_metal.png"), preload("res://assets/asteroids/asteroid_ice.png")]
const ASTEROID_TEXTURES := {
"iron": preload("res://assets/icons/resources/iron.png"),
"titanium": preload("res://assets/icons/resources/titanium.png"),
"uranium": preload("res://assets/icons/resources/uranium.png"),
"aluminium": preload("res://assets/icons/resources/aluminium.png"),
"sulfur": preload("res://assets/icons/resources/sulfur.png"),
}
const WRECK_TEXTURES := [preload("res://assets/wrecks/wreck_frigate.png"), preload("res://assets/wrecks/wreck_freighter.png"), preload("res://assets/wrecks/wreck_interceptor.png")]
const LOOT := preload("res://scripts/items/loot_pickup.gd")
const ASTEROID_DESTRUCTION := preload("res://scripts/game/asteroid_destruction_effect.gd")
@ -25,9 +31,10 @@ func _ready() -> void:
_rng.seed = reward_seed
_mineral = ["iron", "aluminium", "titanium", "uranium", "sulfur"][_rng.randi_range(0, 4)]
hull = 20.0 * reward_scale()
collision_layer = 4 if kind == "asteroid" else 0
collision_layer = (4 | 32) if kind == "asteroid" else 0
collision_mask = 0
if kind == "asteroid":
add_to_group("asteroids")
var shape := CollisionShape2D.new()
var circle := CircleShape2D.new()
circle.radius = 30
@ -37,7 +44,7 @@ func _ready() -> void:
_points.append(Vector2.RIGHT.rotated(TAU * index / 10) * _rng.randf_range(23, 35))
_asteroid_sprite = Sprite2D.new()
_asteroid_sprite.name = "AsteroidSprite"
_asteroid_sprite.texture = ASTEROID_TEXTURES[posmod(reward_seed, ASTEROID_TEXTURES.size())]
_asteroid_sprite.texture = ASTEROID_TEXTURES[_mineral]
var texture_size := _asteroid_sprite.texture.get_size()
_asteroid_sprite.scale = Vector2.ONE * (78.0 / maxf(texture_size.x, texture_size.y))
_asteroid_sprite.rotation = deg_to_rad(posmod(reward_seed, 360))
@ -80,7 +87,8 @@ func _update_label(near: bool) -> void:
if kind == "ruin":
_label.text = "?"
elif kind == "asteroid":
_label.text = "Asteroida · %s\nHP: %.0f" % [{"iron": "Żelazo", "aluminium": "Aluminium", "titanium": "Tytan", "uranium": "Uran", "sulfur": "Siarka"}[_mineral], hull]
_label.text = ""
_label.hide()
else:
_label.text = ("Wrak statku" if kind == "wreck" else "Antyczne znalezisko") + ("\n[E] Przeszukaj" if near else "\nPodleć, aby przeszukać")
@ -160,7 +168,7 @@ func claim() -> bool:
func _show_discovery(reward: String) -> void:
var layer := CanvasLayer.new()
layer.layer = 30
layer.layer = preload("res://scripts/ui/render_layers.gd").DISCOVERY
layer.name = "AncientDiscoveryMessage"
get_parent().add_child(layer)
var label := Label.new()
@ -193,7 +201,7 @@ func _drop_module(ancient: bool) -> ItemInstance:
for definition in GENERATOR.DEFINITIONS:
if definition.category != ItemDefinition.Category.WEAPON:
definitions.append(definition)
var item := GENERATOR.generate_for_definition(definitions[_rng.randi_range(0, definitions.size() - 1)], mini(1000, GameState.system_level + (1 if ancient else 0)), 2 if ancient else 0)
var item := GENERATOR.generate_loot(mini(1000, GameState.system_level + (1 if ancient else 0)), 2 if ancient else 0, definitions[_rng.randi_range(0, definitions.size() - 1)], float(player.get("luck_percent")))
if ancient:
for modifier in item.additional_modifiers:
var bounds := modifier.affix.get_value_range(item.item_level)

View File

@ -14,6 +14,10 @@ func _ready() -> void:
_captain.normalize_skill_ranks()
_captain.changed.connect(_refresh)
player.get_node("ShipInventory").connect("inventory_changed", _refresh)
for label in [level_label, xp_label, credits_label]:
label.add_theme_color_override("font_shadow_color", Color("030811"))
label.add_theme_constant_override("shadow_offset_x", 1)
label.add_theme_constant_override("shadow_offset_y", 1)
_refresh()
func _refresh() -> void:
@ -24,17 +28,17 @@ func _refresh() -> void:
credits_label.text = "Kredyty: %d" % _captain.credits
func _build_portrait() -> void:
custom_minimum_size = Vector2(320, 0)
custom_minimum_size = Vector2(244, 0)
var style := StyleBoxFlat.new()
style.bg_color = Color(0.025, 0.042, 0.064, 0.96)
style.border_color = Color("376779")
style.bg_color = Color(0.025, 0.042, 0.064, 0.0)
style.border_color = Color("37677900")
style.set_border_width_all(1)
style.set_corner_radius_all(8)
style.set_content_margin_all(10)
style.set_content_margin_all(6)
add_theme_stylebox_override("panel", style)
var row := HBoxContainer.new()
row.name = "PortraitRow"
row.add_theme_constant_override("separation", 10)
row.add_theme_constant_override("separation", 8)
$Contents.add_child(row)
$Contents.move_child(row, 1)
var frame := PanelContainer.new()
@ -42,11 +46,12 @@ func _build_portrait() -> void:
var portrait_style := style.duplicate() as StyleBoxFlat
portrait_style.set_content_margin_all(2)
portrait_style.border_color = Color("daa85d")
portrait_style.bg_color = Color("0b1520")
frame.add_theme_stylebox_override("panel", portrait_style)
row.add_child(frame)
var portrait := TextureRect.new()
portrait.name = "CaptainAvatar"
portrait.custom_minimum_size = Vector2(76, 76)
portrait.custom_minimum_size = Vector2(54, 54)
portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE
@ -59,7 +64,7 @@ func _build_portrait() -> void:
row.add_child(info)
var designation := Label.new()
designation.text = "DOWÓDCA STATKU"
designation.add_theme_font_size_override("font_size", 11)
designation.add_theme_font_size_override("font_size", 9)
designation.add_theme_color_override("font_color", Color("91dbea"))
info.add_child(designation)
xp_label.reparent(info, false)

View File

@ -15,7 +15,7 @@ var _refresh_pending := false
func _ready() -> void:
name = "CaptainSkills"
layer = 40
layer = preload("res://scripts/ui/render_layers.gd").MANAGEMENT
process_mode = Node.PROCESS_MODE_ALWAYS
_captain = GameState.captain
_captain.normalize_equipped_skills()
@ -39,7 +39,7 @@ func _ready() -> void:
_bar = HBoxContainer.new()
_bar.name = "CaptainActiveSkills"
_bar.add_theme_constant_override("separation", 6)
add_child(_bar)
get_parent().get_node("HUDRoot").add_child(_bar)
_captain.changed.connect(_queue_refresh)
get_viewport().size_changed.connect(_layout)
_queue_refresh()
@ -307,6 +307,8 @@ func _process(_delta: float) -> void:
_bar.visible = not bool(player.get("_is_destroyed")) and not _screen.visible
func _layout() -> void:
if not is_inside_tree():
return
var viewport_size := get_viewport().get_visible_rect().size
var base_size := _panel.get_combined_minimum_size().max(Vector2(884, 180))
var ui_scale := minf(1.0, minf((viewport_size.x - 32) / base_size.x, (viewport_size.y - 32) / base_size.y))

View File

@ -4,7 +4,7 @@ var ship: CharacterBody2D
var _retrying := false
func _ready() -> void:
layer = 40
layer = preload("res://scripts/ui/render_layers.gd").DEFEAT
name = "DefeatOverlay"
var root := Control.new()
add_child(root)

View File

@ -35,13 +35,14 @@ func _draw() -> void:
if _inventory == null or not is_instance_valid(player) or size.x < 50 or size.y < 50:
return
var outline := PackedVector2Array([Vector2(24, 0), Vector2(size.x - 24, 0), Vector2(size.x, 24), Vector2(size.x, size.y - 12), Vector2(size.x - 12, size.y), Vector2(12, size.y), Vector2(0, size.y - 12), Vector2(0, 24)])
draw_colored_polygon(outline, Color(0.025, 0.041, 0.063, 0.96))
draw_colored_polygon(outline, Color(0.025, 0.041, 0.063, 1.0))
var closed := outline.duplicate()
closed.append(outline[0])
draw_polyline(closed, Color("376779"), 1.5, true)
draw_line(Vector2(28, 2), Vector2(size.x - 28, 2), CYAN, 2, true)
# Put weapon cards beside the counters rather than stacking three rows.
_text("DPS: %.1f" % _inventory.get_total_dps(), Vector2(20, 81), 172, 12, CYAN)
_text("PRĘDKOŚĆ: %.0f" % player.velocity.length(), Vector2(20, 101), 172, 11, Color("c4d6df"))
var absorption: Vector3 = player.get("absorption")
for index in 3:
_text("%s - %.1f%%" % [DamageTypes.NAMES[index], absorption[index]], Vector2(205, 77 + index * 15), 2.0 * (size.x - 238) / 3.0, 11, Color("a9d69a"))

View File

@ -0,0 +1,29 @@
extends "res://scripts/ui/captain_skill_button.gd"
func configure_navigation(artwork: Texture2D, accent: Color, caption: String) -> void:
configure(artwork, accent)
custom_minimum_size = Vector2(64, 80)
for state in ["normal", "hover", "pressed"]:
var frame := get_theme_stylebox(state).duplicate() as StyleBoxFlat
frame.set_corner_radius_all(9)
frame.border_color = accent if state == "hover" else accent.darkened(0.45)
frame.shadow_color = Color(accent, 0.12 if state == "hover" else 0.04)
frame.shadow_size = 5 if state == "hover" else 3
add_theme_stylebox_override(state, frame)
var picture := get_node_or_null("RoundedArtwork") as TextureRect
if picture != null:
picture.offset_left = 5
picture.offset_right = -5
picture.offset_top = 5
picture.offset_bottom = -21
var label := Label.new()
label.text = caption
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 9)
label.add_theme_color_override("font_color", accent.lightened(0.25))
add_child(label)
label.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_WIDE)
label.offset_top = -21
label.offset_bottom = -2

View File

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

View File

@ -0,0 +1,9 @@
extends RefCounted
# Canvas layers are ordered independently of world-space z_index values.
const SCANNER := 8
const DISCOVERY := 10
const HUD := 20
const MANAGEMENT := 40
const DEFEAT := 50
const WINDOW_Z := 100

View File

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

View File

@ -0,0 +1,61 @@
extends Node
const GAME := preload("res://scenes/game/game.tscn")
func _ready() -> void:
GameState.start_new_run()
GameState.captain.skills["reactor_overload"] = 1
GameState.captain.equipped_skills[0] = "reactor_overload"
GameState.captain.skill_cooldowns["reactor_overload"] = 25.0
GameState.captain.skill_durations["reactor_overload"] = 3.0
var game := GAME.instantiate() as Node2D
add_child(game)
await get_tree().process_frame
game.set_physics_process(false)
var ship := game.get_node("PlayerShip") as CharacterBody2D
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.set_physics_process(false)
assert(GameState.captain.skill_cooldowns.is_empty())
assert(GameState.captain.skill_durations.is_empty())
var hud := game.get_node("HUD")
var captain := hud.get_node("HUDRoot/CaptainPanel") as PanelContainer
assert(captain.size.x <= 250)
assert(captain.get_theme_stylebox("panel").bg_color.a == 0.0)
assert(captain.get_node("Contents/PortraitRow/AvatarFrame/CaptainAvatar").custom_minimum_size == Vector2(54, 54))
assert(hud.get_node("HUDRoot/TopNavigation").get_child_count() == 2)
assert(hud.get_node("HUDRoot/TopNavigation").get_child(0).get_node("RoundedArtwork").texture != null)
var inventory := ship.get_node("ShipInventory") as ShipInventory
var item := ItemInstance.new()
item.definition = preload("res://resources/items/cloak.tres")
assert(inventory.add_item(item))
assert(inventory.move_item("cargo", inventory.cargo.find(item), "extension", 0))
assert(ship.call("activate_ability", 0))
assert(ship.call("is_cloaked"))
assert(GameState.captain.activate_skill("reactor_overload", ship))
assert(ship.call("get_ability_cooldown", 0) > 0.0)
assert(not GameState.captain.skill_cooldowns.is_empty())
ship.call("take_damage", 100000.0)
assert(ship.get("_is_destroyed"))
assert(not ship.call("is_cloaked"))
assert(ship.call("get_ability_cooldown", 0) == 0.0)
assert(GameState.captain.skill_cooldowns.is_empty())
assert(GameState.captain.skill_durations.is_empty())
inventory.preserve_for_restart()
game.free()
GameState.system_level += 1
GameState.system_seed = GameState.create_system_seed()
GameState.captain.skill_cooldowns["reactor_overload"] = 12.0
GameState.captain.skill_durations["reactor_overload"] = 2.0
var next_game := GAME.instantiate() as Node2D
add_child(next_game)
await get_tree().process_frame
var next_ship := next_game.get_node("PlayerShip")
assert(next_ship.call("get_ability_cooldown", 0) == 0.0)
assert(GameState.captain.skill_cooldowns.is_empty())
assert(GameState.captain.skill_durations.is_empty())
assert(GameState.captain.get_skill_rank("reactor_overload") == 1)
assert(next_ship.get_node("ShipInventory").extension_slots[0].definition == item.definition)
print("HUD_COOLDOWN_RESET_TEST: PASS (death/new board reset captain and extension cooldowns/effects, equipment and learned skills retained, compact transparent captain, new artwork)")
get_tree().quit()

View File

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

View File

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

View File

@ -0,0 +1,140 @@
extends Node2D
const PLAYER := preload("res://scenes/game/player_ship.tscn")
const ENEMY := preload("res://scenes/game/enemy_ship.tscn")
const GENERATOR := preload("res://scripts/items/item_generator.gd")
const IDS := ["teleport", "cloak", "mine_layer", "flares", "solar_shield_charge"]
var ship: CharacterBody2D
var inventory: ShipInventory
func equip(id: String, slot: int) -> void:
var item := GENERATOR.generate_for_definition(load("res://resources/items/%s.tres" % id), 1)
assert(item.additional_modifiers.is_empty())
assert(item.definition.icon != null)
assert(inventory.add_item(item))
assert(inventory.move_item("cargo", inventory.cargo.find(item), "extension", slot))
func _ready() -> void:
GameState.start_new_run()
ship = PLAYER.instantiate() as CharacterBody2D
ship.name = "PlayerShip"
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)
inventory = ship.get_node("ShipInventory") as ShipInventory
for index in IDS.size():
equip(IDS[index], index)
var enemy := ENEMY.instantiate() as CharacterBody2D
enemy.position = Vector2(300, 0)
add_child(enemy)
enemy.set("_player", ship)
enemy.set_physics_process(false)
await get_tree().physics_frame
await get_tree().process_frame
# Teleport: partial energy is fully consumed; landing stays inside the playable area.
var origin := ship.position
ship.set("energy", 37.0)
assert(ship.call("activate_ability", 0))
assert(ship.position.distance_to(origin) >= 400.0)
assert(absf(ship.position.x) <= 2440 and absf(ship.position.y) <= 1640)
assert(ship.get("energy") == 0.0)
assert(ship.call("get_ability_cooldown", 0) == 60.0)
assert(not ship.call("activate_ability", 0))
# Moving a module or equipping a duplicate cannot bypass its cooldown.
assert(inventory.move_item("extension", 0, "extension", 1))
assert(ship.call("get_ability_cooldown", 1) == 60.0)
equip("teleport", 4)
assert(ship.call("get_ability_cooldown", 4) == 60.0)
assert(not ship.call("activate_ability", 4))
equip("solar_shield_charge", 4)
assert(inventory.move_item("extension", 1, "extension", 0))
ship.position = origin
enemy.position = origin + Vector2(300, 0)
assert(enemy.call("select_combat_target") == ship)
# Cloak removes targeting, lasts ten seconds and restores the original artwork.
var original_material: Material = ship.get_node("Sprite2D").material
assert(ship.call("activate_ability", 1))
assert(ship.call("is_cloaked"))
assert(enemy.call("select_combat_target") == null)
assert(ship.get_node("Sprite2D").material != original_material)
assert(not ship.call("activate_ability", 1))
# Flares attract enemies even while the player is invisible, and expire in ten seconds.
assert(ship.call("activate_ability", 3))
assert(ship.call("get_ability_cooldown", 3) == 30.0)
var flares := get_tree().get_nodes_in_group("player_flares")
assert(flares.size() == 3)
assert(flares.has(enemy.call("select_combat_target")))
assert(not ship.call("activate_ability", 3))
for flare in flares:
flare.set_physics_process(false)
flare.call("_physics_process", 10.0)
assert(flare.is_queued_for_deletion())
assert(enemy.call("select_combat_target") == null)
systems.call("_physics_process", 9.9)
assert(ship.call("is_cloaked"))
systems.call("_physics_process", 0.11)
assert(not ship.call("is_cloaked"))
assert(ship.get_node("Sprite2D").material == original_material)
assert(enemy.call("select_combat_target") == ship)
assert(not ship.call("activate_ability", 1))
# Mine: 5% maximum hull damage despite armor; slow lasts five seconds; one hit only.
assert(ship.call("activate_ability", 2))
var mine: Node2D = get_tree().get_nodes_in_group("player_mines")[0]
mine.set_physics_process(false)
enemy.position = mine.position
enemy.set("shields", 0.0)
enemy.set("absorption", Vector3(75, 0, 0))
var maximum := float(enemy.get("max_hull"))
mine.call("_physics_process", 0.8)
assert(mine.is_queued_for_deletion())
assert(is_equal_approx(float(enemy.get("hull")), maximum * 0.95))
mine.call("_physics_process", 0.1)
assert(is_equal_approx(float(enemy.get("hull")), maximum * 0.95))
assert(enemy.call("get_slow_multiplier") == 0.5)
enemy.set("_player", null)
enemy.call("_physics_process", 5.1)
assert(enemy.call("get_slow_multiplier") == 1.0)
# Solar shields: blocked away from stars and at full shields; fills in five seconds.
ship.set("shields", 0.0)
assert(not ship.call("activate_ability", 4))
var star := Node2D.new()
star.position = ship.position
add_child(star)
ship.set("charging_star", star)
assert(ship.call("activate_ability", 4))
assert(ship.call("get_ability_cooldown", 4) == 30.0)
systems.call("_physics_process", 2.5)
assert(is_equal_approx(float(ship.get("shields")), float(ship.get("max_shields")) * 0.5))
systems.call("_physics_process", 2.5)
assert(is_equal_approx(float(ship.get("shields")), float(ship.get("max_shields"))))
systems.get("cooldowns").clear()
assert(not ship.call("activate_ability", 4))
ship.set("shields", 0.0)
assert(ship.call("activate_ability", 4))
ship.position += Vector2(1000, 0)
systems.call("_physics_process", 1.0)
assert(ship.get("shields") == 0.0 and systems.get("solar_remaining") == 0.0)
# Docking, defeat and pause prevent activation; expired cooldowns allow reuse.
systems.call("_physics_process", 61.0)
ship.set("is_docked", true)
assert(not ship.call("activate_ability", 3))
ship.set("is_docked", false)
get_tree().paused = true
assert(not ship.call("activate_ability", 3))
get_tree().paused = false
ship.set("_is_destroyed", true)
assert(not ship.call("activate_ability", 3))
ship.set("_is_destroyed", false)
assert(ship.call("activate_ability", 3))
inventory.preserve_for_restart()
assert(not GameState.restart_inventory.has("extension_cooldowns"))
var restored := PLAYER.instantiate() as CharacterBody2D
add_child(restored)
restored.set_physics_process(false)
restored.get_node("WeaponController").set_physics_process(false)
assert(restored.call("get_ability_cooldown", 3) == 0.0)
assert(restored.call("activate_ability", 3))
print("TACTICAL_EXTENSIONS_TEST: PASS (five modules, icons, loot, teleport safety/energy, cloak duration/AI/restoration, flares targeting/lifetime, mine damage/slow/single hit, solar recharge/range, shared cooldowns, pause/dock/defeat, cooldown reset on reload)")
get_tree().quit()

View File

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

View File

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

View File

@ -157,10 +157,10 @@ func _ready() -> void:
add_child(next_system)
next_system.get_node("PlayerShip").set_physics_process(false)
assert(GameState.captain == profile and profile.get_spent_learning_points() == 2)
assert(profile.skill_cooldowns["reactor_overload"] == 17.0)
assert(profile.skill_cooldowns.is_empty() and profile.skill_durations.is_empty())
GameState.start_new_run()
assert(GameState.captain.skills.is_empty() and GameState.captain.skill_cooldowns.is_empty())
print("CAPTAIN_SKILLS_TEST: PASS (six skills, three ranks, five equipped slots, F5 input, no cooldown reset, offers, pause, economy, ammo, mining, buffs, shield lock, new run)")
print("CAPTAIN_SKILLS_TEST: PASS (eight skills, three ranks, five equipped slots, F5 input, slot swaps preserve cooldown, new boards reset cooldown, offers, pause, economy, ammo, mining, buffs, shield lock, new run)")
get_tree().quit()
func _test_offers() -> void:

View File

@ -0,0 +1,102 @@
extends Node2D
const CATALOG := preload("res://scripts/captain/captain_skills.gd")
const GENERATOR := preload("res://scripts/items/item_generator.gd")
const EXPLORATION := preload("res://scripts/levels/exploration_generator.gd")
const PLAYER := preload("res://scenes/game/player_ship.tscn")
const DEFINITION := preload("res://resources/items/plasma_cannon.tres")
var ship: CharacterBody2D
func snapshot() -> Dictionary:
var container := Node2D.new()
add_child(container)
EXPLORATION.spawn(container, {"planets": []}, ship)
var result := {}
for poi in container.get_children():
result[poi.get("poi_id")] = {"kind": poi.get("kind"), "position": poi.position, "reward_seed": poi.get("reward_seed")}
container.free()
return result
func count_kind(data: Dictionary, kind: String) -> int:
var count := 0
for poi in data.values():
if poi["kind"] == kind:
count += 1
return count
func _ready() -> void:
GameState.start_new_run()
ship = PLAYER.instantiate() as CharacterBody2D
ship.name = "PlayerShip"
add_child(ship)
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
assert(CATALOG.IDS.has("lucky") and CATALOG.IDS.has("explorer"))
for id in ["lucky", "explorer"]:
assert(not CATALOG.DATA[id]["active"])
assert(CATALOG.icon(id) != null)
assert(not CATALOG.description(id, 3).is_empty())
# Every roll threshold is exact, with 0% baseline and a hard level cap.
for rank in range(4):
GameState.captain.skills["lucky"] = rank
var chance: float = [0.0, 0.05, 0.1, 0.25][rank]
assert(is_equal_approx(GameState.captain.get_skill_bonus("lucky"), chance))
for roll in 1000:
assert(GameState.captain.roll_loot_level(10, float(roll) / 1000.0) == (11 if float(roll) / 1000.0 < chance else 10))
assert(GameState.captain.roll_loot_level(1000, 0.0) == 1000)
assert(GameState.captain.roll_loot_level(0, 0.99) == 1)
# Exercise actual drops, including stat scaling and the normal level generator.
seed(888)
var upgrades := 0
for index in 2000:
var item := GENERATOR.generate_loot(10, 0, DEFINITION)
assert(item.item_level in [10, 11])
if item.item_level == 11:
upgrades += 1
assert(upgrades == 0 if rank == 0 else absf(float(upgrades) / 2000.0 - chance) < 0.04)
assert(GENERATOR.generate_for_definition(DEFINITION, 10).item_level == 10)
# Both new skills participate in the normal level-up offer and training path.
for id in ["lucky", "explorer"]:
var captain := CaptainProgress.new()
for skill in CATALOG.IDS:
captain.skills[skill] = 3
captain.skills[id] = 2
captain.learning_points = 1
captain.prepare_skill_offers()
assert(captain.skill_offers == [id])
assert(captain.train_skill(id))
assert(captain.get_skill_rank(id) == 3)
# Each rank adds exactly 1/3/6 wrecks and leaves every base POI intact.
var baseline := {}
for rank in range(4):
GameState.start_new_run()
GameState.system_seed = 222
GameState.captain.skills["explorer"] = rank
var current := snapshot()
if rank == 0:
baseline = current
var bonus: int = [0, 1, 3, 6][rank]
assert(count_kind(current, "wreck") == count_kind(baseline, "wreck") + bonus)
assert(count_kind(current, "asteroid") == count_kind(baseline, "asteroid"))
for id in baseline:
assert(current[id] == baseline[id])
GameState.start_new_run()
GameState.system_seed = 222
var before := snapshot()
GameState.captain.skills["explorer"] = 3
GameState.captain.emit_changed()
assert(snapshot() == before) # Reloading the current board does not add wrecks.
GameState.system_seed = 333
var next_board := snapshot()
var rng := RandomNumberGenerator.new()
rng.seed = 333 ^ 0x504f49
assert(count_kind(next_board, "wreck") == rng.randi_range(2, 4) + 6)
GameState.explored_pois["333:1000"] = true
var reloaded := snapshot()
assert(not reloaded.has("333:1000"))
assert(reloaded.size() == next_board.size() - 1)
assert(GameState.exploration_wreck_bonuses[333] == 6)
GameState.start_new_run()
assert(GameState.exploration_wreck_bonuses.is_empty())
print("LUCKY_EXPLORER_TEST: PASS (0/5/10/25% luck, thresholds, 8000 drops, level cap/scaling, skill offers/training, +1/3/6 wrecks, stable base POIs, next-board application, reload/claimed-wreck persistence)")
get_tree().quit()

View File

@ -0,0 +1 @@
uid://63vawkp5glvs

View File

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

View File

@ -4,7 +4,7 @@ const GENERATOR := preload("res://scripts/items/item_generator.gd")
const DATABASE := preload("res://resources/items/affix_database.tres")
func _ready() -> void:
assert(DATABASE.affixes.size() == 15)
assert(DATABASE.affixes.size() == 16)
for level in [1, 50, 1000]:
for tier in [0, 1, 2]:
for sample in 50:

View File

@ -0,0 +1,81 @@
extends Node2D
const PLAYER := preload("res://scenes/game/player_ship.tscn")
const ENEMY := preload("res://scenes/game/enemy_ship.tscn")
const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn")
const PROJECTILE_SCRIPT := preload("res://scripts/game/weapon_projectile.gd")
class AccurateEnemy:
extends "res://scripts/game/enemy_ship.gd"
func _laser_aim_direction(direction: Vector2) -> Vector2:
return direction
func _ready() -> void:
GameState.start_new_run()
var ship := PLAYER.instantiate() as CharacterBody2D
ship.name = "PlayerShip"
ship.position = Vector2(150, 0)
add_child(ship)
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
var enemy := ENEMY.instantiate() as CharacterBody2D
enemy.set_script(AccurateEnemy)
enemy.position = Vector2(-200, 0)
add_child(enemy)
enemy.set("_player", ship)
enemy.set_physics_process(false)
var asteroid := StaticBody2D.new()
asteroid.set_script(preload("res://scripts/levels/exploration_poi.gd"))
asteroid.set("kind", "asteroid")
asteroid.set("poi_id", "enemy-cover-test")
asteroid.set("player", ship)
add_child(asteroid)
asteroid.set_physics_process(false)
var asteroid_hp := float(asteroid.get("hull"))
var player_hp := float(ship.get("hull")) + float(ship.get("shields"))
var minerals := GameState.captain.minerals.duplicate()
await get_tree().physics_frame
await get_tree().process_frame
for kind in [ItemDefinition.WeaponKind.PLASMA, ItemDefinition.WeaponKind.MACHINE_GUN, ItemDefinition.WeaponKind.GAMMA, ItemDefinition.WeaponKind.MISSILE]:
enemy.set("weapon_kind", kind)
enemy.call("_fire_plasma", Vector2.RIGHT)
var shot: Area2D
for child in get_children():
if child is Area2D and child.get_script() == PROJECTILE_SCRIPT and not child.is_queued_for_deletion():
shot = child
assert(shot != null and (shot.collision_mask & 32) != 0)
shot.set_physics_process(false)
shot.set("damage", 10000.0)
shot.call("_physics_process", 1.0)
assert(shot.is_queued_for_deletion())
assert(shot.position.x < 0.0) # Stops at the asteroid even at high speed.
assert(float(asteroid.get("hull")) == asteroid_hp)
assert(not asteroid.get("_claimed"))
assert(float(ship.get("hull")) + float(ship.get("shields")) == player_hp)
assert(GameState.captain.minerals == minerals)
enemy.set("weapon_kind", ItemDefinition.WeaponKind.LASER)
enemy.call("_fire_plasma", Vector2.RIGHT)
var beam: Line2D
for child in get_children():
if child is Line2D:
beam = child
assert(beam != null and beam.get_point_position(1).x < 0.0)
assert(float(asteroid.get("hull")) == asteroid_hp)
assert(float(ship.get("hull")) + float(ship.get("shields")) == player_hp)
var legacy := preload("res://scenes/game/enemy_plasma_projectile.tscn").instantiate() as Area2D
legacy.call("configure", Vector2(-100, 0), Vector2.RIGHT, 10000.0)
add_child(legacy)
legacy.set_physics_process(false)
legacy.call("_physics_process", 1.0)
assert(legacy.is_queued_for_deletion())
assert(float(asteroid.get("hull")) == asteroid_hp)
# Player mining remains functional, using the existing target layer.
var player_shot := PROJECTILE.instantiate() as Area2D
player_shot.call("configure", Vector2(-100, 0), Vector2.RIGHT, ItemDefinition.WeaponKind.PLASMA, 600.0, 2.0, 500.0)
add_child(player_shot)
player_shot.set_physics_process(false)
player_shot.call("_physics_process", 1.0)
assert(player_shot.is_queued_for_deletion())
assert(is_equal_approx(float(asteroid.get("hull")), asteroid_hp - 2.0))
print("ENEMY_ASTEROID_COVER_TEST: PASS (plasma/machine gun/gamma/missile/laser, fast-shot collision, no asteroid damage/mining/rewards, player protected, legacy projectile, player mining retained)")
get_tree().quit()

View File

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

View File

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

View File

@ -0,0 +1,80 @@
extends Node2D
const PLAYER := preload("res://scenes/game/player_ship.tscn")
const AFFIX := preload("res://resources/items/affixes/luck_percent.tres")
const GENERATOR := preload("res://scripts/items/item_generator.gd")
func lucky_item(definition: ItemDefinition, amount: float) -> ItemInstance:
var item := ItemInstance.new()
item.definition = definition
var modifier := ItemStatModifier.new()
modifier.affix = AFFIX
modifier.stat_key = "luck_percent"
modifier.value = amount
item.additional_modifiers.append(modifier)
return item
func _ready() -> void:
GameState.start_new_run()
assert(AFFIX.get_value_range(1).is_equal_approx(Vector2(0.5, 1.0)))
assert(AFFIX.get_value_range(1000).is_equal_approx(Vector2(3.0, 6.0)))
var last := Vector2.ZERO
for level in [1, 10, 100, 500, 1000]:
var bounds := AFFIX.get_value_range(level)
assert(bounds.x >= last.x and bounds.y >= last.y)
for index in 100:
var roll := AFFIX.roll_value(level)
assert(roll >= bounds.x and roll <= bounds.y)
last = bounds
assert(AFFIX.can_roll(preload("res://resources/items/plasma_cannon.tres"), 1))
assert(not AFFIX.can_roll(preload("res://resources/items/cloak.tres"), 1))
var ship := PLAYER.instantiate() as CharacterBody2D
ship.name = "PlayerShip"
add_child(ship)
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
var inventory := ship.get_node("ShipInventory") as ShipInventory
var item := lucky_item(preload("res://resources/items/compact_generator.tres"), 0.75)
assert(item.get_tooltip_text().contains("Szczęście: +0.75 %"))
assert(inventory.add_item(item))
assert(ship.call("get_luck_chance") == 0.0) # Cargo does not grant stats.
assert(inventory.move_item("cargo", inventory.cargo.find(item), "module", 3))
assert(is_equal_approx(float(ship.call("get_luck_chance")), 0.0075))
GameState.captain.skills["lucky"] = 1
assert(is_equal_approx(float(ship.call("get_luck_chance")), 0.0575))
assert(GameState.captain.roll_loot_level(10, 0.0574, float(ship.get("luck_percent"))) == 11)
assert(GameState.captain.roll_loot_level(10, 0.0575, float(ship.get("luck_percent"))) == 10)
assert(inventory.move_item("module", 3, "cargo", inventory.cargo.find(null)))
assert(is_equal_approx(float(ship.call("get_luck_chance")), 0.05))
# Combine strong equipment and rank III, with exactly one shared 60% cap.
var reactor := lucky_item(preload("res://resources/items/compact_generator.tres"), 30.0)
var engine := lucky_item(preload("res://resources/items/engine_extension.tres"), 30.0)
for equipped in [reactor, engine]:
assert(inventory.add_item(equipped))
var slot := ShipInventory.MODULE_CATEGORIES.find(equipped.definition.category)
assert(inventory.move_item("cargo", inventory.cargo.find(equipped), "module", slot))
GameState.captain.skills["lucky"] = 3
assert(is_equal_approx(float(ship.call("get_luck_chance")), 0.6))
assert(GameState.captain.roll_loot_level(10, 0.59999, 60.0) == 11)
assert(GameState.captain.roll_loot_level(10, 0.6, 60.0) == 10)
assert(GameState.captain.roll_loot_level(1000, 0.0, 60.0) == 1000)
assert(is_equal_approx(GameState.captain.get_luck_chance(100000.0), 0.6))
var stronger := lucky_item(reactor.definition, 50.0)
var comparisons := inventory.get_item_comparison(stronger, reactor)
for row in comparisons:
assert(row["key"] != "luck_percent") # Already capped: no fictitious gain.
seed(42)
var upgrades := 0
for index in 1000:
var loot := GENERATOR.generate_loot(10, 0, preload("res://resources/items/plasma_cannon.tres"), float(ship.get("luck_percent")))
if loot.item_level == 11:
upgrades += 1
assert(upgrades >= 550 and upgrades <= 650)
inventory.preserve_for_restart()
var restored := PLAYER.instantiate() as CharacterBody2D
add_child(restored)
restored.set_physics_process(false)
restored.get_node("WeaponController").set_physics_process(false)
assert(is_equal_approx(float(restored.call("get_luck_chance")), 0.6))
print("LUCK_AFFIX_TEST: PASS (roll bounds/scaling, categories, cargo/equipment, skill stacking, strict 60% cap, thresholds, tooltips/comparison, 1000 drops, persistence)")
get_tree().quit()

View File

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

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