diff --git a/assets/icons/captain/ARTWORK.md b/assets/icons/captain/ARTWORK.md new file mode 100644 index 0000000..8e3323b --- /dev/null +++ b/assets/icons/captain/ARTWORK.md @@ -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. diff --git a/assets/icons/captain/business_shark.png b/assets/icons/captain/business_shark.png new file mode 100644 index 0000000..0d43b09 Binary files /dev/null and b/assets/icons/captain/business_shark.png differ diff --git a/assets/icons/captain/business_shark.png.import b/assets/icons/captain/business_shark.png.import new file mode 100644 index 0000000..d461228 --- /dev/null +++ b/assets/icons/captain/business_shark.png.import @@ -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 diff --git a/assets/icons/captain/business_shark.svg b/assets/icons/captain/business_shark.svg new file mode 100644 index 0000000..21073d1 --- /dev/null +++ b/assets/icons/captain/business_shark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/captain/business_shark.svg.import b/assets/icons/captain/business_shark.svg.import new file mode 100644 index 0000000..7ce1861 --- /dev/null +++ b/assets/icons/captain/business_shark.svg.import @@ -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 diff --git a/assets/icons/captain/efficient_magazine.png b/assets/icons/captain/efficient_magazine.png new file mode 100644 index 0000000..f2d458e Binary files /dev/null and b/assets/icons/captain/efficient_magazine.png differ diff --git a/assets/icons/captain/efficient_magazine.png.import b/assets/icons/captain/efficient_magazine.png.import new file mode 100644 index 0000000..7881bfd --- /dev/null +++ b/assets/icons/captain/efficient_magazine.png.import @@ -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 diff --git a/assets/icons/captain/efficient_magazine.svg b/assets/icons/captain/efficient_magazine.svg new file mode 100644 index 0000000..d07327e --- /dev/null +++ b/assets/icons/captain/efficient_magazine.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/captain/efficient_magazine.svg.import b/assets/icons/captain/efficient_magazine.svg.import new file mode 100644 index 0000000..152878f --- /dev/null +++ b/assets/icons/captain/efficient_magazine.svg.import @@ -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 diff --git a/assets/icons/captain/miner.png b/assets/icons/captain/miner.png new file mode 100644 index 0000000..46c2732 Binary files /dev/null and b/assets/icons/captain/miner.png differ diff --git a/assets/icons/captain/miner.png.import b/assets/icons/captain/miner.png.import new file mode 100644 index 0000000..5d0b56d --- /dev/null +++ b/assets/icons/captain/miner.png.import @@ -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 diff --git a/assets/icons/captain/miner.svg b/assets/icons/captain/miner.svg new file mode 100644 index 0000000..5ec5576 --- /dev/null +++ b/assets/icons/captain/miner.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/captain/miner.svg.import b/assets/icons/captain/miner.svg.import new file mode 100644 index 0000000..a603a0d --- /dev/null +++ b/assets/icons/captain/miner.svg.import @@ -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 diff --git a/assets/icons/captain/power_redirect.png b/assets/icons/captain/power_redirect.png new file mode 100644 index 0000000..a6092b3 Binary files /dev/null and b/assets/icons/captain/power_redirect.png differ diff --git a/assets/icons/captain/power_redirect.png.import b/assets/icons/captain/power_redirect.png.import new file mode 100644 index 0000000..5399f12 --- /dev/null +++ b/assets/icons/captain/power_redirect.png.import @@ -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 diff --git a/assets/icons/captain/power_redirect.svg b/assets/icons/captain/power_redirect.svg new file mode 100644 index 0000000..8d1fcb8 --- /dev/null +++ b/assets/icons/captain/power_redirect.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/captain/power_redirect.svg.import b/assets/icons/captain/power_redirect.svg.import new file mode 100644 index 0000000..3e72178 --- /dev/null +++ b/assets/icons/captain/power_redirect.svg.import @@ -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 diff --git a/assets/icons/captain/reactor_overload.png b/assets/icons/captain/reactor_overload.png new file mode 100644 index 0000000..60dde5c Binary files /dev/null and b/assets/icons/captain/reactor_overload.png differ diff --git a/assets/icons/captain/reactor_overload.png.import b/assets/icons/captain/reactor_overload.png.import new file mode 100644 index 0000000..13e9e72 --- /dev/null +++ b/assets/icons/captain/reactor_overload.png.import @@ -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 diff --git a/assets/icons/captain/reactor_overload.svg b/assets/icons/captain/reactor_overload.svg new file mode 100644 index 0000000..a35c89b --- /dev/null +++ b/assets/icons/captain/reactor_overload.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/captain/reactor_overload.svg.import b/assets/icons/captain/reactor_overload.svg.import new file mode 100644 index 0000000..eb4f901 --- /dev/null +++ b/assets/icons/captain/reactor_overload.svg.import @@ -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 diff --git a/assets/icons/captain/tactical_maneuver.png b/assets/icons/captain/tactical_maneuver.png new file mode 100644 index 0000000..4088758 Binary files /dev/null and b/assets/icons/captain/tactical_maneuver.png differ diff --git a/assets/icons/captain/tactical_maneuver.png.import b/assets/icons/captain/tactical_maneuver.png.import new file mode 100644 index 0000000..2640f22 --- /dev/null +++ b/assets/icons/captain/tactical_maneuver.png.import @@ -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 diff --git a/assets/icons/captain/tactical_maneuver.svg b/assets/icons/captain/tactical_maneuver.svg new file mode 100644 index 0000000..2bd2eb3 --- /dev/null +++ b/assets/icons/captain/tactical_maneuver.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/captain/tactical_maneuver.svg.import b/assets/icons/captain/tactical_maneuver.svg.import new file mode 100644 index 0000000..365a009 --- /dev/null +++ b/assets/icons/captain/tactical_maneuver.svg.import @@ -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 diff --git a/resources/ui/skill_icon.gdshader b/resources/ui/skill_icon.gdshader new file mode 100644 index 0000000..16ca4d2 --- /dev/null +++ b/resources/ui/skill_icon.gdshader @@ -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); +} diff --git a/resources/ui/skill_icon.gdshader.uid b/resources/ui/skill_icon.gdshader.uid new file mode 100644 index 0000000..ec10adc --- /dev/null +++ b/resources/ui/skill_icon.gdshader.uid @@ -0,0 +1 @@ +uid://dprgc6tmi7yoh diff --git a/scripts/abilities/shield_charge_ability.gd b/scripts/abilities/shield_charge_ability.gd index 59d596e..9876bb7 100644 --- a/scripts/abilities/shield_charge_ability.gd +++ b/scripts/abilities/shield_charge_ability.gd @@ -3,11 +3,15 @@ extends ActiveAbility @export var shield_amount: float = 100.0 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"): return "Tarcze są pełne" return "Wymagana pełna energia" if ship.get("energy") < ship.get("max_energy") else "" func activate(ship: CharacterBody2D) -> void: + if GameState.captain.is_skill_active("power_redirect"): + return ship.set("energy", 0.0) ship.set("shields", minf(ship.get("max_shields"), ship.get("shields") + shield_amount)) ship.call("_update_shield_visual") diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd index 1b0f0ed..e878f4b 100644 --- a/scripts/captain/captain_progress.gd +++ b/scripts/captain/captain_progress.gd @@ -2,6 +2,8 @@ extends Resource class_name CaptainProgress const BALANCE := preload("res://scripts/captain/captain_balance.gd") +const SKILLS := preload("res://scripts/captain/captain_skills.gd") +const ACTIVE_SKILL_SLOTS := 5 @export var level: int = 1 @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 learning_points: int = 0 @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: return BALANCE.xp_required(level) 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: - return 0.0 # The old captain skill system has been retired. +func get_skill_bonus(skill: String) -> float: + return SKILLS.value(skill, get_skill_rank(skill)) func normalize_skill_ranks() -> void: - # Refund legacy ranks for the future captain skill system. - var refunded := get_spent_learning_points() - if refunded > 0: - learning_points += refunded - skills.clear() - emit_changed() - return + for skill in skills.keys(): + if not SKILLS.DATA.has(skill): + skills.erase(skill) + else: + 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() + return true func get_spent_learning_points() -> int: var spent := 0 - for skill in BALANCE.SKILL_NAMES: - spent += maxi(0, int(skills.get(skill, 0))) + for skill in SKILLS.IDS: + spent += get_skill_rank(skill) return spent func get_skill_reset_cost() -> int: return BALANCE.SKILL_RESET_COST_PER_LEVEL * maxi(1, level) 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: - if not can_reset_skills(): - 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: return false +func can_train(skill: String) -> bool: + return learning_points > 0 and skill in skill_offers and get_skill_rank(skill) < SKILLS.MAX_RANK + func train_skill(skill: String) -> bool: if not can_train(skill): return false 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 + 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() return true @@ -69,6 +168,7 @@ func add_rewards(xp_amount: int, credit_amount: int) -> void: experience -= get_xp_required() level += 1 learning_points += BALANCE.LEARNING_POINTS_PER_LEVEL + prepare_skill_offers() emit_changed() func reward_enemy(system_level: int, tier: int, xp_multiplier: float, credit_multiplier: float, grant_credits: bool = true) -> Vector2i: diff --git a/scripts/captain/captain_skills.gd b/scripts/captain/captain_skills.gd new file mode 100644 index 0000000..2f8a45b --- /dev/null +++ b/scripts/captain/captain_skills.gd @@ -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 "" diff --git a/scripts/captain/captain_skills.gd.uid b/scripts/captain/captain_skills.gd.uid new file mode 100644 index 0000000..2ec613c --- /dev/null +++ b/scripts/captain/captain_skills.gd.uid @@ -0,0 +1 @@ +uid://cl3wkhfx6hl6r diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd index 886e20e..0485af1 100644 --- a/scripts/game/flight_hud.gd +++ b/scripts/game/flight_hud.gd @@ -9,8 +9,7 @@ extends CanvasLayer var _debug_expanded := false var _ability_buttons: Dictionary = {} var _ability_bar: HBoxContainer -var _ability_ready_style: StyleBoxFlat -var _ability_blocked_style: StyleBoxFlat +var _passive_skills: VBoxContainer var _inventory_clip: Control var _sidebar: PanelContainer var _sidebar_toggle: Button @@ -25,7 +24,7 @@ var _debug_item_status: Label var _captain_panel: PanelContainer var _cockpit: Control var _crafting_window: PanelContainer -var _skills_window: PanelContainer +var _captain_skills_ui: CanvasLayer func _create_navigation() -> void: 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("standalone", true) _crafting_window.get_node("Contents").add_child(crafting) - _skills_window = _create_window("CaptainSkillsWindow", "UMIEJĘTNOŚCI KAPITANA", Vector2(480, 300)) - var empty := Control.new() - empty.name = "SkillsContent" - empty.custom_minimum_size = Vector2(440, 230) - _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", _skills_window, "res://assets/icons/ui/captain_skills.svg", Color("b9a0ef")]]: + _captain_skills_ui = CanvasLayer.new() + _captain_skills_ui.set_script(preload("res://scripts/ui/captain_skills_ui.gd")) + _captain_skills_ui.set("player", player) + add_child(_captain_skills_ui) + for entry in [["Twórz", _crafting_window, "res://assets/icons/ui/crafting.svg", Color("efb568")], ["Umiejętności kapitana", null, "res://assets/icons/ui/captain_skills.svg", Color("b9a0ef")]]: var button := Button.new() button.tooltip_text = entry[0] button.icon = load(entry[2]) as Texture2D @@ -63,7 +61,14 @@ func _create_navigation() -> void: style.set_content_margin_all(8) button.add_theme_stylebox_override(state, style) navigation.add_child(button) - button.pressed.connect(_toggle_window.bind(entry[1])) + 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])) func _create_window(node_name: String, title: String, dimensions: Vector2) -> PanelContainer: var window := PanelContainer.new() @@ -105,7 +110,6 @@ func _create_window(node_name: String, title: String, dimensions: Vector2) -> Pa func _toggle_window(window: PanelContainer) -> void: var opening := not window.visible _crafting_window.hide() - _skills_window.hide() window.visible = opening func _ready() -> void: @@ -152,6 +156,13 @@ func _create_sidebar() -> void: _captain_panel.position = Vector2(12, 12) _captain_panel.size = Vector2(320, _captain_panel.get_combined_minimum_size().y) _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.name = "DebugActions" _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): return 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_actions.position = Vector2(18, top + 30) _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) if _ability_bar != null: var abilities := _ability_bar.get_parent() as Control - abilities.offset_bottom = _cockpit.position.y - $HUDRoot.size.y - 14.0 - abilities.offset_top = abilities.offset_bottom - 44.0 + abilities.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT) + 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() _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 @@ -301,7 +320,7 @@ Tarcze: %.1f pkt/s GameState.system_level, int(game.call("get_remaining_enemies")), 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")), _get_bonus_summary(), ] @@ -382,8 +401,6 @@ Kredyty: %+.1f%%""".replace("\r", "") % [ ] func _create_ability_bar() -> void: - _ability_ready_style = _make_ability_style(true) - _ability_blocked_style = _make_ability_style(false) var panel := PanelContainer.new() panel.name = "ActiveAbilities" $HUDRoot.add_child(panel) @@ -395,22 +412,22 @@ func _create_ability_bar() -> void: panel.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) _ability_bar = HBoxContainer.new() _ability_bar.name = "AbilityButtons" - _ability_bar.alignment = BoxContainer.ALIGNMENT_CENTER - _ability_bar.add_theme_constant_override("separation", 4) + _ability_bar.alignment = BoxContainer.ALIGNMENT_END + _ability_bar.add_theme_constant_override("separation", 6) panel.add_child(_ability_bar) for index in ShipInventory.EXTENSION_SLOT_COUNT: var button := Button.new() + button.set_script(preload("res://scripts/ui/captain_skill_button.gd")) button.name = "Ability%d" % (index + 1) - button.custom_minimum_size = Vector2(44, 44) - 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 + button.call("configure", null, Color("91dbea")) var shortcut := Label.new() shortcut.text = str(index + 1) - shortcut.position = Vector2(3, 1) + shortcut.position = Vector2(4, 2) shortcut.mouse_filter = Control.MOUSE_FILTER_IGNORE 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_theme_font_size_override("font_size", 11) 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) button.disabled = not reason.is_empty() var item: ItemInstance = player.get_node("ShipInventory").get_item("extension", index) - button.icon = item.definition.icon - button.text = "" - 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.call("configure", item.definition.icon, Color("91dbea")) + button.call("set_visual_state", 0.0, false, not reason.is_empty()) 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 -func _make_ability_style(ready: bool) -> StyleBoxFlat: - var style := StyleBoxFlat.new() - style.bg_color = Color("174237") if ready else Color("202731") - style.border_color = Color("77e8bc") if ready else Color("526071") - style.set_border_width_all(2 if ready else 1) - style.set_content_margin_all(6) - return style +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() + style.bg_color = Color(0.025, 0.042, 0.064, 0.94) + style.border_color = accent.darkened(0.6) + style.set_border_width_all(1) + style.border_width_left = 2 + 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 diff --git a/scripts/game/game.gd b/scripts/game/game.gd index 50cadb7..a241d87 100644 --- a/scripts/game/game.gd +++ b/scripts/game/game.gd @@ -137,6 +137,12 @@ func get_remaining_enemies() -> int: remaining += 1 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: if _transitioning or bool(player.get("_is_destroyed")): services_menu.call("hide_services") diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 29abb33..5f3f9a3 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -166,13 +166,16 @@ func take_damage(amount: float, _critical: bool = false, damage_type: int = Dama _destroy_ship() 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 shields = minf(max_shields, shields + actual_damage * shield_on_damage_percent / 100.0) _update_shield_visual() 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: return GameState.captain.get_skill_bonus("mechanic") @@ -254,11 +257,15 @@ func _restart_level_after_destruction() -> void: get_tree().reload_current_scene.call_deferred() 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: _mini_boost_active = false return _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 _update_mini_boost(delta) 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) if was_boosting: 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 shields = minf(max_shields, shields + shield_regeneration * delta) if shields != previous_shields: diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index f7f5b40..85b0efd 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -75,7 +75,8 @@ func preserve_for_restart() -> void: func consume_ammunition() -> bool: if ammunition <= 0: return false - ammunition -= 1 + if GameState.captain.should_consume_ammunition("ordinary"): + ammunition -= 1 ammunition_changed.emit() return true @@ -98,7 +99,7 @@ func get_item(slot_kind: String, index: int) -> ItemInstance: return slots[index] 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: 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 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: return GameState.captain.credits >= get_missile_buy_cost() @@ -345,7 +346,8 @@ func _weapon_dps(item: ItemInstance, bonuses: Dictionary) -> float: func consume_irradiated_ammunition() -> bool: if irradiated_ammunition <= 0: return false - irradiated_ammunition -= 1 + if GameState.captain.should_consume_ammunition("irradiated"): + irradiated_ammunition -= 1 ammunition_changed.emit() return true @@ -363,12 +365,15 @@ func craft_irradiated_ammunition() -> bool: return true 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: if not can_buy_irradiated_ammunition(): return false - GameState.captain.credits -= 15 * maxi(1, GameState.system_level) + GameState.captain.credits -= get_irradiated_ammunition_buy_cost() irradiated_ammunition += 10 GameState.captain.emit_changed() ammunition_changed.emit() diff --git a/scripts/levels/exploration_poi.gd b/scripts/levels/exploration_poi.gd index bd9acd4..5999d33 100644 --- a/scripts/levels/exploration_poi.gd +++ b/scripts/levels/exploration_poi.gd @@ -113,7 +113,7 @@ func claim() -> bool: var summary := "" var scale := reward_scale() 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 summary = "+%d %s" % [amount, {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu"}[_mineral]] elif kind == "ruin": diff --git a/scripts/ui/captain_skill_button.gd b/scripts/ui/captain_skill_button.gd new file mode 100644 index 0000000..2909ace --- /dev/null +++ b/scripts/ui/captain_skill_button.gd @@ -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 diff --git a/scripts/ui/captain_skill_button.gd.uid b/scripts/ui/captain_skill_button.gd.uid new file mode 100644 index 0000000..cfecf37 --- /dev/null +++ b/scripts/ui/captain_skill_button.gd.uid @@ -0,0 +1 @@ +uid://b6cgtrcb6cg2m diff --git a/scripts/ui/captain_skills_ui.gd b/scripts/ui/captain_skills_ui.gd new file mode 100644 index 0000000..10180e2 --- /dev/null +++ b/scripts/ui/captain_skills_ui.gd @@ -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 diff --git a/scripts/ui/captain_skills_ui.gd.uid b/scripts/ui/captain_skills_ui.gd.uid new file mode 100644 index 0000000..d917099 --- /dev/null +++ b/scripts/ui/captain_skills_ui.gd.uid @@ -0,0 +1 @@ +uid://cs7ixgwu5l0qw diff --git a/scripts/ui/flight_cockpit.gd b/scripts/ui/flight_cockpit.gd index 836eebc..458568a 100644 --- a/scripts/ui/flight_cockpit.gd +++ b/scripts/ui/flight_cockpit.gd @@ -48,6 +48,7 @@ func _draw() -> void: var cell := (size.x - 238) / 3.0 var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))] var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"] + var boss_alive := bool(game.call("is_boss_alive")) for index in 3: var x := 218.0 + index * cell var color := AMBER if index == 0 else (CYAN if index == 1 else Color("ff8989")) @@ -55,6 +56,8 @@ func _draw() -> void: if index == 1: _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")) + elif index == 2 and boss_alive: + _text("BOSS", Vector2(x, 45), cell, 21, Color("ff8989")) else: _text(str(values[index]).pad_zeros(2), Vector2(x, 45), cell, 21, color) if index < 2: diff --git a/scripts/ui/planet_services_menu.gd b/scripts/ui/planet_services_menu.gd index 0a0f16c..3e68630 100644 --- a/scripts/ui/planet_services_menu.gd +++ b/scripts/ui/planet_services_menu.gd @@ -85,7 +85,7 @@ func _position_at_planet() -> void: func _refresh_ammunition_service() -> void: 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() _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) @@ -95,7 +95,7 @@ func _refresh_ammunition_service() -> void: buy_ammunition_button.disabled = _inventory == null or not _inventory.can_buy_ammunition() if _inventory: 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: if visible and _inventory: diff --git a/tests/captain/captain_skills_test.gd b/tests/captain/captain_skills_test.gd new file mode 100644 index 0000000..aa73ead --- /dev/null +++ b/tests/captain/captain_skills_test.gd @@ -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) diff --git a/tests/captain/captain_skills_test.gd.uid b/tests/captain/captain_skills_test.gd.uid new file mode 100644 index 0000000..e31464e --- /dev/null +++ b/tests/captain/captain_skills_test.gd.uid @@ -0,0 +1 @@ +uid://bnasxkcqdek5p diff --git a/tests/captain/captain_skills_test.tscn b/tests/captain/captain_skills_test.tscn new file mode 100644 index 0000000..5e8fb9f --- /dev/null +++ b/tests/captain/captain_skills_test.tscn @@ -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") diff --git a/tests/levels/boss_spawn_test.gd b/tests/levels/boss_spawn_test.gd index ac78740..75ebb3d 100644 --- a/tests/levels/boss_spawn_test.gd +++ b/tests/levels/boss_spawn_test.gd @@ -18,6 +18,7 @@ func _ready() -> void: enemy.set_physics_process(false) enemies.append(enemy) assert(enemies.size() == 4) + assert(not game.is_boss_alive()) for index in enemies.size() - 1: enemies[index].take_damage(100000.0) game._physics_process(0.0) @@ -40,6 +41,7 @@ func _ready() -> void: player.set("_is_destroyed", false) game._physics_process(0.0) 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) var boss: CharacterBody2D for enemy in get_tree().get_nodes_in_group("enemies"): @@ -55,6 +57,7 @@ func _ready() -> void: boss.take_damage(100000.0) game._physics_process(0.0) assert(game.system_cleared and game.get_remaining_enemies() == 0) + assert(not game.is_boss_alive()) game._physics_process(0.0) assert(game.get_remaining_enemies() == 0) for definition in preload("res://scripts/items/item_generator.gd").DEFINITIONS: