Duzo zmian
1
assets/icons/captain/explorer.svg
Normal 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 |
43
assets/icons/captain/explorer.svg.import
Normal 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
|
||||
1
assets/icons/captain/lucky.svg
Normal 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 |
43
assets/icons/captain/lucky.svg.import
Normal 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
|
||||
20
assets/icons/extensions/artwork_prompts.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
1
assets/icons/extensions/cloak.svg
Normal 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 |
43
assets/icons/extensions/cloak.svg.import
Normal 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
|
||||
BIN
assets/icons/extensions/cloak_art.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
40
assets/icons/extensions/cloak_art.png.import
Normal 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
|
||||
3
assets/icons/extensions/cloak_shields_prompts.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"tool": "built-in image_gen"
|
||||
}
|
||||
1
assets/icons/extensions/flares.svg
Normal 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 |
43
assets/icons/extensions/flares.svg.import
Normal 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
|
||||
BIN
assets/icons/extensions/flares_art.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
40
assets/icons/extensions/flares_art.png.import
Normal 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
|
||||
BIN
assets/icons/extensions/mine_art.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
assets/icons/extensions/mine_art.png.import
Normal 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
|
||||
1
assets/icons/extensions/mine_layer.svg
Normal 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 |
43
assets/icons/extensions/mine_layer.svg.import
Normal 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
|
||||
1
assets/icons/extensions/solar_shield_charge.svg
Normal 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 |
43
assets/icons/extensions/solar_shield_charge.svg.import
Normal 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
|
||||
BIN
assets/icons/extensions/solar_shield_charge_art.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
40
assets/icons/extensions/solar_shield_charge_art.png.import
Normal 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
|
||||
1
assets/icons/extensions/teleport.svg
Normal 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 |
43
assets/icons/extensions/teleport.svg.import
Normal 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
|
||||
BIN
assets/icons/extensions/teleport_art.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
40
assets/icons/extensions/teleport_art.png.import
Normal 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
|
||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 2.5 MiB |
6
assets/icons/resources/aluminium_ore_prompt.json
Normal 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."
|
||||
}
|
||||
BIN
assets/icons/ui/captain_skills_art.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
40
assets/icons/ui/captain_skills_art.png.import
Normal 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
|
||||
BIN
assets/icons/ui/crafting_art.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
40
assets/icons/ui/crafting_art.png.import
Normal 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
|
||||
13
assets/icons/ui/navigation_art_prompts.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
19
assets/sounds/music/music2.mp3.import
Normal 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
|
||||
@ -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")])
|
||||
|
||||
15
resources/items/affixes/luck_percent.tres
Normal 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
|
||||
19
resources/items/cloak.tres
Normal 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")
|
||||
19
resources/items/flares.tres
Normal 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")
|
||||
19
resources/items/mine_layer.tres
Normal 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")
|
||||
19
resources/items/solar_shield_charge.tres
Normal 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")
|
||||
19
resources/items/teleport.tres
Normal 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")
|
||||
9
resources/ui/cloak.gdshader
Normal 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));
|
||||
}
|
||||
1
resources/ui/cloak.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bvir23us3og83
|
||||
@ -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")
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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 = ""
|
||||
|
||||
|
||||
25
scripts/abilities/tactical_extension_ability.gd
Normal 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)
|
||||
1
scripts/abilities/tactical_extension_ability.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://mjofjrs3v7by
|
||||
@ -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)
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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)})
|
||||
|
||||
71
scripts/game/deployed_flare.gd
Normal 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)
|
||||
1
scripts/game/deployed_flare.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b4lvkvbncex2d
|
||||
61
scripts/game/deployed_mine.gd
Normal 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))
|
||||
1
scripts/game/deployed_mine.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cb3vd7xbdbu6i
|
||||
36
scripts/game/deployment_flash.gd
Normal 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)
|
||||
1
scripts/game/deployment_flash.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dpedhxnegxr3s
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
23
scripts/game/extension_pulse.gd
Normal 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)
|
||||
1
scripts/game/extension_pulse.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cb7y8kdev6y4f
|
||||
160
scripts/game/extension_systems.gd
Normal 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)
|
||||
1
scripts/game/extension_systems.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dosf61lp5sny1
|
||||
17
scripts/game/extension_vfx.gd
Normal 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
|
||||
1
scripts/game/extension_vfx.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://djhax4e4n5hdh
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
44
scripts/game/teleport_vortex.gd
Normal 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)
|
||||
1
scripts/game/teleport_vortex.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dvprav6rgv8h4
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"))
|
||||
|
||||
29
scripts/ui/navigation_button.gd
Normal 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
|
||||
1
scripts/ui/navigation_button.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bq0dmkp34jo8g
|
||||
9
scripts/ui/render_layers.gd
Normal 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
|
||||
1
scripts/ui/render_layers.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cc5diriqmv08s
|
||||
61
tests/abilities/hud_cooldown_reset_test.gd
Normal 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()
|
||||
1
tests/abilities/hud_cooldown_reset_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b1jbnovika6bd
|
||||
4
tests/abilities/hud_cooldown_reset_test.tscn
Normal 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")
|
||||
140
tests/abilities/tactical_extensions_test.gd
Normal 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()
|
||||
1
tests/abilities/tactical_extensions_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bd5nib3xihg7u
|
||||
4
tests/abilities/tactical_extensions_test.tscn
Normal 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")
|
||||
@ -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:
|
||||
|
||||
102
tests/captain/lucky_explorer_test.gd
Normal 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()
|
||||
1
tests/captain/lucky_explorer_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://63vawkp5glvs
|
||||
4
tests/captain/lucky_explorer_test.tscn
Normal 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")
|
||||
@ -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:
|
||||
|
||||
81
tests/items/enemy_asteroid_cover_test.gd
Normal 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()
|
||||
1
tests/items/enemy_asteroid_cover_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://d2pwe68pu6fvh
|
||||
4
tests/items/enemy_asteroid_cover_test.tscn
Normal 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")
|
||||
80
tests/items/luck_affix_test.gd
Normal 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()
|
||||
1
tests/items/luck_affix_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dp8axo8tkjwmc
|
||||