Wizualen poprawki

This commit is contained in:
Tomasz Boruc 2026-10-15 21:46:44 +02:00
parent 6f9bd71298
commit d7581d2f70
46 changed files with 1505 additions and 76 deletions

View File

@ -0,0 +1,27 @@
# Captain skill artwork
Generated with the built-in imagegen tool. Files: business_shark.png, reactor_overload.png, tactical_maneuver.png, efficient_magazine.png, miner.png, power_redirect.png.
## business_shark
Use case: stylized-concept. Final production square ability icon for a realistic painterly space RPG. Highly polished hand-painted digital game art, cinematic material detail, strong simple silhouette readable at 40 pixels, centered composition with 12% safe margin. Dark navy outer corners, restrained luminous accent, no text, no letters, no numbers, no interface, no border. One iconic subject, not a scene. Business Shark passive skill: an elegant gold metallic shark emblem curling around a small stack of futuristic credit coins, warm amber highlights, brushed gold and deep charcoal materials, confident mercantile space captain mood.
## reactor_overload
Use case: stylized-concept. Final production square ability icon for a realistic painterly space RPG. Highly polished hand-painted digital game art, cinematic material detail, strong simple silhouette readable at 40 pixels, centered composition with 12% safe margin. Dark navy outer corners, restrained luminous accent, no text, no letters, no numbers, no interface, no border. One iconic subject, not a scene. Reactor Overload active skill: a compact circular spaceship fusion reactor with a brilliant cyan-white energy core inside heavy gunmetal containment rings, vivid electric blue arcs escaping the core, teal glow illuminating machined metal, dynamic energy, clear large central reactor silhouette.
## tactical_maneuver
Use case: stylized-concept. Final production square ability icon for a realistic painterly space RPG. Highly polished hand-painted digital game art, cinematic material detail, strong simple silhouette readable at 40 pixels, centered composition with 12% safe margin. Dark navy outer corners, restrained luminous accent, no text, no letters, no numbers, no interface, no border. One iconic subject, not a scene. Tactical Maneuver active skill: a sleek silver spacecraft banking sharply diagonally upward, two curved violet-blue contrails sweeping behind it to show an evasive turn, a faint ghost silhouette of its former position, purple light on metallic hull. Strong clear ship shape and dramatic diagonal movement.
## efficient_magazine
Use case: stylized-concept. Final production square ability icon for a realistic painterly space RPG. Highly polished hand-painted digital game art, cinematic material detail, strong simple silhouette readable at 40 pixels, centered composition with 12% safe margin. Dark navy outer corners, restrained luminous accent, no text, no letters, no numbers, no interface, no border. One iconic subject, not a scene. Efficient Magazine passive skill: a robust gunmetal ammunition magazine with three large brass-tipped futuristic kinetic cartridges, a luminous green circular recycling arrow wrapping around the cartridge group, sage-green accent light, realistic brushed metal, unmistakable ammo conservation emblem.
## miner
Use case: stylized-concept. Final production square ability icon for a realistic painterly space RPG. Highly polished hand-painted digital game art, cinematic material detail, strong simple silhouette readable at 40 pixels, centered composition with 12% safe margin. Dark navy outer corners, restrained luminous accent, no text, no letters, no numbers, no interface, no border. One iconic subject, not a scene. Miner passive skill: a rugged futuristic plasma mining pick striking a small jagged asteroid rich with large bright titanium and copper mineral veins, a few amber sparks at impact, dark metal tool with worn industrial edges, warm copper and steel-blue material contrast, clear bold pickaxe-and-rock silhouette.
## power_redirect
Use case: stylized-concept. Final production square ability icon for a realistic painterly space RPG. Highly polished hand-painted digital game art, cinematic material detail, strong simple silhouette readable at 40 pixels, centered composition with 12% safe margin. Dark navy outer corners, restrained luminous accent, no text, no letters, no numbers, no interface, no border. One iconic subject, not a scene. Power Redirect active skill: a heavy metallic spaceship shield emitter on the left splitting open and releasing a powerful curved orange-gold energy stream into a bright compact cyan reactor core on the right. A bold luminous flow of power visually connects the two components. Burnt orange, pale gold, electric teal highlights, realistic sci-fi machinery, strong simple two-part silhouette.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cqgv8uxur4ilq"
path="res://.godot/imported/business_shark.png-5d3a96ae879adf7dc656217f25b40537.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/business_shark.png"
dest_files=["res://.godot/imported/business_shark.png-5d3a96ae879adf7dc656217f25b40537.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<defs><linearGradient id="g" x2="1" y2="1"><stop stop-color="#ffe5a3"/><stop offset="1" stop-color="#b57b30"/></linearGradient></defs>
<path d="M48 5 84 24v45L48 91 12 69V24Z" fill="#211d16" stroke="#a2793b" stroke-width="2"/>
<path d="M25 59c8-15 14-22 31-28l10-14 2 18 12 5-13 9-6 15-15-5-13 12 3-13Z" fill="url(#g)" stroke="#ffe6a6" stroke-width="1.5" stroke-linejoin="round"/>
<circle cx="61" cy="40" r="2" fill="#17212b"/><path d="m50 53 7 4 7-9M23 77h23m-23-5h13" fill="none" stroke="#eac47b" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 632 B

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bavcivfj1hvam"
path="res://.godot/imported/business_shark.svg-89848d8b572ca882ea2021aa89d320aa.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/business_shark.svg"
dest_files=["res://.godot/imported/business_shark.svg-89848d8b572ca882ea2021aa89d320aa.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bpqtxf51nyry3"
path="res://.godot/imported/efficient_magazine.png-5217444fb6fcaeb4761f635d1dd088a2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/efficient_magazine.png"
dest_files=["res://.godot/imported/efficient_magazine.png-5217444fb6fcaeb4761f635d1dd088a2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<path d="M48 5 84 24v45L48 91 12 69V24Z" fill="#17271e" stroke="#698c6e" stroke-width="2"/>
<g fill="#d4eac3" stroke="#88b080" stroke-width="2" stroke-linejoin="round"><path d="M24 63V37l7-13 7 13v26Z"/><path d="M42 63V37l7-13 7 13v26Z"/></g>
<path d="M23 53h34M22 67h36" stroke="#698c6e" stroke-width="3"/><path d="M65 32c14 10 14 28 0 37m-1-11-1 13 12-4" fill="none" stroke="#bce89d" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://sf0b8u85a7fn"
path="res://.godot/imported/efficient_magazine.svg-a8dbaabf81ed2aef576d5ea758c66386.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/efficient_magazine.svg"
dest_files=["res://.godot/imported/efficient_magazine.svg-a8dbaabf81ed2aef576d5ea758c66386.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cevaxidi3ydxu"
path="res://.godot/imported/miner.png-212675e08e370691f2ba34f1899b904d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/miner.png"
dest_files=["res://.godot/imported/miner.png-212675e08e370691f2ba34f1899b904d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<path d="M48 5 84 24v45L48 91 12 69V24Z" fill="#292019" stroke="#a17d54" stroke-width="2"/>
<path d="m22 63 9-12 17 5 7 16-16 6-14-5Z" fill="#8c7968" stroke="#e5ba88" stroke-width="2" stroke-linejoin="round"/>
<path d="m34 27 34 34M32 26c20-11 32-5 43 10-18-7-23-4-30 5Z" fill="#e2c19a" stroke="#f5dab6" stroke-width="2" stroke-linejoin="round"/>
<path d="m59 23-29 45" stroke="#f1b469" stroke-width="5" stroke-linecap="round"/><path d="m62 68 5 8 9-6-5-9Z" fill="#edb678"/>
</svg>

After

Width:  |  Height:  |  Size: 566 B

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bpp1j7oclr0oj"
path="res://.godot/imported/miner.svg-bf2e3ad4c379dbe2e9f9e031795889ba.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/miner.svg"
dest_files=["res://.godot/imported/miner.svg-bf2e3ad4c379dbe2e9f9e031795889ba.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c1g80e3fq87ac"
path="res://.godot/imported/power_redirect.png-1e3d664cc98261556d6bfef217ca50a0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/power_redirect.png"
dest_files=["res://.godot/imported/power_redirect.png-1e3d664cc98261556d6bfef217ca50a0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<path d="M48 5 84 24v45L48 91 12 69V24Z" fill="#2a1b21" stroke="#ac6b61" stroke-width="2"/>
<path d="m20 32 16-7 16 7v15c0 12-9 20-16 25-8-6-16-14-16-25Z" fill="#273a4d" stroke="#819db4" stroke-width="2"/>
<path d="m25 62 22-29" stroke="#f39b7d" stroke-width="3" stroke-linecap="round"/>
<path d="M44 47h22m-7-7 8 7-8 7" fill="none" stroke="#ffd2a8" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="m72 22-17 18h12l-6 15 19-22H68Z" fill="#ffc48a"/><path d="M61 64h15m-12 6h9" stroke="#ef9172" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 652 B

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b8wfl8vnm8bh2"
path="res://.godot/imported/power_redirect.svg-ab8d0be7673fec276bd174a0e425f4cc.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/power_redirect.svg"
dest_files=["res://.godot/imported/power_redirect.svg-ab8d0be7673fec276bd174a0e425f4cc.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cjca12kt2v6vd"
path="res://.godot/imported/reactor_overload.png-dbe9ee0414658af8e7b0396a3bca12f9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/reactor_overload.png"
dest_files=["res://.godot/imported/reactor_overload.png-dbe9ee0414658af8e7b0396a3bca12f9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<defs><radialGradient id="g"><stop stop-color="#d9ffff"/><stop offset="1" stop-color="#148ca8"/></radialGradient></defs>
<path d="M48 5 84 24v45L48 91 12 69V24Z" fill="#102330" stroke="#448ea8" stroke-width="2"/>
<circle cx="48" cy="48" r="26" fill="none" stroke="#46bbd6" stroke-width="4" stroke-dasharray="32 9"/>
<circle cx="48" cy="48" r="17" fill="url(#g)" opacity=".5"/>
<path d="m52 20-21 32h15l-3 24 23-35H51Z" fill="#d4fcff" stroke="#80e7f4" stroke-width="2" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 585 B

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbm86lpa2e35s"
path="res://.godot/imported/reactor_overload.svg-af2e2a6c8e1071e550b60db938d47304.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/reactor_overload.svg"
dest_files=["res://.godot/imported/reactor_overload.svg-af2e2a6c8e1071e550b60db938d47304.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://gknhk6bd2kxy"
path="res://.godot/imported/tactical_maneuver.png-c777f3e535ae44e1d2d91a50b6bb904d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/tactical_maneuver.png"
dest_files=["res://.godot/imported/tactical_maneuver.png-c777f3e535ae44e1d2d91a50b6bb904d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<path d="M48 5 84 24v45L48 91 12 69V24Z" fill="#211a33" stroke="#8774b4" stroke-width="2"/>
<path d="M23 68c-7-23 5-40 29-43m-8-6 10 5-7 8M29 77c21 2 39-9 42-30m-7 5 8-8 6 10" fill="none" stroke="#b6a0ec" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="m50 30 16 36-16-9-15 9Z" fill="#dfd4ff" stroke="#a48ad8" stroke-width="2" stroke-linejoin="round"/><path d="m50 43 0 12" stroke="#5e4b8e" stroke-width="3"/>
</svg>

After

Width:  |  Height:  |  Size: 528 B

View File

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dxuw3d6gfvcfd"
path="res://.godot/imported/tactical_maneuver.svg-332c20dd68467e5c2748d2183a9c7da9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/tactical_maneuver.svg"
dest_files=["res://.godot/imported/tactical_maneuver.svg-332c20dd68467e5c2748d2183a9c7da9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,21 @@
shader_type canvas_item;
render_mode unshaded;
uniform vec2 icon_size = vec2(32.0);
uniform float corner_radius = 4.0;
uniform float cooldown : hint_range(0.0, 1.0) = 0.0;
uniform float dimming : hint_range(0.0, 1.0) = 0.0;
void fragment() {
vec4 artwork = texture(TEXTURE, UV);
vec2 p = abs((UV - 0.5) * icon_size) - icon_size * 0.5 + corner_radius;
float distance_to_edge = length(max(p, vec2(0.0))) + min(max(p.x, p.y), 0.0) - corner_radius;
float aa = max(fwidth(distance_to_edge), 0.5);
float mask = 1.0 - smoothstep(-aa * 0.5, aa * 0.5, distance_to_edge);
float luminance = dot(artwork.rgb, vec3(0.2126, 0.7152, 0.0722));
artwork.rgb = mix(artwork.rgb, vec3(luminance), dimming * 0.65);
artwork.rgb *= 1.0 - dimming * 0.38;
float cooling = (1.0 - step(cooldown, UV.y)) * step(0.001, cooldown);
artwork.rgb *= 1.0 - cooling * 0.6;
COLOR = vec4(artwork.rgb, artwork.a * mask);
}

View File

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

View File

@ -3,11 +3,15 @@ extends ActiveAbility
@export var shield_amount: float = 100.0 @export var shield_amount: float = 100.0
func unavailable_reason(ship: CharacterBody2D) -> String: func unavailable_reason(ship: CharacterBody2D) -> String:
if GameState.captain.is_skill_active("power_redirect"):
return "Tarcze wyłączone przez Przekierowanie zasilania"
if ship.get("shields") >= ship.get("max_shields"): if ship.get("shields") >= ship.get("max_shields"):
return "Tarcze są pełne" return "Tarcze są pełne"
return "Wymagana pełna energia" if ship.get("energy") < ship.get("max_energy") else "" return "Wymagana pełna energia" if ship.get("energy") < ship.get("max_energy") else ""
func activate(ship: CharacterBody2D) -> void: func activate(ship: CharacterBody2D) -> void:
if GameState.captain.is_skill_active("power_redirect"):
return
ship.set("energy", 0.0) ship.set("energy", 0.0)
ship.set("shields", minf(ship.get("max_shields"), ship.get("shields") + shield_amount)) ship.set("shields", minf(ship.get("max_shields"), ship.get("shields") + shield_amount))
ship.call("_update_shield_visual") ship.call("_update_shield_visual")

View File

@ -2,6 +2,8 @@ extends Resource
class_name CaptainProgress class_name CaptainProgress
const BALANCE := preload("res://scripts/captain/captain_balance.gd") const BALANCE := preload("res://scripts/captain/captain_balance.gd")
const SKILLS := preload("res://scripts/captain/captain_skills.gd")
const ACTIVE_SKILL_SLOTS := 5
@export var level: int = 1 @export var level: int = 1
@export var experience: int = 0 @export var experience: int = 0
@ -10,55 +12,152 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd")
@export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0} @export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0}
@export var learning_points: int = 0 @export var learning_points: int = 0
@export var skills: Dictionary = {} @export var skills: Dictionary = {}
@export var equipped_skills: Array[String] = ["", "", "", "", ""]
var skill_offers: Array[String] = []
var skill_cooldowns: Dictionary = {}
var skill_durations: Dictionary = {}
var _ammo_credit: Dictionary = {}
var _skill_rng := RandomNumberGenerator.new()
func _init() -> void:
_skill_rng.randomize()
func get_xp_required() -> int: func get_xp_required() -> int:
return BALANCE.xp_required(level) return BALANCE.xp_required(level)
func get_skill_rank(skill: String) -> int: func get_skill_rank(skill: String) -> int:
return clampi(int(skills.get(skill, 0)), 0, BALANCE.MAX_SKILL_RANK) return clampi(int(skills.get(skill, 0)), 0, SKILLS.MAX_RANK) if SKILLS.DATA.has(skill) else 0
func get_skill_bonus(_skill: String) -> float: func get_skill_bonus(skill: String) -> float:
return 0.0 # The old captain skill system has been retired. return SKILLS.value(skill, get_skill_rank(skill))
func normalize_skill_ranks() -> void: func normalize_skill_ranks() -> void:
# Refund legacy ranks for the future captain skill system. for skill in skills.keys():
var refunded := get_spent_learning_points() if not SKILLS.DATA.has(skill):
if refunded > 0: skills.erase(skill)
learning_points += refunded else:
skills.clear() skills[skill] = get_skill_rank(skill)
normalize_equipped_skills()
func normalize_equipped_skills() -> void:
equipped_skills.resize(ACTIVE_SKILL_SLOTS)
var seen: Array[String] = []
for index in ACTIVE_SKILL_SLOTS:
var skill := equipped_skills[index]
if not SKILLS.DATA.has(skill) or not bool(SKILLS.DATA[skill]["active"]) or get_skill_rank(skill) <= 0 or skill in seen:
equipped_skills[index] = ""
else:
seen.append(skill)
func equip_skill(slot: int, skill: String) -> bool:
if slot < 0 or slot >= ACTIVE_SKILL_SLOTS:
return false
if not skill.is_empty() and (not SKILLS.DATA.has(skill) or not bool(SKILLS.DATA[skill]["active"]) or get_skill_rank(skill) <= 0):
return false
normalize_equipped_skills()
# Moving a skill between slots never resets its cooldown or active duration.
var previous_slot := equipped_skills.find(skill) if not skill.is_empty() else -1
if previous_slot >= 0:
equipped_skills[previous_slot] = ""
equipped_skills[slot] = skill
emit_changed() emit_changed()
return return true
func get_spent_learning_points() -> int: func get_spent_learning_points() -> int:
var spent := 0 var spent := 0
for skill in BALANCE.SKILL_NAMES: for skill in SKILLS.IDS:
spent += maxi(0, int(skills.get(skill, 0))) spent += get_skill_rank(skill)
return spent return spent
func get_skill_reset_cost() -> int: func get_skill_reset_cost() -> int:
return BALANCE.SKILL_RESET_COST_PER_LEVEL * maxi(1, level) return BALANCE.SKILL_RESET_COST_PER_LEVEL * maxi(1, level)
func can_reset_skills() -> bool: func can_reset_skills() -> bool:
return get_spent_learning_points() > 0 and credits >= get_skill_reset_cost() return false # Skills belong to the entire run; choices cannot be rerolled at a shop.
func reset_skills() -> bool: func reset_skills() -> bool:
if not can_reset_skills():
return false return false
credits -= get_skill_reset_cost()
learning_points += get_spent_learning_points()
for skill in BALANCE.SKILL_NAMES:
skills[skill] = 0
emit_changed()
return true
func can_train(_skill: String) -> bool: func can_train(skill: String) -> bool:
return false return learning_points > 0 and skill in skill_offers and get_skill_rank(skill) < SKILLS.MAX_RANK
func train_skill(skill: String) -> bool: func train_skill(skill: String) -> bool:
if not can_train(skill): if not can_train(skill):
return false return false
skills[skill] = get_skill_rank(skill) + 1 skills[skill] = get_skill_rank(skill) + 1
if get_skill_rank(skill) == 1 and bool(SKILLS.DATA[skill]["active"]):
normalize_equipped_skills()
var empty := equipped_skills.find("")
if empty >= 0:
equipped_skills[empty] = skill
learning_points -= 1 learning_points -= 1
skill_offers.clear()
prepare_skill_offers()
emit_changed()
return true
func prepare_skill_offers() -> void:
if learning_points <= 0 or not skill_offers.is_empty():
return
var pool: Array[String] = []
for skill in SKILLS.IDS:
if get_skill_rank(skill) < SKILLS.MAX_RANK:
pool.append(skill)
if pool.is_empty():
learning_points = 0
return
while skill_offers.size() < 3 and not pool.is_empty():
var index := _skill_rng.randi_range(0, pool.size() - 1)
skill_offers.append(pool[index])
pool.remove_at(index)
func discounted_price(base_price: int) -> int:
return maxi(1, roundi(base_price * (1.0 - get_skill_bonus("business_shark"))))
func should_consume_ammunition(ammo_kind: String) -> bool:
# Accumulate fractions for an exact saving rate instead of a lucky streak.
var credit := float(_ammo_credit.get(ammo_kind, 0.0)) + get_skill_bonus("efficient_magazine")
var saved := credit >= 1.0 - 0.000001
_ammo_credit[ammo_kind] = maxf(0.0, credit - 1.0) if saved else credit
return not saved
func tick_skills(delta: float) -> void:
for skill in skill_cooldowns:
skill_cooldowns[skill] = maxf(0.0, float(skill_cooldowns[skill]) - delta)
for skill in skill_durations:
skill_durations[skill] = maxf(0.0, float(skill_durations[skill]) - delta)
func is_skill_active(skill: String) -> bool:
return float(skill_durations.get(skill, 0.0)) > 0.0
func energy_regeneration_multiplier() -> float:
var bonus := 1.0
for skill in ["reactor_overload", "power_redirect"]:
if is_skill_active(skill):
bonus += get_skill_bonus(skill)
return bonus
func skill_unavailable_reason(skill: String, ship: CharacterBody2D) -> String:
if not SKILLS.DATA.has(skill) or not bool(SKILLS.DATA[skill]["active"]) or get_skill_rank(skill) <= 0:
return "Umiejętność nie jest odblokowana"
if skill not in equipped_skills:
return "Przypisz umiejętność do slotu F1–F5 w panelu kapitana"
if bool(ship.get("_is_destroyed")) or bool(ship.get("is_docked")):
return "Wymagany aktywny lot"
if float(skill_cooldowns.get(skill, 0.0)) > 0.0:
return "Odnowienie: %.1f s" % float(skill_cooldowns[skill])
if skill == "power_redirect" and (float(ship.get("max_shields")) <= 0.0 or float(ship.get("shields")) < float(ship.get("max_shields")) * 0.25):
return "Wymagane min. 25% tarcz"
return ""
func activate_skill(skill: String, ship: CharacterBody2D) -> bool:
if not skill_unavailable_reason(skill, ship).is_empty():
return false
skill_cooldowns[skill] = SKILLS.COOLDOWN
skill_durations[skill] = SKILLS.DURATION
if skill == "power_redirect":
ship.set("shields", 0.0)
ship.call("_update_shield_visual")
emit_changed() emit_changed()
return true return true
@ -69,6 +168,7 @@ func add_rewards(xp_amount: int, credit_amount: int) -> void:
experience -= get_xp_required() experience -= get_xp_required()
level += 1 level += 1
learning_points += BALANCE.LEARNING_POINTS_PER_LEVEL learning_points += BALANCE.LEARNING_POINTS_PER_LEVEL
prepare_skill_offers()
emit_changed() emit_changed()
func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_multiplier: float, grant_credits: bool = true) -> Vector2i: func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_multiplier: float, grant_credits: bool = true) -> Vector2i:

View File

@ -0,0 +1,39 @@
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 DATA := {
"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"},
"efficient_magazine": {"name": "Oszczędny magazynek", "active": false, "values": [0.10, 0.20, 0.50], "color": "a9d69a"},
"miner": {"name": "Górnik", "active": false, "values": [3, 6, 10], "color": "e5ac76"},
"power_redirect": {"name": "Przekierowanie zasilania", "active": true, "values": [0.50, 0.90, 1.50], "color": "f28b6e"},
}
static func value(skill: String, rank: int) -> float:
if not DATA.has(skill) or rank <= 0:
return 0.0
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
static func description(skill: String, rank: int) -> String:
var amount := roundi(value(skill, rank) * 100.0)
match skill:
"business_shark":
return "Zakupy za kredyty tańsze o %d%%." % amount
"reactor_overload":
return "Regeneracja energii +%d%% przez 5 sekund." % amount
"tactical_maneuver":
return "Szansa uniku wynosi %d%% przez 5 sekund. Unik anuluje całe trafienie." % amount
"efficient_magazine":
return "Zużycie amunicji mniejsze o %d%%. Obejmuje zwykłą i napromieniowaną amunicję, bez rakiet." % amount
"miner":
return "+%d szt. surowca z każdej zniszczonej asteroidy." % roundi(value(skill, rank))
"power_redirect":
return "Regeneracja energii +%d%% przez 5 sekund. Zużywa wszystkie tarcze i blokuje ich odzysk na czas działania. Wymaga min. 25%% tarcz." % amount
return ""

View File

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

View File

@ -9,8 +9,7 @@ extends CanvasLayer
var _debug_expanded := false var _debug_expanded := false
var _ability_buttons: Dictionary = {} var _ability_buttons: Dictionary = {}
var _ability_bar: HBoxContainer var _ability_bar: HBoxContainer
var _ability_ready_style: StyleBoxFlat var _passive_skills: VBoxContainer
var _ability_blocked_style: StyleBoxFlat
var _inventory_clip: Control var _inventory_clip: Control
var _sidebar: PanelContainer var _sidebar: PanelContainer
var _sidebar_toggle: Button var _sidebar_toggle: Button
@ -25,7 +24,7 @@ var _debug_item_status: Label
var _captain_panel: PanelContainer var _captain_panel: PanelContainer
var _cockpit: Control var _cockpit: Control
var _crafting_window: PanelContainer var _crafting_window: PanelContainer
var _skills_window: PanelContainer var _captain_skills_ui: CanvasLayer
func _create_navigation() -> void: func _create_navigation() -> void:
var navigation := HBoxContainer.new() var navigation := HBoxContainer.new()
@ -43,12 +42,11 @@ func _create_navigation() -> void:
crafting.set_script(preload("res://scripts/ui/ship_crafting_panel.gd")) crafting.set_script(preload("res://scripts/ui/ship_crafting_panel.gd"))
crafting.set("standalone", true) crafting.set("standalone", true)
_crafting_window.get_node("Contents").add_child(crafting) _crafting_window.get_node("Contents").add_child(crafting)
_skills_window = _create_window("CaptainSkillsWindow", "UMIEJĘTNOŚCI KAPITANA", Vector2(480, 300)) _captain_skills_ui = CanvasLayer.new()
var empty := Control.new() _captain_skills_ui.set_script(preload("res://scripts/ui/captain_skills_ui.gd"))
empty.name = "SkillsContent" _captain_skills_ui.set("player", player)
empty.custom_minimum_size = Vector2(440, 230) add_child(_captain_skills_ui)
_skills_window.get_node("Contents").add_child(empty) 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.svg", Color("efb568")], ["Umiejętności kapitana", _skills_window, "res://assets/icons/ui/captain_skills.svg", Color("b9a0ef")]]:
var button := Button.new() var button := Button.new()
button.tooltip_text = entry[0] button.tooltip_text = entry[0]
button.icon = load(entry[2]) as Texture2D button.icon = load(entry[2]) as Texture2D
@ -63,6 +61,13 @@ func _create_navigation() -> void:
style.set_content_margin_all(8) style.set_content_margin_all(8)
button.add_theme_stylebox_override(state, style) button.add_theme_stylebox_override(state, style)
navigation.add_child(button) navigation.add_child(button)
if entry[1] == null:
button.name = "OpenCaptainSkills"
button.pressed.connect(func() -> void:
_crafting_window.hide()
_captain_skills_ui.call("open_management")
)
else:
button.pressed.connect(_toggle_window.bind(entry[1])) button.pressed.connect(_toggle_window.bind(entry[1]))
func _create_window(node_name: String, title: String, dimensions: Vector2) -> PanelContainer: func _create_window(node_name: String, title: String, dimensions: Vector2) -> PanelContainer:
@ -105,7 +110,6 @@ func _create_window(node_name: String, title: String, dimensions: Vector2) -> Pa
func _toggle_window(window: PanelContainer) -> void: func _toggle_window(window: PanelContainer) -> void:
var opening := not window.visible var opening := not window.visible
_crafting_window.hide() _crafting_window.hide()
_skills_window.hide()
window.visible = opening window.visible = opening
func _ready() -> void: func _ready() -> void:
@ -152,6 +156,13 @@ func _create_sidebar() -> void:
_captain_panel.position = Vector2(12, 12) _captain_panel.position = Vector2(12, 12)
_captain_panel.size = Vector2(320, _captain_panel.get_combined_minimum_size().y) _captain_panel.size = Vector2(320, _captain_panel.get_combined_minimum_size().y)
_captain_panel.resized.connect(_layout_debug) _captain_panel.resized.connect(_layout_debug)
_passive_skills = VBoxContainer.new()
_passive_skills.name = "PassiveCaptainSkills"
_passive_skills.add_theme_constant_override("separation", 6)
$HUDRoot.add_child(_passive_skills)
_passive_skills.minimum_size_changed.connect(func() -> void: _layout_debug.call_deferred())
GameState.captain.changed.connect(_refresh_passive_skills)
_refresh_passive_skills()
_debug_actions = VBoxContainer.new() _debug_actions = VBoxContainer.new()
_debug_actions.name = "DebugActions" _debug_actions.name = "DebugActions"
_debug_actions.add_theme_constant_override("separation", 6) _debug_actions.add_theme_constant_override("separation", 6)
@ -229,6 +240,11 @@ func _layout_debug() -> void:
if not is_instance_valid(_debug_scroll) or not is_instance_valid(_captain_panel): if not is_instance_valid(_debug_scroll) or not is_instance_valid(_captain_panel):
return return
var top := _captain_panel.position.y + _captain_panel.size.y + 6.0 var top := _captain_panel.position.y + _captain_panel.size.y + 6.0
if is_instance_valid(_passive_skills):
_passive_skills.position = Vector2(_captain_panel.position.x, top)
_passive_skills.size = Vector2(320, _passive_skills.get_combined_minimum_size().y)
if _passive_skills.visible:
top += _passive_skills.size.y + 8.0
debug_toggle.position = Vector2(12, top) debug_toggle.position = Vector2(12, top)
_debug_actions.position = Vector2(18, top + 30) _debug_actions.position = Vector2(18, top + 30)
_debug_actions.size = Vector2(310, _debug_actions.get_combined_minimum_size().y) _debug_actions.size = Vector2(310, _debug_actions.get_combined_minimum_size().y)
@ -265,8 +281,11 @@ func _layout_sidebar() -> void:
_cockpit.position = Vector2(($HUDRoot.size.x - _cockpit.size.x * 0.85) * 0.5, $HUDRoot.size.y - _cockpit.size.y * 0.85 - 12) _cockpit.position = Vector2(($HUDRoot.size.x - _cockpit.size.x * 0.85) * 0.5, $HUDRoot.size.y - _cockpit.size.y * 0.85 - 12)
if _ability_bar != null: if _ability_bar != null:
var abilities := _ability_bar.get_parent() as Control var abilities := _ability_bar.get_parent() as Control
abilities.offset_bottom = _cockpit.position.y - $HUDRoot.size.y - 14.0 abilities.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
abilities.offset_top = abilities.offset_bottom - 44.0 abilities.position = Vector2(_cockpit.position.x + _cockpit.size.x * _cockpit.scale.x - 204.0, _cockpit.position.y - 40.0)
abilities.size = Vector2(204, 36)
if is_instance_valid(_captain_skills_ui):
_captain_skills_ui.call_deferred("_layout")
_layout_debug() _layout_debug()
_sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 136) _sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 136)
_sidebar.visible = _sidebar_open _sidebar.visible = _sidebar_open
@ -301,7 +320,7 @@ Tarcze: %.1f pkt/s
GameState.system_level, GameState.system_level,
int(game.call("get_remaining_enemies")), int(game.call("get_remaining_enemies")),
player.velocity.length(), player.velocity.length(),
float(player.get("energy_regeneration")) + float(player.call("get_solar_regeneration")), float(player.call("get_energy_regeneration")),
float(player.get("shield_regeneration")), float(player.get("shield_regeneration")),
_get_bonus_summary(), _get_bonus_summary(),
] ]
@ -382,8 +401,6 @@ Kredyty: %+.1f%%""".replace("\r", "") % [
] ]
func _create_ability_bar() -> void: func _create_ability_bar() -> void:
_ability_ready_style = _make_ability_style(true)
_ability_blocked_style = _make_ability_style(false)
var panel := PanelContainer.new() var panel := PanelContainer.new()
panel.name = "ActiveAbilities" panel.name = "ActiveAbilities"
$HUDRoot.add_child(panel) $HUDRoot.add_child(panel)
@ -395,22 +412,22 @@ func _create_ability_bar() -> void:
panel.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) panel.add_theme_stylebox_override("panel", StyleBoxEmpty.new())
_ability_bar = HBoxContainer.new() _ability_bar = HBoxContainer.new()
_ability_bar.name = "AbilityButtons" _ability_bar.name = "AbilityButtons"
_ability_bar.alignment = BoxContainer.ALIGNMENT_CENTER _ability_bar.alignment = BoxContainer.ALIGNMENT_END
_ability_bar.add_theme_constant_override("separation", 4) _ability_bar.add_theme_constant_override("separation", 6)
panel.add_child(_ability_bar) panel.add_child(_ability_bar)
for index in ShipInventory.EXTENSION_SLOT_COUNT: for index in ShipInventory.EXTENSION_SLOT_COUNT:
var button := Button.new() var button := Button.new()
button.set_script(preload("res://scripts/ui/captain_skill_button.gd"))
button.name = "Ability%d" % (index + 1) button.name = "Ability%d" % (index + 1)
button.custom_minimum_size = Vector2(44, 44) button.call("configure", null, Color("91dbea"))
button.expand_icon = true
button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
button.add_theme_constant_override("icon_max_width", 30)
button.focus_mode = Control.FOCUS_NONE
var shortcut := Label.new() var shortcut := Label.new()
shortcut.text = str(index + 1) shortcut.text = str(index + 1)
shortcut.position = Vector2(3, 1) shortcut.position = Vector2(4, 2)
shortcut.mouse_filter = Control.MOUSE_FILTER_IGNORE shortcut.mouse_filter = Control.MOUSE_FILTER_IGNORE
shortcut.add_theme_font_size_override("font_size", 9) shortcut.add_theme_font_size_override("font_size", 9)
shortcut.add_theme_font_override("font", ThemeDB.fallback_font)
shortcut.add_theme_color_override("font_outline_color", Color("030810"))
shortcut.add_theme_constant_override("outline_size", 4)
button.add_child(shortcut) button.add_child(shortcut)
button.add_theme_font_size_override("font_size", 11) button.add_theme_font_size_override("font_size", 11)
button.pressed.connect(func() -> void: player.call("activate_ability", index)) button.pressed.connect(func() -> void: player.call("activate_ability", index))
@ -430,22 +447,69 @@ func _update_ability_bar() -> void:
var reason: String = player.call("ability_unavailable_reason", index) var reason: String = player.call("ability_unavailable_reason", index)
button.disabled = not reason.is_empty() button.disabled = not reason.is_empty()
var item: ItemInstance = player.get_node("ShipInventory").get_item("extension", index) var item: ItemInstance = player.get_node("ShipInventory").get_item("extension", index)
button.icon = item.definition.icon button.call("configure", item.definition.icon, Color("91dbea"))
button.text = "" button.call("set_visual_state", 0.0, false, not reason.is_empty())
button.add_theme_color_override("icon_disabled_color", Color(0.45, 0.5, 0.55, 0.65))
var style := _ability_ready_style if reason.is_empty() else _ability_blocked_style
button.add_theme_stylebox_override("normal", style)
button.add_theme_stylebox_override("disabled", style)
button.add_theme_stylebox_override("hover", style)
button.add_theme_color_override("font_color", Color("a5ffda"))
button.add_theme_color_override("font_disabled_color", Color("a3aebb"))
button.tooltip_text = "[%d] %s\n" % [index + 1, ability.display_name] + ability.description + ("\n" + reason if not reason.is_empty() else "\nGotowe") 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 _ability_bar.get_parent().visible = any_equipped
func _make_ability_style(ready: bool) -> StyleBoxFlat: func _refresh_passive_skills() -> void:
if not is_instance_valid(_passive_skills):
return
for child in _passive_skills.get_children():
_passive_skills.remove_child(child)
child.queue_free()
var catalog := preload("res://scripts/captain/captain_skills.gd")
for skill in catalog.IDS:
var rank := GameState.captain.get_skill_rank(skill)
if rank <= 0 or bool(catalog.DATA[skill]["active"]):
continue
var accent := Color(catalog.DATA[skill]["color"])
var card := PanelContainer.new()
card.name = skill
card.custom_minimum_size = Vector2(320, 54)
card.tooltip_text = catalog.description(skill, rank)
var style := StyleBoxFlat.new() var style := StyleBoxFlat.new()
style.bg_color = Color("174237") if ready else Color("202731") style.bg_color = Color(0.025, 0.042, 0.064, 0.94)
style.border_color = Color("77e8bc") if ready else Color("526071") style.border_color = accent.darkened(0.6)
style.set_border_width_all(2 if ready else 1) style.set_border_width_all(1)
style.set_content_margin_all(6) style.border_width_left = 2
return style style.set_corner_radius_all(7)
style.set_content_margin_all(8)
card.add_theme_stylebox_override("panel", style)
_passive_skills.add_child(card)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
card.add_child(row)
var icon := Button.new()
icon.set_script(preload("res://scripts/ui/captain_skill_button.gd"))
icon.call("configure", catalog.icon(skill), accent)
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.disabled = true
row.add_child(icon)
var contents := VBoxContainer.new()
contents.size_flags_horizontal = Control.SIZE_EXPAND_FILL
contents.add_theme_constant_override("separation", 3)
row.add_child(contents)
var header := HBoxContainer.new()
contents.add_child(header)
var title := _passive_label(catalog.DATA[skill]["name"], 14, Color("dfedf4"))
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(title)
header.add_child(_passive_label("●".repeat(rank) + "○".repeat(3 - rank), 10, accent))
var value := roundi(GameState.captain.get_skill_bonus(skill) * 100)
var summary := ""
match skill:
"business_shark": summary = "−%d%% kosztów zakupów" % value
"efficient_magazine": summary = "−%d%% zużycia amunicji" % value
"miner": summary = "+%d surowca / asteroidę" % roundi(GameState.captain.get_skill_bonus(skill))
contents.add_child(_passive_label(summary, 12, accent))
_passive_skills.visible = _passive_skills.get_child_count() > 0
_layout_debug.call_deferred()
func _passive_label(text: String, font_size: int, color: Color) -> Label:
var label := Label.new()
label.text = text
label.add_theme_font_override("font", ThemeDB.fallback_font)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", color)
return label

View File

@ -137,6 +137,12 @@ func get_remaining_enemies() -> int:
remaining += 1 remaining += 1
return remaining return remaining
func is_boss_alive() -> bool:
for enemy in get_tree().get_nodes_in_group("enemies"):
if is_ancestor_of(enemy) and not enemy.is_queued_for_deletion() and int(enemy.get("loot_tier")) == 2 and float(enemy.get("hull")) > 0.0:
return true
return false
func _physics_process(_delta: float) -> void: func _physics_process(_delta: float) -> void:
if _transitioning or bool(player.get("_is_destroyed")): if _transitioning or bool(player.get("_is_destroyed")):
services_menu.call("hide_services") services_menu.call("hide_services")

View File

@ -166,13 +166,16 @@ func take_damage(amount: float, _critical: bool = false, damage_type: int = Dama
_destroy_ship() _destroy_ship()
func restore_shields_from_damage(actual_damage: float) -> void: func restore_shields_from_damage(actual_damage: float) -> void:
if _is_destroyed or actual_damage <= 0.0 or shield_on_damage_percent <= 0.0: if _is_destroyed or actual_damage <= 0.0 or shield_on_damage_percent <= 0.0 or GameState.captain.is_skill_active("power_redirect"):
return return
shields = minf(max_shields, shields + actual_damage * shield_on_damage_percent / 100.0) shields = minf(max_shields, shields + actual_damage * shield_on_damage_percent / 100.0)
_update_shield_visual() _update_shield_visual()
func get_evasion_chance() -> float: func get_evasion_chance() -> float:
return GameState.captain.get_skill_bonus("piloting") return GameState.captain.get_skill_bonus("tactical_maneuver") if GameState.captain.is_skill_active("tactical_maneuver") else 0.0
func get_energy_regeneration() -> float:
return (energy_regeneration + get_solar_regeneration()) * GameState.captain.energy_regeneration_multiplier()
func get_damage_reduction() -> float: func get_damage_reduction() -> float:
return GameState.captain.get_skill_bonus("mechanic") return GameState.captain.get_skill_bonus("mechanic")
@ -254,11 +257,15 @@ func _restart_level_after_destruction() -> void:
get_tree().reload_current_scene.call_deferred() get_tree().reload_current_scene.call_deferred()
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
if not _is_destroyed:
GameState.captain.tick_skills(delta)
if GameState.captain.is_skill_active("power_redirect"):
shields = 0.0
if _is_destroyed or is_docked: if _is_destroyed or is_docked:
_mini_boost_active = false _mini_boost_active = false
return return
_contact_damage_timer = maxf(0.0, _contact_damage_timer - delta) _contact_damage_timer = maxf(0.0, _contact_damage_timer - delta)
energy = minf(max_energy, energy + (energy_regeneration + get_solar_regeneration()) * delta) energy = minf(max_energy, energy + get_energy_regeneration() * delta)
var was_mini_boosting := _mini_boost_active var was_mini_boosting := _mini_boost_active
_update_mini_boost(delta) _update_mini_boost(delta)
if _mini_boost_active: if _mini_boost_active:
@ -277,7 +284,7 @@ func _physics_process(delta: float) -> void:
_boost_speed = move_toward(_boost_speed, 0.0, space_drag * delta) _boost_speed = move_toward(_boost_speed, 0.0, space_drag * delta)
if was_boosting: if was_boosting:
velocity = velocity.move_toward(Vector2.ZERO, space_drag * delta) velocity = velocity.move_toward(Vector2.ZERO, space_drag * delta)
if shield_regeneration > 0.0 and shields < max_shields: if shield_regeneration > 0.0 and shields < max_shields and not GameState.captain.is_skill_active("power_redirect"):
var previous_shields := shields var previous_shields := shields
shields = minf(max_shields, shields + shield_regeneration * delta) shields = minf(max_shields, shields + shield_regeneration * delta)
if shields != previous_shields: if shields != previous_shields:

View File

@ -75,6 +75,7 @@ func preserve_for_restart() -> void:
func consume_ammunition() -> bool: func consume_ammunition() -> bool:
if ammunition <= 0: if ammunition <= 0:
return false return false
if GameState.captain.should_consume_ammunition("ordinary"):
ammunition -= 1 ammunition -= 1
ammunition_changed.emit() ammunition_changed.emit()
return true return true
@ -98,7 +99,7 @@ func get_item(slot_kind: String, index: int) -> ItemInstance:
return slots[index] return slots[index]
func get_ammunition_buy_cost() -> int: func get_ammunition_buy_cost() -> int:
return AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level) return GameState.captain.discounted_price(AMMUNITION_BUY_BASE_COST * maxi(1, GameState.system_level))
func can_buy_ammunition() -> bool: func can_buy_ammunition() -> bool:
return GameState.captain.credits >= get_ammunition_buy_cost() return GameState.captain.credits >= get_ammunition_buy_cost()
@ -182,7 +183,7 @@ func can_craft_missile() -> bool:
return GameState.captain.parts >= MISSILE_CRAFT_PARTS_COST return GameState.captain.parts >= MISSILE_CRAFT_PARTS_COST
func get_missile_buy_cost() -> int: func get_missile_buy_cost() -> int:
return MISSILE_BUY_BASE_COST * maxi(1, GameState.system_level) return GameState.captain.discounted_price(MISSILE_BUY_BASE_COST * maxi(1, GameState.system_level))
func can_buy_missile() -> bool: func can_buy_missile() -> bool:
return GameState.captain.credits >= get_missile_buy_cost() return GameState.captain.credits >= get_missile_buy_cost()
@ -345,6 +346,7 @@ func _weapon_dps(item: ItemInstance, bonuses: Dictionary) -> float:
func consume_irradiated_ammunition() -> bool: func consume_irradiated_ammunition() -> bool:
if irradiated_ammunition <= 0: if irradiated_ammunition <= 0:
return false return false
if GameState.captain.should_consume_ammunition("irradiated"):
irradiated_ammunition -= 1 irradiated_ammunition -= 1
ammunition_changed.emit() ammunition_changed.emit()
return true return true
@ -363,12 +365,15 @@ func craft_irradiated_ammunition() -> bool:
return true return true
func can_buy_irradiated_ammunition() -> bool: func can_buy_irradiated_ammunition() -> bool:
return GameState.captain.credits >= 15 * maxi(1, GameState.system_level) return GameState.captain.credits >= get_irradiated_ammunition_buy_cost()
func get_irradiated_ammunition_buy_cost() -> int:
return GameState.captain.discounted_price(15 * maxi(1, GameState.system_level))
func buy_irradiated_ammunition() -> bool: func buy_irradiated_ammunition() -> bool:
if not can_buy_irradiated_ammunition(): if not can_buy_irradiated_ammunition():
return false return false
GameState.captain.credits -= 15 * maxi(1, GameState.system_level) GameState.captain.credits -= get_irradiated_ammunition_buy_cost()
irradiated_ammunition += 10 irradiated_ammunition += 10
GameState.captain.emit_changed() GameState.captain.emit_changed()
ammunition_changed.emit() ammunition_changed.emit()

View File

@ -113,7 +113,7 @@ func claim() -> bool:
var summary := "" var summary := ""
var scale := reward_scale() var scale := reward_scale()
if kind == "asteroid": if kind == "asteroid":
var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale)) var amount := maxi(1, roundi(_rng.randi_range(1, 3) * scale)) + roundi(GameState.captain.get_skill_bonus("miner"))
GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount GameState.captain.minerals[_mineral] = int(GameState.captain.minerals.get(_mineral, 0)) + amount
summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu"}[_mineral]] summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu"}[_mineral]]
elif kind == "ruin": elif kind == "ruin":

View File

@ -0,0 +1,74 @@
extends Button
const ICON_SHADER := preload("res://resources/ui/skill_icon.gdshader")
var _icon_material: ShaderMaterial
var _normal: StyleBoxFlat
var _disabled: StyleBoxFlat
var _active: StyleBoxFlat
var _was_active := false
var _artwork: Texture2D
func configure(artwork: Texture2D, accent: Color) -> void:
if _normal != null and _artwork == artwork:
return
_artwork = artwork
_was_active = false
text = ""
var old_picture := get_node_or_null("RoundedArtwork")
if old_picture != null:
remove_child(old_picture)
old_picture.queue_free()
custom_minimum_size = Vector2(36, 36)
focus_mode = Control.FOCUS_NONE
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
_normal = _frame(Color("405c70"), Color("0a1420"))
_disabled = _frame(Color("2d4253"), Color("09111c"))
_active = _frame(Color("a9efb4"), Color("142b28"))
add_theme_stylebox_override("normal", _normal)
add_theme_stylebox_override("disabled", _disabled)
add_theme_stylebox_override("hover", _frame(accent.lightened(0.3), Color("162638")))
add_theme_stylebox_override("pressed", _frame(Color("e5f6ff"), Color("1b3445")))
add_theme_stylebox_override("focus", StyleBoxEmpty.new())
if artwork == null:
text = "·"
add_theme_color_override("font_disabled_color", Color("4a6376"))
return
var picture := TextureRect.new()
picture.name = "RoundedArtwork"
picture.texture = artwork
picture.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
picture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
picture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
picture.mouse_filter = Control.MOUSE_FILTER_IGNORE
_icon_material = ShaderMaterial.new()
_icon_material.shader = ICON_SHADER
picture.material = _icon_material
add_child(picture)
move_child(picture, 0)
picture.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
picture.offset_left = 2
picture.offset_top = 2
picture.offset_right = -2
picture.offset_bottom = -2
picture.resized.connect(func() -> void: _icon_material.set_shader_parameter("icon_size", picture.size))
func set_visual_state(cooldown_fraction: float, active: bool, blocked: bool) -> void:
if _icon_material != null:
_icon_material.set_shader_parameter("cooldown", clampf(cooldown_fraction, 0.0, 1.0))
_icon_material.set_shader_parameter("dimming", 0.0 if active else (0.6 if blocked else 0.0))
if active != _was_active:
_was_active = active
add_theme_stylebox_override("normal", _active if active else _normal)
add_theme_stylebox_override("disabled", _active if active else _disabled)
func _frame(border: Color, background: Color) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(1)
style.set_corner_radius_all(6)
style.set_content_margin_all(0)
style.shadow_color = Color(0.0, 0.0, 0.0, 0.35)
style.shadow_size = 2
style.shadow_offset = Vector2(0, 1)
return style

View File

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

View File

@ -0,0 +1,324 @@
extends CanvasLayer
const CATALOG := preload("res://scripts/captain/captain_skills.gd")
const BUTTON_STYLE := preload("res://scripts/ui/hud_button_style.gd")
var player: CharacterBody2D
var _captain: CaptainProgress
var _screen: Control
var _panel: PanelContainer
var _contents: VBoxContainer
var _bar: HBoxContainer
var _buttons: Dictionary = {}
var _was_paused := false
var _owns_pause := false
var _refresh_pending := false
func _ready() -> void:
name = "CaptainSkills"
layer = 40
process_mode = Node.PROCESS_MODE_ALWAYS
_captain = GameState.captain
_captain.normalize_equipped_skills()
_screen = Control.new()
_screen.name = "SkillManagement"
add_child(_screen)
_screen.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_screen.hide()
var dim := ColorRect.new()
dim.color = Color(0.015, 0.025, 0.045, 0.9)
_screen.add_child(dim)
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_panel = PanelContainer.new()
_panel.name = "Panel"
_panel.add_theme_stylebox_override("panel", _style(Color("567b99"), Color("0a1422"), 22))
_screen.add_child(_panel)
_panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
_contents = VBoxContainer.new()
_contents.add_theme_constant_override("separation", 14)
_panel.add_child(_contents)
_bar = HBoxContainer.new()
_bar.name = "CaptainActiveSkills"
_bar.add_theme_constant_override("separation", 6)
add_child(_bar)
_captain.changed.connect(_queue_refresh)
get_viewport().size_changed.connect(_layout)
_queue_refresh()
func _queue_refresh() -> void:
if not _refresh_pending:
_refresh_pending = true
_refresh.call_deferred()
func _label(text: String, font_size: int, color: Color = Color("dae8ef")) -> Label:
var label := Label.new()
label.text = text
label.add_theme_font_override("font", ThemeDB.fallback_font)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", color)
return label
func _style(border: Color, background: Color, margin: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(1)
style.set_corner_radius_all(10)
style.set_content_margin_all(margin)
return style
func _refresh() -> void:
_refresh_pending = false
_captain.prepare_skill_offers()
_rebuild_bar()
if not _captain.skill_offers.is_empty() and not bool(player.get("_is_destroyed")):
open_management()
elif _screen.visible:
_build_management()
func open_management() -> void:
if bool(player.get("_is_destroyed")):
return
if not _screen.visible:
_was_paused = get_tree().paused
_owns_pause = true
get_tree().paused = true
_screen.show()
_build_management()
func _close() -> void:
if not _captain.skill_offers.is_empty():
return
_screen.hide()
if _owns_pause:
get_tree().paused = _was_paused
_owns_pause = false
func _build_management() -> void:
for child in _contents.get_children():
_contents.remove_child(child)
child.queue_free()
var choosing := not _captain.skill_offers.is_empty()
var heading := HBoxContainer.new()
_contents.add_child(heading)
var title := _label("AWANS KAPITANA" if choosing else "UMIEJĘTNOŚCI KAPITANA", 25, Color("91dbea"))
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
heading.add_child(title)
if not choosing:
var close := Button.new()
close.name = "Close"
close.text = "WRÓĆ DO LOTU"
close.pressed.connect(_close)
heading.add_child(close)
_contents.add_child(_label("POZIOM %d · GRA WSTRZYMANA" % _captain.level, 13, Color("91aabd")))
if choosing:
_contents.add_child(_label("Wybierz jedną umiejętność. Wybór obowiązuje przez całą rozgrywkę.", 16))
var choices := HBoxContainer.new()
choices.name = "Choices"
choices.add_theme_constant_override("separation", 14)
_contents.add_child(choices)
for skill in _captain.skill_offers:
choices.add_child(_choice_card(skill))
if _captain.learning_points > 1:
_contents.add_child(_label("Pozostałe wybory po tym awansie: %d" % (_captain.learning_points - 1), 13, Color("efb568")))
_contents.add_child(HSeparator.new())
_contents.add_child(_label("POSIADANE UMIEJĘTNOŚCI", 14, Color("91dbea")))
var owned := GridContainer.new()
owned.columns = 3
owned.add_theme_constant_override("h_separation", 12)
owned.add_theme_constant_override("v_separation", 10)
_contents.add_child(owned)
for skill in CATALOG.IDS:
var rank := _captain.get_skill_rank(skill)
if rank <= 0:
continue
var row := HBoxContainer.new()
row.custom_minimum_size.x = 260
row.tooltip_text = CATALOG.description(skill, rank)
owned.add_child(row)
row.add_child(_icon(skill, 38))
var info := VBoxContainer.new()
row.add_child(info)
info.add_child(_label(CATALOG.DATA[skill]["name"], 13))
info.add_child(_label("%s · %s" % ["●".repeat(rank) + "○".repeat(3 - rank), "AKTYWNA" if CATALOG.DATA[skill]["active"] else "PASYWNA"], 11, Color(CATALOG.DATA[skill]["color"])))
if owned.get_child_count() == 0:
_contents.add_child(_label("Nie masz jeszcze umiejętności. To początek Twojej specjalizacji.", 14, Color("91aabd")))
_build_loadout()
_layout.call_deferred()
func _build_loadout() -> void:
_contents.add_child(HSeparator.new())
_contents.add_child(_label("AKTYWNE SLOTY F1–F5 · MAKSYMALNIE 5 UMIEJĘTNOŚCI", 13, Color("91dbea")))
var row := HBoxContainer.new()
row.name = "ActiveSkillLoadout"
row.add_theme_constant_override("separation", 10)
_contents.add_child(row)
for slot in CaptainProgress.ACTIVE_SKILL_SLOTS:
var column := VBoxContainer.new()
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(column)
column.add_child(_label("F%d" % (slot + 1), 12, Color("efb568")))
var picker := OptionButton.new()
picker.name = "SlotF%d" % (slot + 1)
picker.fit_to_longest_item = false
picker.custom_minimum_size = Vector2(155, 36)
picker.add_theme_font_override("font", ThemeDB.fallback_font)
picker.add_theme_font_size_override("font_size", 12)
picker.add_theme_constant_override("icon_max_width", 26)
picker.get_popup().add_theme_constant_override("icon_max_width", 28)
picker.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
picker.add_item("Pusty slot")
picker.set_item_metadata(0, "")
for skill in CATALOG.IDS:
if CATALOG.DATA[skill]["active"] and _captain.get_skill_rank(skill) > 0:
picker.add_icon_item(CATALOG.icon(skill), CATALOG.DATA[skill]["name"])
var index := picker.item_count - 1
picker.set_item_metadata(index, skill)
if _captain.equipped_skills[slot] == skill:
picker.select(index)
picker.tooltip_text = "Wybierz aktywną umiejętność dla F%d. Pasywne działają bez zajmowania slotu." % (slot + 1)
picker.item_selected.connect(func(index: int) -> void: _captain.equip_skill(slot, str(picker.get_item_metadata(index))))
column.add_child(picker)
func _icon(skill: String, dimensions: float) -> TextureRect:
var icon := TextureRect.new()
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
icon.texture = CATALOG.icon(skill)
icon.custom_minimum_size = Vector2.ONE * dimensions
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
return icon
func _choice_card(skill: String) -> PanelContainer:
var rank := _captain.get_skill_rank(skill)
var next_rank := rank + 1
var data: Dictionary = CATALOG.DATA[skill]
var color := Color(data["color"])
var card := PanelContainer.new()
card.name = skill
card.custom_minimum_size = Vector2(266, 340)
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card.add_theme_stylebox_override("panel", _style(color.darkened(0.4), Color("101e2e"), 16))
var body := VBoxContainer.new()
body.add_theme_constant_override("separation", 10)
card.add_child(body)
body.add_child(_label("NOWA UMIEJĘTNOŚĆ" if rank == 0 else "ULEPSZENIE %s → %s" % [str(rank), str(next_rank)], 11, color))
body.add_child(_icon(skill, 76))
var title := _label(data["name"], 19)
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
body.add_child(title)
body.add_child(_label("POZIOM %s / III · %s" % [["I", "II", "III"][next_rank - 1], "AKTYWNA" if data["active"] else "PASYWNA"], 11, color))
var description := _label(CATALOG.description(skill, next_rank), 14)
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
description.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_child(description)
if data["active"]:
body.add_child(_label("DZIAŁANIE 5 s · ODNOWIENIE 30 s", 11, Color("91aabd")))
else:
body.add_child(_label("STAŁY EFEKT", 11, Color("91aabd")))
var select := Button.new()
select.name = "Choose"
select.text = "WYBIERZ" if rank == 0 else "ULEPSZ DO %s" % ["I", "II", "III"][next_rank - 1]
select.custom_minimum_size.y = 38
select.focus_mode = Control.FOCUS_NONE
BUTTON_STYLE.apply(select, color)
select.pressed.connect(_choose.bind(skill))
body.add_child(select)
return card
func _choose(skill: String) -> void:
if not _captain.train_skill(skill):
return
# Clear any held mouse fire state before resuming the simulation.
player.get_node("WeaponController").set("_fire_buttons_down", [false, false, false])
player.get_node("WeaponController").set("_fire_just_pressed", [false, false, false])
if _captain.skill_offers.is_empty():
_close()
func _rebuild_bar() -> void:
for child in _bar.get_children():
_bar.remove_child(child)
child.queue_free()
_buttons.clear()
_captain.normalize_equipped_skills()
for index in CaptainProgress.ACTIVE_SKILL_SLOTS:
var skill := _captain.equipped_skills[index]
var column := VBoxContainer.new()
_bar.add_child(column)
var button := Button.new()
button.set_script(preload("res://scripts/ui/captain_skill_button.gd"))
button.name = "SlotF%d" % (index + 1)
button.call("configure", CATALOG.icon(skill) if not skill.is_empty() else null, Color(CATALOG.DATA[skill]["color"]) if not skill.is_empty() else Color("344657"))
button.disabled = skill.is_empty()
button.pressed.connect(_activate.bind(skill))
column.add_child(button)
var binding := _label("F%d" % (index + 1), 9)
binding.position = Vector2(4, 2)
binding.mouse_filter = Control.MOUSE_FILTER_IGNORE
binding.add_theme_color_override("font_outline_color", Color("030810"))
binding.add_theme_constant_override("outline_size", 4)
button.add_child(binding)
var status := _label("", 9)
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
status.mouse_filter = Control.MOUSE_FILTER_IGNORE
status.add_theme_color_override("font_outline_color", Color("030810"))
status.add_theme_constant_override("outline_size", 4)
button.add_child(status)
status.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT)
status.offset_left = -33
status.offset_top = -14
status.offset_right = -3
status.offset_bottom = -1
_buttons[index] = {"button": button, "status": status, "skill": skill, "key": KEY_F1 + index, "binding": "F%d" % (index + 1)}
_layout.call_deferred()
func _activate(skill: String) -> void:
if not get_tree().paused:
_captain.activate_skill(skill, player)
func _unhandled_key_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_ESCAPE and _screen.visible:
_close()
get_viewport().set_input_as_handled()
elif not get_tree().paused:
for index in _buttons:
if event.physical_keycode == _buttons[index]["key"] or event.keycode == _buttons[index]["key"]:
_activate(_buttons[index]["skill"])
get_viewport().set_input_as_handled()
func _process(_delta: float) -> void:
for index in _buttons:
var entry: Dictionary = _buttons[index]
var skill: String = entry["skill"]
var button: Button = entry["button"]
var status: Label = entry["status"]
if skill.is_empty():
button.tooltip_text = "[%s] Pusty slot — przypisz skill w panelu kapitana" % entry["binding"]
continue
var duration := float(_captain.skill_durations.get(skill, 0.0))
var cooldown := float(_captain.skill_cooldowns.get(skill, 0.0))
var reason := _captain.skill_unavailable_reason(skill, player)
button.disabled = get_tree().paused or not reason.is_empty()
button.call("set_visual_state", cooldown / CATALOG.COOLDOWN, duration > 0.0, button.disabled)
button.tooltip_text = "%s · poziom %d\n%s\n[%s] · Odnowienie 30 s%s" % [CATALOG.DATA[skill]["name"], _captain.get_skill_rank(skill), CATALOG.description(skill, _captain.get_skill_rank(skill)), entry["binding"], "\n" + reason if not reason.is_empty() else ""]
status.text = "%.1f" % duration if duration > 0.0 else (str(ceili(cooldown)) if cooldown > 0.0 else "")
status.modulate = Color("a9ef83") if duration > 0.0 else Color("dae8ef")
_bar.visible = not bool(player.get("_is_destroyed")) and not _screen.visible
func _layout() -> void:
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))
_panel.size = base_size
_panel.scale = Vector2.ONE * ui_scale
_panel.position = (viewport_size - base_size * ui_scale) * 0.5
var cockpit := get_parent().get("_cockpit") as Control
if is_instance_valid(cockpit):
_bar.position = cockpit.position + Vector2(0, -40)
else:
_bar.position = Vector2(16, viewport_size.y - 190)
func _exit_tree() -> void:
if _owns_pause and is_inside_tree():
get_tree().paused = _was_paused

View File

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

View File

@ -48,6 +48,7 @@ func _draw() -> void:
var cell := (size.x - 238) / 3.0 var cell := (size.x - 238) / 3.0
var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))] var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))]
var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"] var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"]
var boss_alive := bool(game.call("is_boss_alive"))
for index in 3: for index in 3:
var x := 218.0 + index * cell var x := 218.0 + index * cell
var color := AMBER if index == 0 else (CYAN if index == 1 else Color("ff8989")) var color := AMBER if index == 0 else (CYAN if index == 1 else Color("ff8989"))
@ -55,6 +56,8 @@ func _draw() -> void:
if index == 1: if index == 1:
_text("ZWYKŁE %02d" % _inventory.ammunition, Vector2(x, 35), cell, 11, CYAN) _text("ZWYKŁE %02d" % _inventory.ammunition, Vector2(x, 35), cell, 11, CYAN)
_text("GAMMA %02d" % _inventory.irradiated_ammunition, Vector2(x, 49), cell, 11, Color("a9ef83")) _text("GAMMA %02d" % _inventory.irradiated_ammunition, Vector2(x, 49), cell, 11, Color("a9ef83"))
elif index == 2 and boss_alive:
_text("BOSS", Vector2(x, 45), cell, 21, Color("ff8989"))
else: else:
_text(str(values[index]).pad_zeros(2), Vector2(x, 45), cell, 21, color) _text(str(values[index]).pad_zeros(2), Vector2(x, 45), cell, 21, color)
if index < 2: if index < 2:

View File

@ -85,7 +85,7 @@ func _position_at_planet() -> void:
func _refresh_ammunition_service() -> void: func _refresh_ammunition_service() -> void:
if _gamma_buy != null: if _gamma_buy != null:
_gamma_buy.text = "KUP 10 NAPROMIENIOWANEJ — %d KREDYTÓW" % (15 * maxi(1, GameState.system_level)) _gamma_buy.text = "KUP 10 NAPROMIENIOWANEJ — %d KREDYTÓW" % (_inventory.get_irradiated_ammunition_buy_cost() if _inventory else 15 * maxi(1, GameState.system_level))
_gamma_buy.disabled = _inventory == null or not _inventory.can_buy_irradiated_ammunition() _gamma_buy.disabled = _inventory == null or not _inventory.can_buy_irradiated_ammunition()
_missile_button.disabled = _inventory == null or not _inventory.can_buy_missile() _missile_button.disabled = _inventory == null or not _inventory.can_buy_missile()
_missile_button.text = "KUP +1 RAKIETĘ — %d KREDYTÓW" % (_inventory.get_missile_buy_cost() if _inventory else ShipInventory.MISSILE_BUY_BASE_COST) _missile_button.text = "KUP +1 RAKIETĘ — %d KREDYTÓW" % (_inventory.get_missile_buy_cost() if _inventory else ShipInventory.MISSILE_BUY_BASE_COST)
@ -95,7 +95,7 @@ func _refresh_ammunition_service() -> void:
buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition() buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition()
if _inventory: if _inventory:
buy_ammunition_button.text = "KUP %d AMUNICJI — %d KREDYTÓW" % [ShipInventory.AMMUNITION_BUY_AMOUNT, _inventory.get_ammunition_buy_cost()] buy_ammunition_button.text = "KUP %d AMUNICJI — %d KREDYTÓW" % [ShipInventory.AMMUNITION_BUY_AMOUNT, _inventory.get_ammunition_buy_cost()]
services_label.text = "Amunicja: %d • Kredyty: %d\nCena: %d kredytów × poziom układu (%d)" % [_inventory.ammunition, GameState.captain.credits, ShipInventory.AMMUNITION_BUY_BASE_COST, GameState.system_level] services_label.text = "Amunicja: %d • Kredyty: %d\nRekin biznesu: −%d%% ceny zakupów" % [_inventory.ammunition, GameState.captain.credits, roundi(GameState.captain.get_skill_bonus("business_shark") * 100)]
func _buy_ammunition() -> void: func _buy_ammunition() -> void:
if visible and _inventory: if visible and _inventory:

View File

@ -0,0 +1,207 @@
extends Node
const CATALOG := preload("res://scripts/captain/captain_skills.gd")
const PROGRESS := preload("res://scripts/captain/captain_progress.gd")
const POI := preload("res://scripts/levels/exploration_poi.gd")
func _ready() -> void:
_test_offers()
GameState.start_new_run()
var game := preload("res://scenes/game/game.tscn").instantiate()
add_child(game)
game.set_physics_process(false)
var ship: CharacterBody2D = game.get_node("PlayerShip")
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)
var captain := GameState.captain
var inventory: ShipInventory = ship.get_node("ShipInventory")
for rank in range(1, 4):
captain.skills["business_shark"] = rank
assert(inventory.get_ammunition_buy_cost() == [18, 14, 8][rank - 1])
assert(inventory.get_missile_buy_cost() == [9, 7, 4][rank - 1])
assert(inventory.get_irradiated_ammunition_buy_cost() == [14, 11, 6][rank - 1])
captain.skills["efficient_magazine"] = rank
captain._ammo_credit.clear()
inventory.ammunition = 100
inventory.irradiated_ammunition = 100
for shot in 100:
assert(inventory.consume_ammunition())
assert(inventory.consume_irradiated_ammunition())
assert(inventory.ammunition == [10, 20, 50][rank - 1])
assert(inventory.irradiated_ammunition == [10, 20, 50][rank - 1])
inventory.missile_ammunition = 2
assert(inventory.consume_missile())
assert(inventory.missile_ammunition == 1)
captain.skills["reactor_overload"] = rank
captain.skills["tactical_maneuver"] = rank
captain.skills["power_redirect"] = rank
assert(captain.equip_skill(0, "reactor_overload"))
assert(captain.equip_skill(1, "tactical_maneuver"))
assert(captain.equip_skill(2, "power_redirect"))
captain.skill_cooldowns.clear()
captain.skill_durations.clear()
var base_regeneration: float = ship.get_energy_regeneration()
assert(captain.activate_skill("reactor_overload", ship))
assert(is_equal_approx(ship.get_energy_regeneration(), base_regeneration * [1.1, 1.2, 1.7][rank - 1]))
ship.energy = 0.0
ship._physics_process(0.1)
assert(is_equal_approx(ship.energy, base_regeneration * [1.1, 1.2, 1.7][rank - 1] * 0.1))
assert(not captain.activate_skill("reactor_overload", ship))
assert(captain.activate_skill("tactical_maneuver", ship))
assert(is_equal_approx(ship.get_evasion_chance(), [0.3, 0.6, 1.0][rank - 1]))
if rank == 3:
var before_hit: float = ship.shields
ship.take_damage(100.0)
assert(ship.shields == before_hit)
ship.shields = ship.max_shields * 0.24
assert(not captain.activate_skill("power_redirect", ship))
assert(not captain.skill_cooldowns.has("power_redirect"))
ship.shields = ship.max_shields * 0.25
assert(captain.activate_skill("power_redirect", ship))
assert(ship.shields == 0.0)
assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0 + [0.1, 0.2, 0.7][rank - 1] + [0.5, 0.9, 1.5][rank - 1]))
ship.shield_on_damage_percent = 100.0
ship.restore_shields_from_damage(100.0)
assert(ship.shields == 0.0)
var shield_ability := preload("res://scripts/abilities/shield_charge_ability.gd").new()
shield_ability.activate(ship)
assert(ship.shields == 0.0)
var previous_regeneration: float = ship.shield_regeneration
ship.shield_regeneration = 10.0
ship._physics_process(0.1)
assert(ship.shields == 0.0)
captain.tick_skills(4.9)
assert(not captain.is_skill_active("power_redirect"))
assert(ship.get_evasion_chance() == 0.0)
assert(is_equal_approx(captain.energy_regeneration_multiplier(), 1.0))
assert(is_equal_approx(captain.skill_cooldowns["reactor_overload"], 24.9))
ship._physics_process(0.1)
assert(ship.shields > 0.0)
ship.shield_regeneration = previous_regeneration
ship.restore_shields_from_damage(10.0)
assert(ship.shields > 0.0)
captain.tick_skills(25.0)
assert(captain.skill_cooldowns["reactor_overload"] == 0.0)
assert(captain.equipped_skills.size() == 5)
assert(not captain.equip_skill(5, "reactor_overload"))
assert(not captain.equip_skill(-1, "reactor_overload"))
assert(not captain.equip_skill(3, "miner"))
assert(not captain.equip_skill(3, "unknown"))
captain.skill_cooldowns["reactor_overload"] = 12.0
assert(captain.equip_skill(4, "reactor_overload"))
assert(captain.equipped_skills[0] == "" and captain.equipped_skills[4] == "reactor_overload")
assert(captain.skill_cooldowns["reactor_overload"] == 12.0)
assert(captain.equip_skill(4, ""))
captain.skill_cooldowns.clear()
assert(not captain.activate_skill("reactor_overload", ship))
assert(captain.equip_skill(4, "reactor_overload"))
captain.emit_changed()
await get_tree().process_frame
await get_tree().process_frame
var hotkey_ui := game.get_node("HUD/CaptainSkills")
var event := InputEventKey.new()
event.physical_keycode = KEY_F5
event.pressed = true
hotkey_ui._unhandled_key_input(event)
assert(captain.is_skill_active("reactor_overload"))
assert(captain.skill_cooldowns["reactor_overload"] == 30.0)
assert(hotkey_ui.get("_bar").get_child_count() == 5)
captain.credits = 1000
assert(inventory.buy_ammunition())
assert(captain.credits == 992)
_test_mining(game, ship)
inventory.ammunition = 0
inventory.irradiated_ammunition = 0
assert(not inventory.consume_ammunition() and not inventory.consume_irradiated_ammunition())
# Real level-up pauses, preserves the offers on reopen, and advances queued choices.
captain.skills.clear()
captain.normalize_equipped_skills()
captain.skill_cooldowns.clear()
captain.skill_durations.clear()
captain.add_rewards(250, 0)
await get_tree().process_frame
await get_tree().process_frame
var ui := game.get_node("HUD/CaptainSkills")
assert(get_tree().paused and captain.learning_points == 2)
assert(ui.get("_screen").visible)
var offers := captain.skill_offers.duplicate()
ui.open_management()
assert(offers == captain.skill_offers and offers.size() == 3)
ui._close()
assert(get_tree().paused)
var selected: String = offers[0]
ui._choose(selected)
assert(captain.get_skill_rank(selected) == 1 and get_tree().paused)
await get_tree().process_frame
ui._choose(captain.skill_offers[0])
assert(not get_tree().paused and captain.learning_points == 0)
await get_tree().process_frame
ui.open_management()
assert(get_tree().paused)
captain.skill_cooldowns["reactor_overload"] = 17.0
ship.set_physics_process(true)
await get_tree().create_timer(0.1, true).timeout
assert(captain.skill_cooldowns["reactor_overload"] == 17.0)
ship.set_physics_process(false)
ui._close()
var profile := GameState.captain
GameState.system_level = 2
game.queue_free()
await get_tree().process_frame
var next_system := preload("res://scenes/game/game.tscn").instantiate()
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)
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)")
get_tree().quit()
func _test_offers() -> void:
var profile := PROGRESS.new()
profile.add_rewards(250, 0)
assert(profile.level == 3 and profile.learning_points == 2)
assert(profile.skill_offers.size() == 3)
assert(not profile.train_skill("unknown"))
var choices := profile.skill_offers.duplicate()
profile.prepare_skill_offers()
assert(choices == profile.skill_offers)
assert(profile.train_skill(choices[0]))
assert(profile.get_skill_rank(choices[0]) == 1)
for skill in CATALOG.IDS:
profile.skills[skill] = 3
profile.skills["miner"] = 2
profile.skill_offers.clear()
profile.prepare_skill_offers()
assert(profile.skill_offers == ["miner"])
assert(profile.train_skill("miner"))
profile.add_rewards(1000, 0)
assert(profile.skill_offers.is_empty() and profile.learning_points == 0)
assert(not profile.can_reset_skills())
profile.normalize_skill_ranks()
assert(profile.get_skill_rank("miner") == 3)
func _test_mining(game: Node, ship: CharacterBody2D) -> void:
var results: Array[int] = []
for rank in range(4):
GameState.captain.skills["miner"] = rank
GameState.captain.minerals = {"iron": 0, "aluminium": 0, "titanium": 0, "uranium": 0}
var asteroid := StaticBody2D.new()
asteroid.set_script(POI)
asteroid.set("kind", "asteroid")
asteroid.set("player", ship)
asteroid.set("reward_seed", 123)
asteroid.set("poi_id", "captain_skill_test_%d" % rank)
game.add_child(asteroid)
assert(asteroid.claim())
var total := 0
for amount in GameState.captain.minerals.values():
total += int(amount)
results.append(total)
assert(results[1] == results[0] + 3 and results[2] == results[0] + 6 and results[3] == results[0] + 10)

View File

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

View File

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

View File

@ -18,6 +18,7 @@ func _ready() -> void:
enemy.set_physics_process(false) enemy.set_physics_process(false)
enemies.append(enemy) enemies.append(enemy)
assert(enemies.size() == 4) assert(enemies.size() == 4)
assert(not game.is_boss_alive())
for index in enemies.size() - 1: for index in enemies.size() - 1:
enemies[index].take_damage(100000.0) enemies[index].take_damage(100000.0)
game._physics_process(0.0) game._physics_process(0.0)
@ -40,6 +41,7 @@ func _ready() -> void:
player.set("_is_destroyed", false) player.set("_is_destroyed", false)
game._physics_process(0.0) game._physics_process(0.0)
assert(game._boss_spawned and game.get_remaining_enemies() == 1) assert(game._boss_spawned and game.get_remaining_enemies() == 1)
assert(game.is_boss_alive())
assert(not game.system_cleared and not game.services_menu.visible) assert(not game.system_cleared and not game.services_menu.visible)
var boss: CharacterBody2D var boss: CharacterBody2D
for enemy in get_tree().get_nodes_in_group("enemies"): for enemy in get_tree().get_nodes_in_group("enemies"):
@ -55,6 +57,7 @@ func _ready() -> void:
boss.take_damage(100000.0) boss.take_damage(100000.0)
game._physics_process(0.0) game._physics_process(0.0)
assert(game.system_cleared and game.get_remaining_enemies() == 0) assert(game.system_cleared and game.get_remaining_enemies() == 0)
assert(not game.is_boss_alive())
game._physics_process(0.0) game._physics_process(0.0)
assert(game.get_remaining_enemies() == 0) assert(game.get_remaining_enemies() == 0)
for definition in preload("res://scripts/items/item_generator.gd").DEFINITIONS: for definition in preload("res://scripts/items/item_generator.gd").DEFINITIONS: