Wizualen poprawki
27
assets/icons/captain/ARTWORK.md
Normal 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.
|
||||||
BIN
assets/icons/captain/business_shark.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
40
assets/icons/captain/business_shark.png.import
Normal 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
|
||||||
6
assets/icons/captain/business_shark.svg
Normal 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 |
43
assets/icons/captain/business_shark.svg.import
Normal 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
|
||||||
BIN
assets/icons/captain/efficient_magazine.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
40
assets/icons/captain/efficient_magazine.png.import
Normal 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
|
||||||
5
assets/icons/captain/efficient_magazine.svg
Normal 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 |
43
assets/icons/captain/efficient_magazine.svg.import
Normal 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
|
||||||
BIN
assets/icons/captain/miner.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
40
assets/icons/captain/miner.png.import
Normal 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
|
||||||
6
assets/icons/captain/miner.svg
Normal 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 |
43
assets/icons/captain/miner.svg.import
Normal 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
|
||||||
BIN
assets/icons/captain/power_redirect.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
40
assets/icons/captain/power_redirect.png.import
Normal 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
|
||||||
7
assets/icons/captain/power_redirect.svg
Normal 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 |
43
assets/icons/captain/power_redirect.svg.import
Normal 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
|
||||||
BIN
assets/icons/captain/reactor_overload.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
40
assets/icons/captain/reactor_overload.png.import
Normal 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
|
||||||
7
assets/icons/captain/reactor_overload.svg
Normal 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 |
43
assets/icons/captain/reactor_overload.svg.import
Normal 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
|
||||||
BIN
assets/icons/captain/tactical_maneuver.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
40
assets/icons/captain/tactical_maneuver.png.import
Normal 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
|
||||||
5
assets/icons/captain/tactical_maneuver.svg
Normal 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 |
43
assets/icons/captain/tactical_maneuver.svg.import
Normal 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
|
||||||
21
resources/ui/skill_icon.gdshader
Normal 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);
|
||||||
|
}
|
||||||
1
resources/ui/skill_icon.gdshader.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dprgc6tmi7yoh
|
||||||
@ -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")
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
39
scripts/captain/captain_skills.gd
Normal 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 ""
|
||||||
1
scripts/captain/captain_skills.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://cl3wkhfx6hl6r
|
||||||
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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":
|
||||||
|
|||||||
74
scripts/ui/captain_skill_button.gd
Normal 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
|
||||||
1
scripts/ui/captain_skill_button.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://b6cgtrcb6cg2m
|
||||||
324
scripts/ui/captain_skills_ui.gd
Normal 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
|
||||||
1
scripts/ui/captain_skills_ui.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://cs7ixgwu5l0qw
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
207
tests/captain/captain_skills_test.gd
Normal 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)
|
||||||
1
tests/captain/captain_skills_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://bnasxkcqdek5p
|
||||||
6
tests/captain/captain_skills_test.tscn
Normal 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")
|
||||||
@ -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:
|
||||||
|
|||||||