diff --git a/artifacts/alien_shield_icons_preview.png.import b/artifacts/alien_shield_icons_preview.png.import new file mode 100644 index 0000000..1afff79 --- /dev/null +++ b/artifacts/alien_shield_icons_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d0hqh3305qe0h" +path="res://.godot/imported/alien_shield_icons_preview.png-8446b70ba9bc919da2cbe22889246b66.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/alien_shield_icons_preview.png" +dest_files=["res://.godot/imported/alien_shield_icons_preview.png-8446b70ba9bc919da2cbe22889246b66.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 diff --git a/artifacts/artillery_visual_preview.png b/artifacts/artillery_visual_preview.png new file mode 100644 index 0000000..c8c96b7 Binary files /dev/null and b/artifacts/artillery_visual_preview.png differ diff --git a/artifacts/artillery_visual_preview.png.import b/artifacts/artillery_visual_preview.png.import new file mode 100644 index 0000000..1b322d5 --- /dev/null +++ b/artifacts/artillery_visual_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvrae1hhndu76" +path="res://.godot/imported/artillery_visual_preview.png-5770f12a32517cc91b15b62beecb8f8f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/artillery_visual_preview.png" +dest_files=["res://.godot/imported/artillery_visual_preview.png-5770f12a32517cc91b15b62beecb8f8f.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 diff --git a/artifacts/burning_cloud_preview.png b/artifacts/burning_cloud_preview.png new file mode 100644 index 0000000..50b2a1f Binary files /dev/null and b/artifacts/burning_cloud_preview.png differ diff --git a/artifacts/burning_cloud_preview.png.import b/artifacts/burning_cloud_preview.png.import new file mode 100644 index 0000000..b5312fe --- /dev/null +++ b/artifacts/burning_cloud_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dbdijy04ny1g6" +path="res://.godot/imported/burning_cloud_preview.png-3dbc2a21f0fb301884c13cfab09b7cc7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/burning_cloud_preview.png" +dest_files=["res://.godot/imported/burning_cloud_preview.png-3dbc2a21f0fb301884c13cfab09b7cc7.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 diff --git a/artifacts/combat_visuals_preview.png b/artifacts/combat_visuals_preview.png new file mode 100644 index 0000000..bba64ac Binary files /dev/null and b/artifacts/combat_visuals_preview.png differ diff --git a/artifacts/combat_visuals_preview.png.import b/artifacts/combat_visuals_preview.png.import new file mode 100644 index 0000000..17874a7 --- /dev/null +++ b/artifacts/combat_visuals_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://blbf4a22bp5qv" +path="res://.godot/imported/combat_visuals_preview.png-aef70c70ce1b6bd37735a7b5f454f1ba.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/combat_visuals_preview.png" +dest_files=["res://.godot/imported/combat_visuals_preview.png-aef70c70ce1b6bd37735a7b5f454f1ba.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 diff --git a/artifacts/laboratory_icons_preview.png b/artifacts/laboratory_icons_preview.png new file mode 100644 index 0000000..f6d4763 Binary files /dev/null and b/artifacts/laboratory_icons_preview.png differ diff --git a/artifacts/laboratory_icons_preview.png.import b/artifacts/laboratory_icons_preview.png.import new file mode 100644 index 0000000..e24711a --- /dev/null +++ b/artifacts/laboratory_icons_preview.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cae7f5er0cnhk" +path="res://.godot/imported/laboratory_icons_preview.png-0061736563810819ab3ea3920a0c85de.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/laboratory_icons_preview.png" +dest_files=["res://.godot/imported/laboratory_icons_preview.png-0061736563810819ab3ea3920a0c85de.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 diff --git a/artifacts/laboratory_progress.png b/artifacts/laboratory_progress.png new file mode 100644 index 0000000..6fe00fa Binary files /dev/null and b/artifacts/laboratory_progress.png differ diff --git a/artifacts/laboratory_progress.png.import b/artifacts/laboratory_progress.png.import new file mode 100644 index 0000000..7e4f98c --- /dev/null +++ b/artifacts/laboratory_progress.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://driwfd2ewvhnt" +path="res://.godot/imported/laboratory_progress.png-a0ec8f6c017c23f3baaa67fc948c53e2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/laboratory_progress.png" +dest_files=["res://.godot/imported/laboratory_progress.png-a0ec8f6c017c23f3baaa67fc948c53e2.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 diff --git a/artifacts/laboratory_projects.png b/artifacts/laboratory_projects.png new file mode 100644 index 0000000..3985cc1 Binary files /dev/null and b/artifacts/laboratory_projects.png differ diff --git a/artifacts/laboratory_projects.png.import b/artifacts/laboratory_projects.png.import new file mode 100644 index 0000000..2b20fab --- /dev/null +++ b/artifacts/laboratory_projects.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dpvckkd1oc052" +path="res://.godot/imported/laboratory_projects.png-61c88f4e4c24b4b7ef45dab06ea98448.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/laboratory_projects.png" +dest_files=["res://.godot/imported/laboratory_projects.png-61c88f4e4c24b4b7ef45dab06ea98448.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 diff --git a/artifacts/orb_cargo_stacks.png b/artifacts/orb_cargo_stacks.png new file mode 100644 index 0000000..100d0dc Binary files /dev/null and b/artifacts/orb_cargo_stacks.png differ diff --git a/artifacts/orb_cargo_stacks.png.import b/artifacts/orb_cargo_stacks.png.import new file mode 100644 index 0000000..c8ac341 --- /dev/null +++ b/artifacts/orb_cargo_stacks.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bofs5beqwrrn6" +path="res://.godot/imported/orb_cargo_stacks.png-a0840626f60312aabcfb60a252e57756.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/orb_cargo_stacks.png" +dest_files=["res://.godot/imported/orb_cargo_stacks.png-a0840626f60312aabcfb60a252e57756.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 diff --git a/artifacts/ship_shapes_review.png b/artifacts/ship_shapes_review.png new file mode 100644 index 0000000..e089155 Binary files /dev/null and b/artifacts/ship_shapes_review.png differ diff --git a/artifacts/ship_shapes_review.png.import b/artifacts/ship_shapes_review.png.import new file mode 100644 index 0000000..1cce77a --- /dev/null +++ b/artifacts/ship_shapes_review.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbcfmnuaaa3j7" +path="res://.godot/imported/ship_shapes_review.png-1e1ea44a0338e74b069fb7d1b10cc50f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://artifacts/ship_shapes_review.png" +dest_files=["res://.godot/imported/ship_shapes_review.png-1e1ea44a0338e74b069fb7d1b10cc50f.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 diff --git a/assets/icons/artillery_cannon.png b/assets/icons/artillery_cannon.png new file mode 100644 index 0000000..be2d68b Binary files /dev/null and b/assets/icons/artillery_cannon.png differ diff --git a/assets/icons/artillery_cannon.png.import b/assets/icons/artillery_cannon.png.import new file mode 100644 index 0000000..3d0e42f --- /dev/null +++ b/assets/icons/artillery_cannon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://j7nsii3e0emk" +path="res://.godot/imported/artillery_cannon.png-1872c95a0636c00288e7bc2dec35368f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/artillery_cannon.png" +dest_files=["res://.godot/imported/artillery_cannon.png-1872c95a0636c00288e7bc2dec35368f.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 diff --git a/assets/icons/artillery_cannon_v2.png b/assets/icons/artillery_cannon_v2.png new file mode 100644 index 0000000..5ef7a9b Binary files /dev/null and b/assets/icons/artillery_cannon_v2.png differ diff --git a/assets/icons/artillery_cannon_v2.png.import b/assets/icons/artillery_cannon_v2.png.import new file mode 100644 index 0000000..812ce5c --- /dev/null +++ b/assets/icons/artillery_cannon_v2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dm67sb4g54omb" +path="res://.godot/imported/artillery_cannon_v2.png-c466f12d8d70fa0cc2e0853d053dae80.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/artillery_cannon_v2.png" +dest_files=["res://.godot/imported/artillery_cannon_v2.png-c466f12d8d70fa0cc2e0853d053dae80.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 diff --git a/assets/icons/artillery_cannon_v2_prompt.md b/assets/icons/artillery_cannon_v2_prompt.md new file mode 100644 index 0000000..fb8306c --- /dev/null +++ b/assets/icons/artillery_cannon_v2_prompt.md @@ -0,0 +1,3 @@ +# Artillery icon v2 — built-in ImageGen + +Use case: stylized-concept. Asset type: square inventory icon for spaceship ARTILLERY CANNON. One compact massive sci-fi ship-mounted cannon, bold horizontal barrel pointing upper-right, thick angular armored breech at lower left, prominent muzzle brake, metallic graphite steel with subtle amber accents. CLOSE UP, weapon occupies 90% of square, clean instantly readable silhouette at 40 pixels. No tripod, wheels, ground, handheld grip, or tall pedestal. NO muzzle flash, explosion, flames, smoke or sparks hiding the gun. Dark navy near-black background. Premium detailed painted 3D game item icon with crisp silver edge lighting, controlled high contrast. No text, labels, numbers, border or watermark. diff --git a/assets/icons/incendiary_missile.png b/assets/icons/incendiary_missile.png new file mode 100644 index 0000000..9cd1296 Binary files /dev/null and b/assets/icons/incendiary_missile.png differ diff --git a/assets/icons/incendiary_missile.png.import b/assets/icons/incendiary_missile.png.import new file mode 100644 index 0000000..ea0df2b --- /dev/null +++ b/assets/icons/incendiary_missile.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bxfwdui8jd2pc" +path="res://.godot/imported/incendiary_missile.png-85a9b7ff7dc833510d13ce2c15069bd5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/incendiary_missile.png" +dest_files=["res://.godot/imported/incendiary_missile.png-85a9b7ff7dc833510d13ce2c15069bd5.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 diff --git a/assets/icons/laboratory_weapon_prompts.md b/assets/icons/laboratory_weapon_prompts.md new file mode 100644 index 0000000..1718d12 --- /dev/null +++ b/assets/icons/laboratory_weapon_prompts.md @@ -0,0 +1,10 @@ +# Laboratory weapon icons — built-in ImageGen + +## incendiary_missile +Use case: stylized-concept. Asset type: square inventory weapon icon for a science fiction space game. Subject: a human industrial homing incendiary missile launcher with one red-tipped missile, orange burning gas venting around it. Single bold centered weapon filling the square with small margins, diagonal three-quarter view, dark navy near-black background. Premium painted 3D art, detailed graphite steel, polished metallic edges, strong readable silhouette even at 40 pixels, matching existing sci-fi equipment icons. No text, numbers, labels, frame, watermark or collage. + +## radiation_laser +Use case: stylized-concept. Asset type: square inventory weapon icon for a science fiction space game. Subject: a human industrial radiation beam cannon, uranium green crystal chamber and a vivid green laser beam. Single bold centered weapon filling the square with small margins, diagonal three-quarter view, dark navy near-black background. Premium painted 3D art, detailed graphite steel, polished metallic edges, strong readable silhouette even at 40 pixels, matching existing sci-fi equipment icons. No text, numbers, labels, frame, watermark or collage. + +## artillery_cannon +Use case: stylized-concept. Asset type: square inventory weapon icon for a science fiction space game. Subject: a massive human industrial spaceship artillery cannon, long thick steel barrel, reinforced recoil pistons and brilliant amber muzzle blast. Single bold centered weapon filling the square with small margins, diagonal three-quarter view, dark navy near-black background. Premium painted 3D art, detailed graphite steel, polished metallic edges, strong readable silhouette even at 40 pixels, matching existing sci-fi equipment icons. No text, numbers, labels, frame, watermark or collage. diff --git a/assets/icons/radiation_laser.png b/assets/icons/radiation_laser.png new file mode 100644 index 0000000..b91433b Binary files /dev/null and b/assets/icons/radiation_laser.png differ diff --git a/assets/icons/radiation_laser.png.import b/assets/icons/radiation_laser.png.import new file mode 100644 index 0000000..9981860 --- /dev/null +++ b/assets/icons/radiation_laser.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c8ldbvblujbrc" +path="res://.godot/imported/radiation_laser.png-5dc2fc33815c9c1fec357878a602175b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icons/radiation_laser.png" +dest_files=["res://.godot/imported/radiation_laser.png-5dc2fc33815c9c1fec357878a602175b.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 diff --git a/assets/sounds/effects/ARTILLERY_CREDITS.md b/assets/sounds/effects/ARTILLERY_CREDITS.md new file mode 100644 index 0000000..56bd6d1 --- /dev/null +++ b/assets/sounds/effects/ARTILLERY_CREDITS.md @@ -0,0 +1,9 @@ +# Artillery firing recordings + +- Author: Iwan Gabovitch (qubodup). +- Source: https://opengameart.org/content/tiny-naval-battle-sounds-set +- Archive: qubodup-NavalBattleSoundSet-cc0_0.7z (version 2). +- License: CC0 — https://creativecommons.org/publicdomain/zero/1.0/ +- `artillery_shot.ogg`: original `ogg/GunShot.ogg`. +- `artillery_shot_reverb.ogg`: original `ogg/GunShotGverb.ogg`. +- Files copied without alteration; playback volume and pitch are controlled in game. diff --git a/assets/sounds/effects/artillery_shot.ogg b/assets/sounds/effects/artillery_shot.ogg new file mode 100644 index 0000000..ce90f6a Binary files /dev/null and b/assets/sounds/effects/artillery_shot.ogg differ diff --git a/assets/sounds/effects/artillery_shot.ogg.import b/assets/sounds/effects/artillery_shot.ogg.import new file mode 100644 index 0000000..a2168af --- /dev/null +++ b/assets/sounds/effects/artillery_shot.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://b5nf2prtt0sy6" +path="res://.godot/imported/artillery_shot.ogg-27bec10b84d845c13ff2e9dfb3b14a2a.oggvorbisstr" + +[deps] + +source_file="res://assets/sounds/effects/artillery_shot.ogg" +dest_files=["res://.godot/imported/artillery_shot.ogg-27bec10b84d845c13ff2e9dfb3b14a2a.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/assets/sounds/effects/artillery_shot_reverb.ogg b/assets/sounds/effects/artillery_shot_reverb.ogg new file mode 100644 index 0000000..70fc07f Binary files /dev/null and b/assets/sounds/effects/artillery_shot_reverb.ogg differ diff --git a/assets/sounds/effects/artillery_shot_reverb.ogg.import b/assets/sounds/effects/artillery_shot_reverb.ogg.import new file mode 100644 index 0000000..16553a8 --- /dev/null +++ b/assets/sounds/effects/artillery_shot_reverb.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://01gm27frm67s" +path="res://.godot/imported/artillery_shot_reverb.ogg-935682c3d6acccbfe1295a42b597c12c.oggvorbisstr" + +[deps] + +source_file="res://assets/sounds/effects/artillery_shot_reverb.ogg" +dest_files=["res://.godot/imported/artillery_shot_reverb.ogg-935682c3d6acccbfe1295a42b597c12c.oggvorbisstr"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/resources/items/artillery_cannon.tres b/resources/items/artillery_cannon.tres new file mode 100644 index 0000000..7178ba5 --- /dev/null +++ b/resources/items/artillery_cannon.tres @@ -0,0 +1,28 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"] +[ext_resource type="Texture2D" path="res://assets/icons/artillery_cannon_v2.png" id="2"] + +[resource] +script = ExtResource("1") +icon = ExtResource("2") +item_id = "artillery_cannon" +display_name = "Działo artyleryjskie" +category = 0 +description = "Ciężki, szybki pocisk kinetyczny. Silne pojedyncze uderzenie co 2,2 s. Zużywa 10 zwykłej amunicji, bez energii." +primary_stat = "damage" +primary_value = 50 +stat_suffix = " DMG" +weapon_kind = 1 +weapon_effect = "artillery" +damage_type_override = 0 +fire_interval = 2.2 +projectile_speed = 1450 +weapon_range = 950 +energy_cost = 0 +ammunition_cost = 10 +damage_lower_roll_min = 40 +damage_lower_roll_max = 40 +damage_spread_roll_min = 20 +damage_spread_roll_max = 20 + diff --git a/resources/items/incendiary_missile.tres b/resources/items/incendiary_missile.tres new file mode 100644 index 0000000..3d505f4 --- /dev/null +++ b/resources/items/incendiary_missile.tres @@ -0,0 +1,29 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"] +[ext_resource type="Texture2D" path="res://assets/icons/incendiary_missile.png" id="2"] + +[resource] +script = ExtResource("1") +icon = ExtResource("2") +item_id = "incendiary_missile" +display_name = "Wyrzutnia rakiet zapalających" +category = 0 +description = "Naprowadzana rakieta z małym uderzeniem. Zostawia płonący gaz na 5 s w promieniu 95 px: 110% obrażeń uderzenia na sekundę, kinetyczne. Nakładające się chmury nie sumują obrażeń." +primary_stat = "damage" +primary_value = 10 +stat_suffix = " DMG" +weapon_kind = 3 +weapon_effect = "incendiary" +damage_type_override = 0 +fire_interval = 5 +weapon_cooldown = 5.0 +projectile_speed = 400 +weapon_range = 1000 +energy_cost = 0 +ammunition_cost = 1 +damage_lower_roll_min = 8 +damage_lower_roll_max = 12 +damage_spread_roll_min = 2 +damage_spread_roll_max = 4 + diff --git a/resources/items/radiation_laser.tres b/resources/items/radiation_laser.tres new file mode 100644 index 0000000..08171f5 --- /dev/null +++ b/resources/items/radiation_laser.tres @@ -0,0 +1,28 @@ +[gd_resource type="Resource" script_class="ItemDefinition" load_steps=3 format=3] + +[ext_resource type="Script" path="res://scripts/items/item_definition.gd" id="1"] +[ext_resource type="Texture2D" path="res://assets/icons/radiation_laser.png" id="2"] + +[resource] +script = ExtResource("1") +icon = ExtResource("2") +item_id = "radiation_laser" +display_name = "Laser radiacyjny" +category = 0 +description = "Zielony laser pulsacyjny. Trafienie napromieniowuje: 100% obrażeń impulsu na sekundę przez 10 s. Kolejne impulsy odświeżają czas, działa najmocniejsze napromieniowanie." +primary_stat = "damage" +primary_value = 3.5 +stat_suffix = " DMG" +weapon_kind = 2 +weapon_effect = "radiation_laser" +damage_type_override = 2 +fire_interval = 0.4 +projectile_speed = 0 +weapon_range = 600 +energy_cost = 3 +ammunition_cost = 1 +damage_lower_roll_min = 2 +damage_lower_roll_max = 3 +damage_spread_roll_min = 1 +damage_spread_roll_max = 2 + diff --git a/scripts/game/artillery_effect.gd b/scripts/game/artillery_effect.gd new file mode 100644 index 0000000..2b84a4b --- /dev/null +++ b/scripts/game/artillery_effect.gd @@ -0,0 +1,67 @@ +extends Node2D +static var _sound: AudioStream +static var _shot_index := 0 +var impact := false +var _age := 0.0 +var _material: ShaderMaterial + +static func launch(ship: Node2D, at: Vector2, direction: Vector2) -> void: + preload("res://scripts/game/weapon_camera_impulse.gd").kick(ship) + var flash := Node2D.new() + flash.set_script(load("res://scripts/game/artillery_effect.gd")) + flash.position = at + flash.rotation = direction.angle() + ship.get_parent().add_child(flash) + _shot_index += 1 + _sound = load("res://assets/sounds/effects/artillery_shot%s.ogg" % ("_reverb" if _shot_index%2 else "")) as AudioStream + var audio := AudioStreamPlayer2D.new() + audio.stream = _sound + audio.bus = &"MachineGun" + audio.volume_db = -7.0 + audio.pitch_scale = randf_range(0.92,1.02) + audio.max_distance = 1800 + ship.get_parent().add_child(audio) + audio.global_position = at + audio.finished.connect(audio.queue_free) + audio.play() + +static func hit(parent: Node, at: Vector2, direction: Vector2) -> void: + var flash := Node2D.new() + flash.set_script(load("res://scripts/game/artillery_effect.gd")) + flash.impact = true + flash.position = at + flash.rotation = direction.angle() + parent.add_child(flash) + +func _ready() -> void: + z_index = 25 + if not impact: + var plume := ColorRect.new() + plume.mouse_filter = Control.MOUSE_FILTER_IGNORE + plume.show_behind_parent = true + plume.position = Vector2(-25,-100) + plume.size = Vector2(260,200) + _material = ShaderMaterial.new() + _material.shader = preload("res://scripts/game/artillery_muzzle.gdshader") + _material.set_shader_parameter("seed_phase",randf()*100) + plume.material = _material + add_child(plume) + +func _process(delta: float) -> void: + _age += delta + if _age >= (0.5 if impact else 1.2): + queue_free() + else: + if _material != null: _material.set_shader_parameter("age",_age) + queue_redraw() + +func _draw() -> void: + var t := clampf(_age/0.5,0,1) + var reach := 90.0 if impact else 120.0 + if impact: + draw_arc(Vector2.ZERO,reach*t,0,TAU,40,Color(1,0.65,0.25,(1-t)*0.6),2,true) + for i in 28: + var dir := Vector2.RIGHT.rotated(float(i)*2.39996) + if not impact: dir = Vector2.RIGHT.rotated(sin(i*2.4)*0.65) + var distance := reach*t*(0.5+float(i%5)*0.16) + draw_line(dir*distance,dir*(distance+16*(1-t)),Color(1,0.66,0.18,1-t),2*(1-t)+0.4,true) diff --git a/scripts/game/artillery_effect.gd.uid b/scripts/game/artillery_effect.gd.uid new file mode 100644 index 0000000..9d705e9 --- /dev/null +++ b/scripts/game/artillery_effect.gd.uid @@ -0,0 +1 @@ +uid://dfgl3oawwf3ex diff --git a/scripts/game/artillery_muzzle.gdshader b/scripts/game/artillery_muzzle.gdshader new file mode 100644 index 0000000..cd4de83 --- /dev/null +++ b/scripts/game/artillery_muzzle.gdshader @@ -0,0 +1,30 @@ +shader_type canvas_item; +render_mode unshaded; +uniform float age = 0.0; +uniform float seed_phase = 0.0; +float hash(vec2 p) { p=fract(p*vec2(123.34,456.21)); p+=dot(p,p+45.32); return fract(p.x*p.y); } +float noise(vec2 p) { + vec2 i=floor(p), f=fract(p); f=f*f*(3.0-2.0*f); + return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1)),f.x),f.y); +} +float fbm(vec2 p) { return noise(p)*0.55+noise(p*2.1+7.3)*0.28+noise(p*4.3+12.1)*0.14; } +void fragment() { + vec2 p=vec2(UV.x,UV.y-0.5); + float n=fbm(p*vec2(8,10)+vec2(-age*6.0,seed_phase)); + float x=p.x-0.09; + float flare=max(0.0,1.0-age/0.19); + float width=0.025+sin(clamp(x/0.8,0.0,1.0)*3.14159)*0.14; + float jet=(1.0-smoothstep(width*0.4,width,abs(p.y)+(n-0.5)*0.07)); + jet*=smoothstep(-0.02,0.025,x)*(1.0-smoothstep(0.4,0.85,x)); + jet*=flare; + float smoke_radius=0.11+age*0.24; + vec2 center=vec2(0.19+age*0.25,-age*0.025); + float smoke_edge=length((p-center)/vec2(smoke_radius*1.7,smoke_radius))+(n-0.5)*0.55; + float smoke=(1.0-smoothstep(0.55,1.15,smoke_edge))*smoothstep(0.015,0.12,age)*(1.0-smoothstep(0.45,1.2,age)); + smoke*=smoothstep(0.0,0.12,UV.x)*(1.0-smoothstep(0.9,1.0,UV.x)); + vec3 smoke_color=mix(vec3(0.055,0.06,0.065),vec3(0.32,0.29,0.25),n); + float heat=clamp(jet*0.68+n*0.24,0.0,1.0); + vec3 fire=mix(vec3(1.0,0.14,0.005),vec3(1.5,0.85,0.23),heat); + fire=mix(fire,vec3(1.8,1.55,0.95),pow(heat,5.0)); + COLOR=vec4(mix(smoke_color,fire,smoothstep(0.01,0.3,jet)),max(smoke*0.65,jet)); +} diff --git a/scripts/game/artillery_muzzle.gdshader.uid b/scripts/game/artillery_muzzle.gdshader.uid new file mode 100644 index 0000000..f12181d --- /dev/null +++ b/scripts/game/artillery_muzzle.gdshader.uid @@ -0,0 +1 @@ +uid://pglyfxwskx6d diff --git a/scripts/game/burning_gas.gd b/scripts/game/burning_gas.gd new file mode 100644 index 0000000..b178c91 --- /dev/null +++ b/scripts/game/burning_gas.gd @@ -0,0 +1,69 @@ +extends Node2D +const DURATION := 5.0 +const RADIUS := 95.0 +var damage_per_second := 0.0 +var shield_piercing := 0.0 +var remaining := DURATION +var _age := 0.0 +var _tick_elapsed := 0.0 +var _material: ShaderMaterial +var _phase := 0.0 + +func _ready() -> void: + add_to_group("burning_gas") + z_index = 8 + _phase = fmod(float(get_instance_id()),137.0)*0.17 + var cloud := ColorRect.new() + cloud.mouse_filter = Control.MOUSE_FILTER_IGNORE + cloud.show_behind_parent = true + cloud.position = -Vector2.ONE*RADIUS*1.35 + cloud.size = Vector2.ONE*RADIUS*2.7 + _material = ShaderMaterial.new() + _material.shader = preload("res://scripts/game/burning_gas.gdshader") + _material.set_shader_parameter("phase",_phase) + _material.set_shader_parameter("strength",0.0) + cloud.material = _material + add_child(cloud) + +func _physics_process(delta: float) -> void: + var step := minf(delta,remaining) + remaining = maxf(0.0,remaining-delta) + _tick_elapsed += step + if _tick_elapsed >= 0.25 or remaining <= 0.0: + _deal_damage(_tick_elapsed) + _tick_elapsed = 0.0 + if remaining <= 0.0: queue_free() + +func _deal_damage(step: float) -> void: + for target in get_tree().get_nodes_in_group("enemies"): + if target.is_queued_for_deletion() or float(target.get("hull")) <= 0 or bool(target.get("virus_controlled")): continue + if global_position.distance_to(target.global_position) > RADIUS: continue + var stronger := false + for cloud in get_tree().get_nodes_in_group("burning_gas"): + if cloud == self or cloud.is_queued_for_deletion(): continue + if cloud.damage_per_second < damage_per_second: continue + if cloud.damage_per_second == damage_per_second and cloud.get_instance_id() > get_instance_id(): continue + if cloud.global_position.distance_to(target.global_position) <= RADIUS: + stronger = true + break + if not stronger: target.take_research_dot(damage_per_second*step,DamageTypes.Type.KINETIC,shield_piercing) + +func _process(delta: float) -> void: + _age += delta + if _material != null: + _material.set_shader_parameter("age",_age) + _material.set_shader_parameter("strength",minf(1.0,_age*7.0)*smoothstep(0.0,0.9,remaining)) + queue_redraw() + +func _draw() -> void: + var fade := minf(1.0,remaining)*minf(1.0,_age*7.0) + # Short-lived embers move outward through the rolling flame field. + for i in 28: + var life := fposmod(_age*(0.65+float(i%4)*0.11)+i*0.618,1.0) + var angle := i*2.39996+_phase+sin(_age*0.7+i)*0.15 + var direction := Vector2.RIGHT.rotated(angle) + var tip := direction*(25.0+life*80.0) + tip += Vector2(sin(life*5+i)*7,-life*12) + var alpha := sin(life*PI)*fade + draw_line(tip-direction*(2+life*4),tip,Color(1,0.52,0.1,alpha),1.3,true) + draw_circle(tip,0.9,Color(1,0.9,0.5,alpha)) diff --git a/scripts/game/burning_gas.gd.uid b/scripts/game/burning_gas.gd.uid new file mode 100644 index 0000000..7d5d50d --- /dev/null +++ b/scripts/game/burning_gas.gd.uid @@ -0,0 +1 @@ +uid://1y3m4xkmf0g diff --git a/scripts/game/burning_gas.gdshader b/scripts/game/burning_gas.gdshader new file mode 100644 index 0000000..be8ff11 --- /dev/null +++ b/scripts/game/burning_gas.gdshader @@ -0,0 +1,60 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform float age = 0.0; +uniform float strength = 1.0; +uniform float phase = 0.0; + +float hash(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), + mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0)), f.x), f.y); +} + +float fbm(vec2 p) { + float value = 0.0; + float weight = 0.5; + mat2 turn = mat2(vec2(0.8, -0.6), vec2(0.6, 0.8)); + for (int i = 0; i < 4; i++) { + value += noise(p) * weight; + p = turn * p * 2.03 + vec2(7.1, 3.7); + weight *= 0.5; + } + return value; +} + +void fragment() { + vec2 p = (UV - 0.5) * 2.0; + float t = age * 0.65; + vec2 drift = vec2(t * 0.18, -t * 0.26); + vec2 q = vec2(fbm(p * 2.6 + drift + phase), fbm(p * 2.6 - drift + 12.4 + phase)); + vec2 curled = p * 4.2 + (q - 0.5) * 3.3; + float billow = fbm(curled + vec2(-t * 0.35, t * 0.5)); + float detail = fbm(curled * 2.2 - vec2(t * 0.6, t * 0.35)); + float radius = length(p); + float edge = radius + (q.x - 0.5) * 0.25 + (billow - 0.5) * 0.15; + float silhouette = 1.0 - smoothstep(0.62, 0.88, edge); + float core = 1.0 - smoothstep(0.1, 0.8, radius); + float heat = clamp(billow * 1.05 + detail * 0.32 + core * 0.22 - 0.25, 0.0, 1.0); + float fire = smoothstep(0.27, 0.58, heat) * silhouette; + float veins = pow(1.0 - abs(detail * 2.0 - 1.0), 5.0); + float hot = smoothstep(0.48, 0.82, heat + veins * 0.16); + vec3 flame = mix(vec3(0.65, 0.055, 0.004), vec3(1.2, 0.32, 0.015), smoothstep(0.2, 0.5, heat)); + flame = mix(flame, vec3(1.55, 0.91, 0.29), hot); + flame = mix(flame, vec3(1.7, 1.35, 0.82), pow(hot, 4.0) * 0.65); + vec3 smoke = mix(vec3(0.025, 0.028, 0.035), vec3(0.16, 0.095, 0.052), billow); + float smoke_alpha = silhouette * (0.4 + billow * 0.4); + float glow = exp(-radius * radius * 5.0) * 0.20; + vec3 color = mix(smoke, flame, fire); + color += vec3(0.4, 0.075, 0.005) * glow; + float alpha = max(smoke_alpha, fire * 0.95) + glow * (1.0 - silhouette); + COLOR = vec4(color, clamp(alpha * strength, 0.0, 1.0)); +} diff --git a/scripts/game/burning_gas.gdshader.uid b/scripts/game/burning_gas.gdshader.uid new file mode 100644 index 0000000..cdd3cd5 --- /dev/null +++ b/scripts/game/burning_gas.gdshader.uid @@ -0,0 +1 @@ +uid://bvjtcg8rb8a0j diff --git a/scripts/game/combat_vfx.gd b/scripts/game/combat_vfx.gd new file mode 100644 index 0000000..9dcd310 --- /dev/null +++ b/scripts/game/combat_vfx.gd @@ -0,0 +1,23 @@ +extends RefCounted +const CONTEXT := &"combat_visual_contact" + +static func mark_hit(target: Variant, point: Vector2, direction: Vector2) -> void: + if target is Node2D and is_instance_valid(target): target.set_meta(CONTEXT,{"point":point,"direction":direction}) + +static func consume(target: Node2D) -> Dictionary: + var contact: Dictionary = target.get_meta(CONTEXT,{"point":target.global_position+Vector2.UP*28,"direction":Vector2.DOWN}) + if target.has_meta(CONTEXT): target.remove_meta(CONTEXT) + return contact + +static func feedback(target: Node2D, shields: float, hull: float, kind: int, contact: Dictionary) -> void: + if shields > 0.0: + var shield := target.get_node_or_null("ShieldVisual") + if shield != null: shield.flash_hit(contact.point,kind) + if hull <= 0.0: return + var visual := target.get_node_or_null("HullDamageVisual") + if visual == null: + visual = Node2D.new() + visual.name = "HullDamageVisual" + visual.set_script(preload("res://scripts/game/hull_damage_visual.gd")) + target.add_child(visual) + visual.hit(contact.point,contact.direction,kind) diff --git a/scripts/game/combat_vfx.gd.uid b/scripts/game/combat_vfx.gd.uid new file mode 100644 index 0000000..31885b0 --- /dev/null +++ b/scripts/game/combat_vfx.gd.uid @@ -0,0 +1 @@ +uid://cay75l5mk202v diff --git a/scripts/game/distant_loot.gd b/scripts/game/distant_loot.gd index f72261e..1380699 100644 --- a/scripts/game/distant_loot.gd +++ b/scripts/game/distant_loot.gd @@ -24,6 +24,10 @@ static func drop(enemy: Node2D) -> void: for payload in roll(GameState.system_level,int(enemy.get("loot_tier"))==2,randi(),randf(),randf()): var pickup := Node2D.new() pickup.set_script(preload("res://scripts/items/expedition_pickup.gd")) - pickup.payload = payload + if payload.has("orb"): + pickup.set_script(preload("res://scripts/items/loot_pickup.gd")) + pickup.item = preload("res://scripts/items/special_orbs.gd").create_item(str(payload.orb)) + else: + pickup.payload = payload pickup.position = enemy.position+Vector2(randf_range(-36,36),65) enemy.get_parent().call_deferred("add_child",pickup) diff --git a/scripts/game/distant_sector.gd b/scripts/game/distant_sector.gd index 8cc342e..754338c 100644 --- a/scripts/game/distant_sector.gd +++ b/scripts/game/distant_sector.gd @@ -1,8 +1,6 @@ extends Node const GENERATOR := preload("res://scripts/levels/distant_sector_generator.gd") var game: Node2D -var _status: Label -var _refresh := 0.0 func _ready() -> void: name = "DistantSector" @@ -23,20 +21,6 @@ func _ready() -> void: expeditions.game = game expeditions.player = game.player game.add_child(expeditions) - var panel := PanelContainer.new() - panel.name = "DistantStatusPanel" - game.get_node("HUD/HUDRoot").add_child(panel) - panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) - panel.offset_left = -195 - panel.offset_right = 195 - panel.offset_top = 120 - panel.offset_bottom = 190 - var ui := preload("res://scripts/planets/freighter_menu.gd") - ui.style(panel,8,0.5) - var column := VBoxContainer.new() - panel.add_child(column) - _status = ui.label(column,"",12,Color("e9b4fc")) - _update_status() if GameState.distant_warp_arrival: GameState.distant_warp_arrival = false var arrival := CanvasLayer.new() @@ -78,10 +62,6 @@ func _physics_process(delta: float) -> void: if not game.system_cleared and game.get_remaining_enemies() == 0: if not game._boss_spawned and not game._pending_boss.is_empty(): game._spawn_boss() else: game.system_cleared = true - _refresh += delta - if _refresh >= 0.2: - _refresh = 0 - _update_status() func return_blocked() -> bool: if float(game.player.get("landing_combat_lock")) > 0: return true @@ -89,9 +69,6 @@ func return_blocked() -> bool: if not enemy.is_queued_for_deletion() and float(enemy.get("hull")) > 0 and enemy.global_position.distance_to(game.player.global_position) < 650: return true return false -func _update_status() -> void: - _status.text = "WYPRAWA %d · %s" % [GameState.system_level,"SEKTOR OCZYSZCZONY" if game.system_cleared else ("BOSS" if game._boss_spawned else "%d wrogów" % game.get_remaining_enemies())] - func _return_home() -> bool: var controller := game.get_node_or_null("PlanetExpeditions") if controller == null or not is_instance_valid(controller.screen) or not game.player.is_docked: return false diff --git a/scripts/game/enemy_plasma_projectile.gd b/scripts/game/enemy_plasma_projectile.gd index c1b46ff..d0cddc8 100644 --- a/scripts/game/enemy_plasma_projectile.gd +++ b/scripts/game/enemy_plasma_projectile.gd @@ -24,6 +24,8 @@ func _physics_process(delta: float) -> void: var next_position := global_position + direction * step var query := PhysicsRayQueryParameters2D.create(global_position, next_position, collision_mask, [get_rid()]) var hit := get_world_2d().direct_space_state.intersect_ray(query) + var exclusions: Array[RID] = [get_rid()] + hit = preload("res://scripts/game/shield_intersections.gd").intercept(hit,global_position,next_position,collision_mask,exclusions,get_tree()) if not hit.is_empty(): global_position = hit["position"] _hit_target(hit.get("collider")) @@ -48,7 +50,9 @@ func _hit_target(target: Variant) -> void: effect.global_position = global_position get_parent().add_child(effect) effect.call("configure", false, Color(1.0, 0.32, 0.38)) + effect.call("configure_contact",DamageTypes.Type.ENERGY,direction) if target is Node and target.has_method("take_damage") and not target.is_in_group("asteroids"): + preload("res://scripts/game/combat_vfx.gd").mark_hit(target,global_position,direction) if target is CharacterBody2D: target.call("take_damage", damage, false, DamageTypes.Type.ENERGY) else: diff --git a/scripts/game/enemy_ship.gd b/scripts/game/enemy_ship.gd index ac34c24..1b0a734 100644 --- a/scripts/game/enemy_ship.gd +++ b/scripts/game/enemy_ship.gd @@ -106,6 +106,7 @@ func _ready() -> void: hull = max_hull _sprite = $Sprite2D _shield_visual = Node2D.new() + _shield_visual.name = "ShieldVisual" _shield_visual.set_script(preload("res://scripts/game/shield_visual.gd")) _shield_visual.scale = Vector2.ONE * get_visual_size() / 64.0 add_child(_shield_visual) @@ -331,10 +332,17 @@ func _fire_plasma(direction: Vector2) -> void: var endpoint := muzzle + _laser_aim_direction(direction) * weapon_range var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, shot_mask, [get_rid()]) var hit := get_world_2d().direct_space_state.intersect_ray(query) + hit = preload("res://scripts/game/shield_intersections.gd").intercept(hit,muzzle,endpoint,shot_mask,[get_rid()],get_tree()) if not hit.is_empty(): endpoint = hit["position"] var target: Node = hit.get("collider") if target is CharacterBody2D and target.has_method("take_damage"): + preload("res://scripts/game/combat_vfx.gd").mark_hit(target,endpoint,(endpoint-muzzle).normalized()) + var contact_effect := IMPACT_EFFECT_SCENE.instantiate() + contact_effect.position = endpoint + get_parent().add_child(contact_effect) + contact_effect.configure(false,Color("75dfff")) + contact_effect.configure_contact(DamageTypes.Type.ENERGY,(endpoint-muzzle).normalized()) if target.has_method("take_enemy_weapon_hit"): target.take_enemy_weapon_hit(get_shot_damage(), false, DamageTypes.Type.ENERGY, self) else: @@ -421,6 +429,7 @@ func research_aim_direction(direction: Vector2, roll := -1.0, side := 0.0) -> Ve return direction.rotated(angle*(signf(side) if side != 0.0 else (-1.0 if randf()<0.5 else 1.0))) func take_damage(amount: float, critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC, bypass_absorption: bool = false) -> void: + var visual_contact := preload("res://scripts/game/combat_vfx.gd").consume(self) if is_queued_for_deletion() or hull <= 0.0: return var research := preload("res://scripts/planets/alien_research.gd") @@ -457,8 +466,7 @@ func take_damage(amount: float, critical: bool = false, damage_type: int = Damag _shield_overload(amount*4.0,damage_type) if is_instance_valid(_shield_visual): _shield_visual.set_shield_values(shields, max_shields) - if shield_damage > 0.0: - _shield_visual.flash_hit() + preload("res://scripts/game/combat_vfx.gd").feedback(self,shield_damage,hull_damage,damage_type,visual_contact) _show_damage_popup(actual, critical) if hull <= 0.0: if not _reward_claimed: diff --git a/scripts/game/engine_visual.gd b/scripts/game/engine_visual.gd index bc765fe..91beb16 100644 --- a/scripts/game/engine_visual.gd +++ b/scripts/game/engine_visual.gd @@ -1,110 +1,114 @@ extends Node2D - -const TRAIL_INTERVAL := 0.035 -const MAX_TRAIL_PARTICLES := 90 - +const TRAIL_INTERVAL := 0.025 +const MAX_TRAIL_PARTICLES := 96 +const PROFILES := { + "szerszen":{"color":Color("5dbdff"),"nozzles":[Vector2(-6.5,26),Vector2(6.5,26)],"width":11.0}, + "bastion":{"color":Color("ffb45c"),"nozzles":[Vector2(-22,24),Vector2(0,27),Vector2(22,24)],"width":13.0}, + "wedrowiec":{"color":Color("55f1df"),"nozzles":[Vector2(-9.5,23),Vector2(0,27),Vector2(9.5,23)],"width":9.0}, + "zmija":{"color":Color("bf7cff"),"nozzles":[Vector2(-13,23),Vector2(0,26),Vector2(13,23)],"width":11.0} +} var _ship: CharacterBody2D var _thrust_strength := 0.0 var _engine_phase := 0.0 var _trail_emission_timer := 0.0 var _trail_particles: Array[Dictionary] = [] var _mini_boost := false +var _boost_strength := 0.0 +var _profile: Dictionary +var _plumes: Array[ShaderMaterial] = [] +var _rcs: Array[ShaderMaterial] = [] +var _maneuver := Vector3.ZERO +var _previous_position := Vector2.ZERO func _ready() -> void: _ship = get_parent() as CharacterBody2D set_as_top_level(true) z_index = -1 - if is_instance_valid(_ship): - global_position = _ship.global_position - global_rotation = _ship.global_rotation - -func _process(delta: float) -> void: - if not is_instance_valid(_ship): - return - + _profile = PROFILES[preload("res://scripts/game/ship_catalog.gd").for_path(GameState.selected_ship_path).asset] + for nozzle in _profile.nozzles: + _plumes.append(_add_plume(nozzle,0.0,float(_profile.width),90.0)) + for port in [[Vector2(-10,-16),PI*0.5],[Vector2(10,-16),-PI*0.5],[Vector2(-15,12),PI*0.5],[Vector2(15,12),-PI*0.5],[Vector2(-7,-23),PI],[Vector2(7,-23),PI]]: + _rcs.append(_add_plume(port[0],float(port[1]),6.0,36.0)) global_position = _ship.global_position global_rotation = _ship.global_rotation - _engine_phase += delta - _mini_boost = bool(_ship.call("is_mini_boosting")) - var thrust_active := Input.is_action_pressed("ship_thrust") or _mini_boost - if bool(_ship.get("_is_destroyed")): - thrust_active = false - var target_strength := 1.0 if thrust_active else 0.0 - _thrust_strength = move_toward(_thrust_strength, target_strength, 5.5 * delta) + _previous_position = global_position +func _add_plume(at: Vector2, angle: float, width: float, length: float) -> ShaderMaterial: + var mount := Node2D.new() + mount.position = at + mount.rotation = angle + add_child(mount) + var quad := ColorRect.new() + quad.mouse_filter = Control.MOUSE_FILTER_IGNORE + quad.position = Vector2(-width*0.5,0) + quad.size = Vector2(width,length) + var material := ShaderMaterial.new() + material.shader = preload("res://scripts/game/thruster_plume.gdshader") + material.set_shader_parameter("plume_color",_profile.color) + material.set_shader_parameter("phase",float(_plumes.size()+_rcs.size())*8.7) + quad.material = material + mount.add_child(quad) + return material + +func _process(delta: float) -> void: + if not is_instance_valid(_ship): return + global_position = _ship.global_position + global_rotation = _ship.global_rotation + if global_position.distance_to(_previous_position) > 400: _trail_particles.clear() + _previous_position = global_position + _engine_phase += delta + var active := not bool(_ship.get("_is_destroyed")) and not bool(_ship.get("is_docked")) + _mini_boost = active and bool(_ship.call("is_mini_boosting")) + var boosted := active and bool(_ship.call("is_boosting")) + var thrust_active := active and (Input.is_action_pressed("ship_thrust") or boosted) + var target_strength := (1.0 if thrust_active else 0.0) + _thrust_strength = move_toward(_thrust_strength,target_strength,delta*(9.0 if thrust_active else 6.0)) + _boost_strength = move_toward(_boost_strength,1.0 if boosted else 0.0,delta*7.0) + if not active: + _thrust_strength = 0.0 + _trail_particles.clear() + for plume in _plumes: + plume.set_shader_parameter("power",_thrust_strength) + plume.set_shader_parameter("boost",_boost_strength) + var yaw := Input.get_axis("ship_turn_left","ship_turn_right") if active else 0.0 + var strafe := Input.get_axis("ship_strafe_left","ship_strafe_right") if active else 0.0 + var braking := 1.0 if active and Input.is_action_pressed("ship_brake") else 0.0 + _maneuver = _maneuver.move_toward(Vector3(yaw,strafe,braking),delta*12.0) + var powers := [maxf(_maneuver.x,0.0)+maxf(_maneuver.y,0.0),maxf(-_maneuver.x,0.0)+maxf(-_maneuver.y,0.0),maxf(-_maneuver.x,0.0)+maxf(_maneuver.y,0.0),maxf(_maneuver.x,0.0)+maxf(-_maneuver.y,0.0),_maneuver.z,_maneuver.z] + for i in _rcs.size(): _rcs[i].set_shader_parameter("power",minf(0.85,float(powers[i]))) if thrust_active: _trail_emission_timer += delta - while _trail_emission_timer >= TRAIL_INTERVAL: + var emitted := 0 + while _trail_emission_timer >= TRAIL_INTERVAL and emitted < 8: _trail_emission_timer -= TRAIL_INTERVAL _spawn_trail_particle() + emitted += 1 + _trail_emission_timer = minf(_trail_emission_timer,TRAIL_INTERVAL) _update_trail_particles(delta) queue_redraw() func _spawn_trail_particle() -> void: - if _trail_particles.size() >= MAX_TRAIL_PARTICLES: - return - var aft := Vector2.DOWN.rotated(_ship.global_rotation) - var side := Vector2(-aft.y, aft.x) - var angle := randf_range(-0.18, 0.18) - var particle_direction := aft.rotated(angle) - var life := randf_range(0.45, 0.8) - _trail_particles.append({ - "position": _ship.global_position + aft * 21.0, - "velocity": _ship.velocity + particle_direction * randf_range(30.0, 58.0) + side * randf_range(-6.0, 6.0), - "age": 0.0, - "life": life, - "size": randf_range(1.5, 3.0), - "boost": _mini_boost, - }) + for index in _profile.nozzles.size(): + if _trail_particles.size() >= MAX_TRAIL_PARTICLES: break + var aft := Vector2.DOWN.rotated(global_rotation) + _trail_particles.append({"position":_ship.to_global(_profile.nozzles[index]),"velocity":_ship.velocity*0.12+aft*(42.0 if _mini_boost else 20.0),"age":0.0,"life":0.48 if _mini_boost else 0.28,"size":2.3 if _mini_boost else 1.5,"boost":_mini_boost,"lane":index}) func _update_trail_particles(delta: float) -> void: - for particle_index in range(_trail_particles.size() - 1, -1, -1): - var particle := _trail_particles[particle_index] - particle["position"] = particle["position"] + particle["velocity"] * delta - particle["age"] = float(particle["age"]) + delta - _trail_particles[particle_index] = particle - if float(particle["age"]) >= float(particle["life"]): - _trail_particles.remove_at(particle_index) + for i in range(_trail_particles.size()-1,-1,-1): + var p := _trail_particles[i] + p.position += p.velocity*delta + p.age += delta + if p.age >= p.life: _trail_particles.remove_at(i) func _draw() -> void: - _draw_trail() - if _thrust_strength <= 0.01: - return - - var pulse := 0.82 + 0.18 * sin(_engine_phase * 25.0) - var length := (28.0 + pulse * 14.0) * _thrust_strength * (1.35 if _mini_boost else 1.0) - var outer_color := Color(1.0, 0.28, 0.03) if _mini_boost else Color(0.04, 0.58, 1.0) - var inner_color := Color(1.0, 0.62, 0.12) if _mini_boost else Color(0.24, 0.9, 1.0) - var core_color := Color(1.0, 0.92, 0.7) if _mini_boost else Color(0.88, 0.98, 1.0) - var base_y := 22.0 - var outer_tip := base_y + length - var inner_tip := base_y + length * 0.76 - var core_tip := base_y + length * 0.48 - - draw_colored_polygon(PackedVector2Array([ - Vector2(-5.2, base_y), Vector2(-3.2, base_y + 6.0), Vector2(0.0, outer_tip), - Vector2(3.2, base_y + 6.0), Vector2(5.2, base_y), - ]), Color(outer_color, 0.48 * _thrust_strength)) - draw_colored_polygon(PackedVector2Array([ - Vector2(-2.8, base_y), Vector2(-1.8, base_y + 4.0), Vector2(0.0, inner_tip), - Vector2(1.8, base_y + 4.0), Vector2(2.8, base_y), - ]), Color(inner_color, 0.78 * _thrust_strength)) - draw_colored_polygon(PackedVector2Array([ - Vector2(-1.0, base_y), Vector2(0.0, core_tip), Vector2(1.0, base_y), - ]), Color(core_color, 0.92 * _thrust_strength)) - draw_circle(Vector2(0.0, base_y), 2.3, Color(inner_color, 0.82 * _thrust_strength)) - -func _draw_trail() -> void: - for particle in _trail_particles: - var age := float(particle["age"]) - var life := float(particle["life"]) - var fade := 1.0 - clampf(age / life, 0.0, 1.0) - var current_position: Vector2 = particle["position"] - var particle_velocity: Vector2 = particle["velocity"] - var particle_size := float(particle["size"]) - var current_local := to_local(current_position) - var tail_local := to_local(current_position - particle_velocity.normalized() * particle_size * 3.5) - var trail_color := Color(1.0, 0.42, 0.06) if bool(particle.get("boost", false)) else Color(0.22, 0.78, 1.0) - var particle_color := Color(1.0, 0.76, 0.3) if bool(particle.get("boost", false)) else Color(0.62, 0.94, 1.0) - draw_line(tail_local, current_local, Color(trail_color, fade * 0.52), particle_size * 0.72, true) - draw_circle(current_local, particle_size * fade, Color(particle_color, fade * 0.72)) + var previous := {} + for p in _trail_particles: + var fade := 1.0-float(p.age)/float(p.life) + var point := to_local(p.position) + var lane: int = p.lane + if previous.has(lane): + var prior: Vector2 = previous[lane] + if point.distance_to(prior)<60: + draw_line(prior,point,Color(_profile.color,fade*0.055),float(p.size)*4.0,true) + draw_line(prior,point,Color(_profile.color,fade*0.25),float(p.size)*0.8,true) + previous[lane] = point diff --git a/scripts/game/game_state.gd b/scripts/game/game_state.gd index b4e144b..39cf5f1 100644 --- a/scripts/game/game_state.gd +++ b/scripts/game/game_state.gd @@ -108,8 +108,9 @@ func advance_play_time(delta: float, scene: Node) -> void: var expedition := scene.get_node_or_null("PlanetExpeditions") var location_open := expedition != null and is_instance_valid(expedition.screen) if get_tree().paused and not location_open: return + var researching := preload("res://scripts/planets/laboratory.gd").advance(captain.expedition,delta) var network := preload("res://scripts/planets/mining_network.gd") - if network.advance(captain.expedition, delta) <= 0: return + if network.advance(captain.expedition, delta) <= 0 and not researching: return _production_signal_time += delta if _production_signal_time >= 1.0: _production_signal_time = fmod(_production_signal_time, 1.0) diff --git a/scripts/game/hull_damage_visual.gd b/scripts/game/hull_damage_visual.gd new file mode 100644 index 0000000..509b2ee --- /dev/null +++ b/scripts/game/hull_damage_visual.gd @@ -0,0 +1,66 @@ +extends Node2D +static var _soft: GradientTexture2D +var _sparks: Array[Dictionary] = [] +var _smoke: Array[Dictionary] = [] +var _timer := 0.0 +var _last_hit := -1.0 +var _age := 0.0 + +func _ready() -> void: + z_index = 3 + if _soft == null: + _soft = GradientTexture2D.new() + _soft.width = 48 + _soft.height = 48 + _soft.fill = GradientTexture2D.FILL_RADIAL + _soft.fill_from = Vector2(0.5,0.5) + _soft.fill_to = Vector2(1,0.5) + var gradient := Gradient.new() + gradient.set_color(0,Color.WHITE) + gradient.set_color(1,Color(1,1,1,0)) + _soft.gradient = gradient + +func hit(at: Vector2, direction: Vector2, kind: int) -> void: + if _age-_last_hit<0.08: return + _last_hit = _age + var owner := get_parent() as Node2D + var radius := float(owner.get_visual_size())*0.4 if owner.has_method("get_visual_size") else 24.0 + var local := owner.to_local(at).limit_length(radius) + var point := owner.to_global(local) + for i in 7: + if _sparks.size()>=36: _sparks.pop_front() + _sparks.append({"point":point,"velocity":(-direction).rotated(randf_range(-1.4,1.4))*randf_range(20,75),"age":0.0,"life":randf_range(0.12,0.3),"kind":kind}) + +func _process(delta: float) -> void: + _age += delta + var owner := get_parent() + if owner.has_method("is_mini_boosting") and (bool(owner.get("_is_destroyed")) or bool(owner.get("is_docked"))): + _sparks.clear() + _smoke.clear() + queue_redraw() + return + _timer -= delta + var ratio := float(owner.get("hull"))/maxf(1.0,float(owner.get("max_hull"))) + if ratio<0.35 and ratio>0 and _timer<=0: + _timer = 0.15+ratio*0.6 + var local := Vector2(-8 if int(_age*3)%2 else 8,7) + _smoke.append({"point":owner.to_global(local),"velocity":owner.velocity*0.15+Vector2(randf_range(-4,4),-6),"age":0.0,"life":0.65,"radius":randf_range(3,6)}) + if _smoke.size()>10: _smoke.pop_front() + for pool in [_sparks,_smoke]: + for i in range(pool.size()-1,-1,-1): + pool[i].age += delta + pool[i].point += pool[i].velocity*delta + if pool[i].age >= pool[i].life: pool.remove_at(i) + queue_redraw() + +func _draw() -> void: + for puff in _smoke: + var t := float(puff.age)/float(puff.life) + var radius := float(puff.radius)*(1+t*1.8) + draw_texture_rect(_soft,Rect2(to_local(puff.point)-Vector2.ONE*radius,Vector2.ONE*radius*2),false,Color(0.35,0.26,0.22,(1-t)*0.3)) + if t<0.2: draw_circle(to_local(puff.point),1.2,Color(1,0.48,0.12,(1-t*5)*0.8)) + for spark in _sparks: + var fade := 1.0-float(spark.age)/float(spark.life) + var point := to_local(spark.point) + var color: Color = [Color("ffc176"),Color("8de4ff"),Color("a4ff70")][int(spark.kind)] + draw_line(point,point-to_local(global_position+spark.velocity.normalized()*4),Color(color,fade),1.0,true) diff --git a/scripts/game/hull_damage_visual.gd.uid b/scripts/game/hull_damage_visual.gd.uid new file mode 100644 index 0000000..577a192 --- /dev/null +++ b/scripts/game/hull_damage_visual.gd.uid @@ -0,0 +1 @@ +uid://d2ntnpjk8svra diff --git a/scripts/game/impact_effect.gd b/scripts/game/impact_effect.gd index 79cec70..1d11088 100644 --- a/scripts/game/impact_effect.gd +++ b/scripts/game/impact_effect.gd @@ -1,9 +1,10 @@ extends Node2D - -var _effect_color := Color(1.0, 0.72, 0.32) +var _effect_color := Color(1.0,0.72,0.32) var _elapsed := 0.0 -var _duration := 0.28 +var _duration := 0.30 var _is_explosion := false +var _kind := DamageTypes.Type.ENERGY +var _normal := Vector2.UP var _directions: Array[Vector2] = [] var _speeds: Array[float] = [] var _sizes: Array[float] = [] @@ -11,44 +12,69 @@ var _sizes: Array[float] = [] func configure(is_explosion: bool, effect_color: Color) -> void: _is_explosion = is_explosion _effect_color = effect_color - _duration = 0.58 if is_explosion else 0.28 - var rng := RandomNumberGenerator.new() - rng.randomize() - var spark_count := 26 if is_explosion else 9 - for _spark_index in spark_count: - _directions.append(Vector2.RIGHT.rotated(rng.randf_range(0.0, TAU))) - _speeds.append(rng.randf_range(70.0, 230.0) if is_explosion else rng.randf_range(65.0, 155.0)) - _sizes.append(rng.randf_range(2.0, 5.0) if is_explosion else rng.randf_range(1.5, 3.5)) - z_index = 40 - queue_redraw() + _duration = 0.58 if is_explosion else 0.30 + _seed_sparks() + +func configure_contact(kind: int, incoming: Vector2) -> void: + _kind = clampi(kind,0,2) + _normal = -incoming.normalized() if incoming.length_squared()>0.001 else Vector2.UP + _effect_color = [Color("ffc071"),Color("68d9ff"),Color("88f35b")][_kind] + if not _is_explosion: _duration = [0.28,0.34,0.44][_kind] + _seed_sparks() + +func _seed_sparks() -> void: + _directions.clear() + _speeds.clear() + _sizes.clear() + var count := 22 if _is_explosion else (13 if _kind==0 else 8) + for i in count: + var angle := randf_range(-1.35,1.35) + var direction := Vector2.RIGHT.rotated(randf()*TAU) if _is_explosion else _normal.rotated(angle) + _directions.append(direction) + _speeds.append(randf_range(65,230) if _kind==0 else randf_range(25,100)) + _sizes.append(randf_range(1.1,2.4)) + +func _ready() -> void: + z_index = 30 + add_to_group("combat_impacts") + if get_tree().get_nodes_in_group("combat_impacts").size()>72: + for old in get_tree().get_nodes_in_group("combat_impacts"): + if old!=self and not old.is_queued_for_deletion(): + old.queue_free() + break func _process(delta: float) -> void: _elapsed += delta - if _elapsed >= _duration: - queue_free() - return - queue_redraw() + if _elapsed>=_duration: queue_free() + else: queue_redraw() func _draw() -> void: - var progress := clampf(_elapsed / _duration, 0.0, 1.0) - var fade := 1.0 - progress - var ring_start := 9.0 if _is_explosion else 3.0 - var ring_growth := 72.0 if _is_explosion else 38.0 - draw_arc( - Vector2.ZERO, - ring_start + ring_growth * progress, - 0.0, - TAU, - 40, - Color(_effect_color.r, _effect_color.g, _effect_color.b, fade * (0.75 if _is_explosion else 0.55)), - 2.5 if _is_explosion else 1.5 - ) + var t := clampf(_elapsed/_duration,0,1) + var fade := 1.0-t + var flash := maxf(0.0,1.0-t*4.0) + var glow_radius := 20.0 if _is_explosion else 9.0 + for layer in range(4,0,-1): + draw_circle(Vector2.ZERO,glow_radius*float(layer)/4.0,Color(_effect_color,flash*0.035)) + draw_circle(Vector2.ZERO,2.5*flash,Color(_effect_color.lightened(0.8),flash)) if _is_explosion: - draw_circle(Vector2.ZERO, 18.0 * (1.0 - progress), Color(1.0, 0.82, 0.55, fade * 0.65)) - for spark_index in _directions.size(): - var direction := _directions[spark_index] - var travel := _speeds[spark_index] * _elapsed - var tip := direction * travel - var tail := tip - direction * _sizes[spark_index] * (2.0 + fade * 2.0) - draw_line(tail, tip, Color(_effect_color.r, _effect_color.g, _effect_color.b, fade), _sizes[spark_index]) - draw_circle(tip, maxf(0.7, _sizes[spark_index] * 0.45), Color(1.0, 0.9, 0.72, fade)) + draw_arc(Vector2.ZERO,7+t*65,0,TAU,48,Color(_effect_color,fade*0.5),1.5,true) + if _kind==DamageTypes.Type.ENERGY: + for i in 4: + var angle := float(i)*TAU/4+_normal.angle()+t*0.4 + draw_arc(Vector2.ZERO,4+t*18,angle,angle+0.8,10,Color(_effect_color,fade*0.65),1.1,true) + var a := Vector2.RIGHT.rotated(angle)*4 + var b := Vector2.RIGHT.rotated(angle+0.25)*(9+t*12) + draw_polyline(PackedVector2Array([a,(a+b)*0.5+Vector2(2,-2),b]),Color(_effect_color,fade*0.65),1.0,true) + elif _kind==DamageTypes.Type.RADIATION: + for i in 3: + var angle := i*TAU/3+t*2 + draw_arc(Vector2.ZERO,5+t*17,angle,angle+0.95,12,Color(_effect_color,fade*0.4),1.2,true) + for i in _directions.size(): + var direction := _directions[i] + var point := direction*_speeds[i]*_elapsed + if _kind==DamageTypes.Type.KINETIC: + draw_line(point-direction*(3+fade*7),point,Color(_effect_color,fade),_sizes[i]*fade+0.4,true) + if i%3==0: + draw_line(point+Vector2(-1,0),point+Vector2(1,1),Color(0.65,0.72,0.79,fade),2.0,true) + else: + draw_circle(point,_sizes[i]*fade,Color(_effect_color,fade*0.75)) diff --git a/scripts/game/laser_beam_visual.gd b/scripts/game/laser_beam_visual.gd index 75c89e8..5b3a995 100644 --- a/scripts/game/laser_beam_visual.gd +++ b/scripts/game/laser_beam_visual.gd @@ -1,6 +1,7 @@ extends Line2D var _time := 0.0 +var radiation_beam := false func _ready() -> void: # Draw the beam in layers instead of a flat line. No bloom is required. @@ -15,14 +16,16 @@ func _process(delta: float) -> void: func _draw() -> void: if points.size() < 2: return - var start := points[0] - var end := points[1] var pulse := 0.88 + sin(_time * 65.0) * 0.12 - draw_line(start, end, Color(0.1, 0.65, 1.0, 0.09 * pulse), 16, true) - draw_line(start, end, Color(0.15, 0.8, 1.0, 0.2 * pulse), 9, true) - draw_line(start, end, Color(0.3, 0.92, 1.0, 0.7), 3.8, true) - draw_line(start, end, Color(0.9, 1.0, 1.0, 0.96), 1.4, true) - for point in [start, end]: - draw_circle(point, 9 * pulse, Color(0.1, 0.8, 1.0, 0.12)) - draw_circle(point, 4 * pulse, Color(0.35, 0.95, 1.0, 0.6)) - draw_circle(point, 1.8, Color(0.9, 1.0, 1.0)) + var tint := Color(0.25,1,0.16) if radiation_beam else Color(0.3,0.92,1) + for i in points.size()-1: + var start := points[i] + var end := points[i+1] + draw_line(start,end,Color(tint,0.09*pulse),16,true) + draw_line(start,end,Color(tint,0.2*pulse),9,true) + draw_line(start,end,Color(tint,0.7),3.8,true) + draw_line(start,end,Color(tint.lightened(0.8),0.96),1.4,true) + for point in points: + draw_circle(point,9*pulse,Color(tint,0.12)) + draw_circle(point,4*pulse,Color(tint,0.6)) + draw_circle(point,1.8,tint.lightened(0.8)) diff --git a/scripts/game/radiation_aura.gd b/scripts/game/radiation_aura.gd new file mode 100644 index 0000000..ddc7819 --- /dev/null +++ b/scripts/game/radiation_aura.gd @@ -0,0 +1,18 @@ +extends Node2D +var status: Node +var _age := 0.0 +func _ready() -> void: + z_index = 2 +func _process(delta: float) -> void: + _age += delta + visible = is_instance_valid(status) and status.remaining>0.0 + if visible: queue_redraw() +func _draw() -> void: + var owner := get_parent() + var radius := float(owner.get_visual_size())*0.47 if owner.has_method("get_visual_size") else 29.0 + var pulse := 0.5+0.5*sin(_age*5) + for i in 5: + var angle := i*TAU/5.0+_age*0.35 + draw_arc(Vector2.ZERO,radius,angle,angle+0.32,8,Color(0.5,1,0.24,0.12+pulse*0.15),1.0,true) + var point := Vector2.RIGHT.rotated(angle)*radius + draw_circle(point,1.0,Color(0.7,1,0.4,0.2+pulse*0.25)) diff --git a/scripts/game/radiation_aura.gd.uid b/scripts/game/radiation_aura.gd.uid new file mode 100644 index 0000000..6bf1db0 --- /dev/null +++ b/scripts/game/radiation_aura.gd.uid @@ -0,0 +1 @@ +uid://bkn33hv2w6sas diff --git a/scripts/game/radiation_status.gd b/scripts/game/radiation_status.gd index f107fd7..09ba186 100644 --- a/scripts/game/radiation_status.gd +++ b/scripts/game/radiation_status.gd @@ -9,6 +9,8 @@ var shield_piercing := 0.0 var _tick_elapsed := 0.0 var _base_color := Color.WHITE var _sprite: Sprite2D +var _aura: Node2D +var _visual_time := 0.0 static func apply(target: Node, damage_per_second: float, player_hit: bool, piercing: float = 0.0) -> void: if not target.has_method("take_damage") or not target is CharacterBody2D: @@ -26,16 +28,21 @@ static func apply(target: Node, damage_per_second: float, player_hit: bool, pier status.credited_to_player = player_hit func _ready() -> void: + _aura = Node2D.new() + _aura.set_script(preload("res://scripts/game/radiation_aura.gd")) + _aura.status = self + get_parent().add_child.call_deferred(_aura) _sprite = get_parent().get_node_or_null("Sprite2D") if _sprite != null: _base_color = _sprite.modulate func _physics_process(delta: float) -> void: + _visual_time += delta var step := minf(delta, remaining) remaining = maxf(0.0, remaining - delta) var target := get_parent() if _sprite != null: - _sprite.modulate = Color(0.3, 1.0, 0.35) if remaining > 0.0 else _base_color + _sprite.modulate = _base_color.lerp(Color(0.65,1.0,0.55),0.22+0.12*sin(_visual_time*5)) if remaining > 0.0 else _base_color _tick_elapsed += step if _tick_elapsed >= 0.5 or (remaining <= 0.0 and _tick_elapsed > 0.0): step = _tick_elapsed diff --git a/scripts/game/shield_intersections.gd b/scripts/game/shield_intersections.gd new file mode 100644 index 0000000..252e5c7 --- /dev/null +++ b/scripts/game/shield_intersections.gd @@ -0,0 +1,30 @@ +extends RefCounted + +static func intercept(world_hit: Dictionary, start: Vector2, end: Vector2, mask: int, excluded: Array[RID], tree: SceneTree) -> Dictionary: + var nearest := world_hit + var distance := start.distance_squared_to(world_hit.position) if not world_hit.is_empty() else INF + var segment := end-start + var length_squared := segment.length_squared() + if length_squared < 0.000001: return nearest + for target in tree.get_nodes_in_group("shielded_combatants"): + if not is_instance_valid(target) or target.is_queued_for_deletion(): continue + if (int(target.collision_layer)&mask)==0 or target.get_rid() in excluded: continue + if float(target.shields)<=0.0 or float(target.hull)<=0.0: continue + if target.has_method("is_mini_boosting") and (bool(target.is_docked) or bool(target._is_destroyed)): continue + var shield := target.get_node_or_null("ShieldVisual") as Node2D + if shield == null: continue + var radius := float(shield.shield_radius)*shield.global_transform.x.length() + var offset := start-shield.global_position + var b := offset.dot(segment) + var c := offset.length_squared()-radius*radius + var discriminant := b*b-length_squared*c + if discriminant < 0.0: continue + var t := (-b-sqrt(discriminant))/length_squared + if c < 0.0: t = 0.0 + if t < 0.0 or t > 1.0: continue + var point := start+segment*t + var candidate := start.distance_squared_to(point) + if candidate < distance: + distance = candidate + nearest = {"position":point,"collider":target,"normal":(point-shield.global_position).normalized(),"shield":true} + return nearest diff --git a/scripts/game/shield_intersections.gd.uid b/scripts/game/shield_intersections.gd.uid new file mode 100644 index 0000000..b852c9b --- /dev/null +++ b/scripts/game/shield_intersections.gd.uid @@ -0,0 +1 @@ +uid://dc2pouqr4e0dv diff --git a/scripts/game/shield_visual.gd b/scripts/game/shield_visual.gd index 707e488..9101e24 100644 --- a/scripts/game/shield_visual.gd +++ b/scripts/game/shield_visual.gd @@ -1,50 +1,61 @@ extends Node2D - +@export var shield_radius := 50.0 var current_shields := 100.0 var max_shields := 100.0 var _hit_flash_timer := 0.0 +var _hits: Array[Dictionary] = [] +var _time := 0.0 + +func _ready() -> void: + var owner := get_parent() + if owner is CharacterBody2D and owner.has_method("take_damage"): + owner.add_to_group("shielded_combatants") func _process(delta: float) -> void: - visible = current_shields > 0.0 - if not visible: - return - _hit_flash_timer = maxf(0.0, _hit_flash_timer - delta) - queue_redraw() + _time += delta + _hit_flash_timer = maxf(0.0,_hit_flash_timer-delta) + for i in range(_hits.size()-1,-1,-1): + _hits[i].age += delta + if _hits[i].age >= 0.42: _hits.remove_at(i) + visible = current_shields > 0.0 or not _hits.is_empty() + var owner := get_parent() + if owner.has_method("is_mini_boosting") and bool(owner.get("_is_destroyed")): visible = false + if visible: queue_redraw() func set_shield_values(current: float, maximum: float) -> void: current_shields = current - max_shields = maxf(1.0, maximum) - visible = current_shields > 0.0 + max_shields = maxf(1.0,maximum) + visible = current_shields > 0.0 or not _hits.is_empty() queue_redraw() -func flash_hit() -> void: +func flash_hit(world_point := Vector2.INF, damage_type := DamageTypes.Type.ENERGY, strength := 1.0) -> void: _hit_flash_timer = 0.22 + var direction := Vector2.UP if not world_point.is_finite() else to_local(world_point).normalized() + if direction.length_squared()<0.01: direction = Vector2.UP + if _hits.size()>=6: _hits.pop_front() + _hits.append({"angle":direction.angle(),"age":0.0,"kind":damage_type,"strength":clampf(strength,0.4,1.4)}) + visible = true queue_redraw() func _draw() -> void: - if current_shields <= 0.0: - return - var shield_ratio := clampf(current_shields / max_shields, 0.0, 1.0) - var pulse := 0.035 + (sin(float(Time.get_ticks_msec()) * 0.004) + 1.0) * 0.018 - var flash := clampf(_hit_flash_timer / 0.22, 0.0, 1.0) - draw_circle(Vector2.ZERO, 35.0, Color(0.12, 0.72, 1.0, pulse + flash * 0.08)) - draw_arc( - Vector2.ZERO, - 36.0, - 0.0, - TAU, - 64, - Color(0.35, 0.84, 1.0, 0.3 + shield_ratio * 0.25 + flash * 0.45), - 2.0 + flash * 2.0 - ) - for arc_index in 3: - var start_angle := float(arc_index) * TAU / 3.0 + 0.2 - draw_arc( - Vector2.ZERO, - 40.0, - start_angle, - start_angle + 0.72, - 16, - Color(0.28, 0.78, 1.0, 0.28 + flash * 0.55), - 1.5 + flash - ) + var ratio := clampf(current_shields/max_shields,0.0,1.0) + if current_shields>0: + var ambient := 0.025+sin(_time*1.6)*0.008 + draw_arc(Vector2.ZERO,shield_radius,0,TAU,80,Color(0.25,0.7,1,0.10+ratio*0.13),0.8,true) + draw_arc(Vector2.ZERO,shield_radius-1.0,0,TAU,80,Color(0.12,0.6,1,ambient),4.0,true) + for hit in _hits: + var t := float(hit.age)/0.42 + var fade := (1.0-t)*(1.0-t)*float(hit.strength) + var color: Color = [Color("ffd18b"),Color("75dfff"),Color("a1f475")][int(hit.kind)] + var angle: float = hit.angle + var spread := 0.12+t*1.2 + var radius := shield_radius+t*2.0 + for layer in 3: + draw_arc(Vector2.ZERO,radius-float(layer)*1.5,angle-spread,angle+spread,28,Color(color,fade*(0.6 if layer==0 else 0.13)),1.4 if layer==0 else 3.0,true) + var contact := Vector2.RIGHT.rotated(angle)*shield_radius + for ring in 2: + draw_arc(contact,3.0+t*(17.0+ring*8.0),angle+PI*0.5,angle+PI*1.5,22,Color(color,fade*(0.5 if ring==0 else 0.17)),1.1,true) + if t<0.35: + var core := (1.0-t/0.35)*float(hit.strength) + draw_circle(contact,5*core,Color(color,core*0.22)) + draw_circle(contact,1.8*core,Color(color.lightened(0.7),core)) diff --git a/scripts/game/ship_controller.gd b/scripts/game/ship_controller.gd index 402023d..343a485 100644 --- a/scripts/game/ship_controller.gd +++ b/scripts/game/ship_controller.gd @@ -171,12 +171,16 @@ func _ensure_input_action(action_name: StringName, physical_key: Key) -> void: InputMap.action_add_event(action_name, event) func take_enemy_weapon_hit(amount: float, critical: bool, damage_type: int, source: Node) -> void: + if is_instance_valid(source) and source is Node2D and not has_meta(preload("res://scripts/game/combat_vfx.gd").CONTEXT): + var incoming: Vector2 = (global_position-source.global_position).normalized() + preload("res://scripts/game/combat_vfx.gd").mark_hit(self,global_position-incoming*28,incoming) take_damage(amount,critical,damage_type,source) func get_weapon_energy_cost(amount: float) -> float: return amount*0.5 if _shield_reserve_remaining > 0.0 else amount func take_damage(amount: float, _critical: bool = false, damage_type: int = DamageTypes.Type.KINETIC, source: Node = null) -> void: + var visual_contact := preload("res://scripts/game/combat_vfx.gd").consume(self) if amount > 0 and not is_docked: landing_combat_lock = 6.0 if _is_destroyed or is_docked or amount <= 0.0: @@ -247,8 +251,7 @@ func take_damage(amount: float, _critical: bool = false, damage_type: int = Dama _bulkhead_cooldown = 45.0 _bulkhead_repair_remaining = 5.0 _hull_technology_feedback("AWARYJNE GRODZIE") - if shield_damage > 0.0 and shields > 0.0: - _shield_visual.call("flash_hit") + preload("res://scripts/game/combat_vfx.gd").feedback(self,shield_damage,maxf(0,hull_before-hull),damage_type,visual_contact) _update_shield_visual() if hull <= 0.0: _destroy_ship() diff --git a/scripts/game/thruster_plume.gdshader b/scripts/game/thruster_plume.gdshader new file mode 100644 index 0000000..03c5b28 --- /dev/null +++ b/scripts/game/thruster_plume.gdshader @@ -0,0 +1,27 @@ +shader_type canvas_item; +render_mode unshaded; +uniform vec4 plume_color : source_color = vec4(0.1,0.65,1.0,1.0); +uniform float power = 0.0; +uniform float boost = 0.0; +uniform float phase = 0.0; +float hash(vec2 p) { p=fract(p*vec2(123.34,456.21)); p+=dot(p,p+45.32); return fract(p.x*p.y); } +float noise(vec2 p) { + vec2 i=floor(p),f=fract(p); f=f*f*(3.0-2.0*f); + return mix(mix(hash(i),hash(i+vec2(1,0)),f.x),mix(hash(i+vec2(0,1)),hash(i+vec2(1)),f.x),f.y); +} +void fragment() { + float y=UV.y; + float length=mix(0.30,0.85,boost)*(0.55+power*0.45); + float travel=y/max(length,0.01); + float turbulence=noise(vec2(y*18.0-TIME*24.0,phase+TIME*2.0)); + float width=max(0.018,0.33*pow(max(0.0,1.0-travel),0.65)); + float center=0.5+(turbulence-0.5)*0.11*travel; + float dist=abs(UV.x-center); + float body=1.0-smoothstep(width*0.22,width,dist); + float end=1.0-smoothstep(0.65,1.0,travel); + float core=exp(-dist*dist/max(0.0001,width*width*0.045))*(1.0-smoothstep(0.15,0.65,travel)); + float diamonds=pow(max(0.0,cos(travel*33.0-TIME*7.0)),6.0)*boost*0.22; + vec3 color=mix(plume_color.rgb,vec3(0.8,0.95,1.0),core*0.9+diamonds); + float halo=exp(-dist*dist*18.0)*exp(-y*7.0)*0.13; + COLOR=vec4(color,(body*end*(0.62+turbulence*0.22)+halo)*power); +} diff --git a/scripts/game/thruster_plume.gdshader.uid b/scripts/game/thruster_plume.gdshader.uid new file mode 100644 index 0000000..4b15369 --- /dev/null +++ b/scripts/game/thruster_plume.gdshader.uid @@ -0,0 +1 @@ +uid://n0w1ie6ibtex diff --git a/scripts/game/weapon_camera_impulse.gd b/scripts/game/weapon_camera_impulse.gd new file mode 100644 index 0000000..a6516d3 --- /dev/null +++ b/scripts/game/weapon_camera_impulse.gd @@ -0,0 +1,27 @@ +extends Node +var _strength := 0.0 +var _age := 0.0 +var _last := Vector2.ZERO +static func kick(ship: Node, strength := 1.2) -> void: + var camera := ship.get_node_or_null("Camera2D") as Camera2D + if camera == null or not camera.enabled: return + var impulse := camera.get_node_or_null("WeaponCameraImpulse") + if impulse == null: + impulse = Node.new() + impulse.name = "WeaponCameraImpulse" + impulse.set_script(load("res://scripts/game/weapon_camera_impulse.gd")) + camera.add_child(impulse) + impulse._strength = minf(2.0,strength) + impulse._age = 0.0 +func _process(delta: float) -> void: + var camera := get_parent() as Camera2D + camera.offset -= _last + _last = Vector2.ZERO + _age += delta + if _age>=0.16 or bool(camera.get_parent().get("_is_destroyed")): + queue_free() + return + _last = Vector2(sin(_age*97),cos(_age*83))*_strength*pow(1.0-_age/0.16,2) + camera.offset += _last +func _exit_tree() -> void: + if is_instance_valid(get_parent()): get_parent().offset -= _last diff --git a/scripts/game/weapon_camera_impulse.gd.uid b/scripts/game/weapon_camera_impulse.gd.uid new file mode 100644 index 0000000..e10ec42 --- /dev/null +++ b/scripts/game/weapon_camera_impulse.gd.uid @@ -0,0 +1 @@ +uid://cbj231fonnxx7 diff --git a/scripts/game/weapon_controller.gd b/scripts/game/weapon_controller.gd index d1e9ce3..9fa21ae 100644 --- a/scripts/game/weapon_controller.gd +++ b/scripts/game/weapon_controller.gd @@ -142,6 +142,7 @@ func _fire_projectile(item: ItemInstance) -> void: var muzzle_position := _muzzle_position() var direction := _aim_direction_from(muzzle_position) var projectile := PROJECTILE_SCENE.instantiate() + projectile.weapon_effect = definition.weapon_effect projectile.call( "configure", muzzle_position, @@ -158,8 +159,11 @@ func _fire_projectile(item: ItemInstance) -> void: projectile.set("shield_piercing", item.shield_piercing()) projectile.set("damage_type_override", item.get_damage_type()) projectile.set("missile_homing", definition.missile_homing) - WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind) - if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: + if definition.weapon_effect == "artillery": + preload("res://scripts/game/artillery_effect.gd").launch(ship,muzzle_position,direction) + else: + WEAPON_AUDIO_SCRIPT.play(ship.get_parent(), muzzle_position, definition.weapon_kind) + if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN and definition.weapon_effect != "artillery": var flash := Node2D.new() flash.set_script(preload("res://scripts/game/muzzle_flash.gd")) # Keep the short flash attached to the muzzle while the ship moves. @@ -176,6 +180,7 @@ func _exit_tree() -> void: func _fire_laser(slot_index: int, item: ItemInstance) -> void: var definition := item.definition var line := _laser_lines[slot_index] + line.set("radiation_beam",item.get_damage_type() == DamageTypes.Type.RADIATION) if _shot_timers[slot_index] > 0.0 or float(ship.get("energy")) < _effective_energy_cost(item.get_energy_cost()): return var muzzle_position := _muzzle_position() @@ -188,6 +193,7 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void: [ship.get_rid()] ) var result := ship.get_world_2d().direct_space_state.intersect_ray(query) + result = preload("res://scripts/game/shield_intersections.gd").intercept(result,muzzle_position,endpoint,RAY_COLLISION_MASK,[ship.get_rid()],get_tree()) if not result.is_empty(): endpoint = result["position"] line.clear_points() @@ -203,8 +209,10 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void: var hit: Dictionary = ship.call("roll_weapon_hit", item) var target: Node = result.get("collider") var hull_before := float(target.get("hull")) if target.has_method("take_research_weapon_hit") else -1.0 + preload("res://scripts/game/combat_vfx.gd").mark_hit(target,endpoint,direction) _apply_damage(target, float(hit["damage"]), bool(hit["critical"]), item.shield_piercing(), item.get_damage_type()) - _spawn_laser_impact(endpoint) + _apply_laser_radiation(target,item,float(hit.damage)) + _spawn_laser_impact(endpoint,item.get_damage_type(),direction) if hull_before >= 0 and float(target.get("hull")) < hull_before and preload("res://scripts/planets/alien_research.gd").learned("hull_a_next_next"): var visited := _last_hit_targets.duplicate() visited[target.get_instance_id()] = true @@ -214,11 +222,14 @@ func _fire_laser(slot_index: int, item: ItemInstance) -> void: if is_instance_valid(prior) and prior.get_rid() not in exclusions: exclusions.append(prior.get_rid()) var through := PhysicsRayQueryParameters2D.create(endpoint+direction*2.0,muzzle_position+direction*definition.weapon_range,RAY_COLLISION_MASK,exclusions) var second := ship.get_world_2d().direct_space_state.intersect_ray(through) + second = preload("res://scripts/game/shield_intersections.gd").intercept(second,endpoint+direction*2.0,muzzle_position+direction*definition.weapon_range,RAY_COLLISION_MASK,exclusions,get_tree()) var second_end: Vector2 = second.get("position",muzzle_position+direction*definition.weapon_range) line.add_point(second_end) if not second.is_empty(): + preload("res://scripts/game/combat_vfx.gd").mark_hit(second.get("collider"),second_end,direction) preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(second.get("collider"),float(hit.damage)*0.5,bool(hit.critical),item.get_damage_type(),item.shield_piercing(),visited,false,-1,0.5) - _spawn_laser_impact(second_end) + _apply_laser_radiation(second.get("collider"),item,float(hit.damage)*0.5) + _spawn_laser_impact(second_end,item.get_damage_type(),direction) _laser_pulse_timers[slot_index] = definition.laser_pulse_duration _shot_timers[slot_index] = item.get_fire_interval() @@ -228,6 +239,10 @@ func _break_cloak_on_fire() -> void: if systems != null: systems.call("break_cloak_on_fire") +func _apply_laser_radiation(target: Node, item: ItemInstance, damage: float) -> void: + if item.definition.weapon_effect == "radiation_laser" and item.get_damage_type() == DamageTypes.Type.RADIATION and not target.is_queued_for_deletion(): + preload("res://scripts/game/radiation_status.gd").apply(target,damage,true,item.shield_piercing()) + func _aim_direction_from(origin: Vector2) -> Vector2: var offset := ship.get_global_mouse_position() - origin return offset.normalized() if offset.length_squared() > 0.001 else Vector2.UP.rotated(ship.rotation) @@ -238,7 +253,7 @@ func _muzzle_position() -> Vector2: func _consume_shot_resource(item: ItemInstance) -> bool: if item.definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: - return inventory.consume_ammunition() + return inventory.consume_ammunition(item.definition.ammunition_cost) if item.definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: return inventory.consume_irradiated_ammunition() return _consume_energy(item.get_energy_cost()) @@ -258,8 +273,9 @@ func _apply_damage(target: Variant, damage: float, critical: bool = false, pierc _last_hit_targets = {} preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(target,damage,critical,damage_type,piercing,_last_hit_targets) -func _spawn_laser_impact(at_position: Vector2) -> void: +func _spawn_laser_impact(at_position: Vector2, kind := DamageTypes.Type.ENERGY, incoming := Vector2.UP) -> void: var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D effect.global_position = at_position ship.get_parent().add_child(effect) effect.call("configure", false, Color(0.38, 0.9, 1.0)) + effect.call("configure_contact",kind,incoming) diff --git a/scripts/game/weapon_projectile.gd b/scripts/game/weapon_projectile.gd index 3ac9355..687f24e 100644 --- a/scripts/game/weapon_projectile.gd +++ b/scripts/game/weapon_projectile.gd @@ -13,6 +13,7 @@ var max_range := 800.0 var weapon_kind := ItemDefinition.WeaponKind.PLASMA var homing_target: Node2D var missile_homing := true +var weapon_effect := "" var homing_turn_acceleration := 5.0 var homing_max_turn_speed := 3.0 var missile_steering_acceleration := 1400.0 @@ -139,6 +140,7 @@ func _physics_process(delta: float) -> void: exclusions ) var result := get_world_2d().direct_space_state.intersect_ray(query) + result = preload("res://scripts/game/shield_intersections.gd").intercept(result,previous_position,next_position,collision_mask,exclusions,get_tree()) if not result.is_empty(): global_position = result["position"] _hit_target(result.get("collider")) @@ -197,6 +199,11 @@ func _find_enemy_in_acquisition_range() -> Node2D: return nearest_enemy func _draw() -> void: + if weapon_effect == "artillery": + draw_line(Vector2(-55,0),Vector2.ZERO,Color(1,0.4,0.08,0.2),10,true) + draw_line(Vector2(-38,0),Vector2(10,0),Color(1,0.8,0.35),4,true) + draw_circle(Vector2(9,0),3,Color(1,0.98,0.85)) + return if weapon_kind == ItemDefinition.WeaponKind.MISSILE: _draw_missile_smoke() match weapon_kind: @@ -272,7 +279,7 @@ func _on_area_entered(area: Area2D) -> void: _hit_target(area) func _hit_target(target: Node) -> void: - if target == source_ship: return + if is_instance_valid(source_ship) and target == source_ship: return if target == _pierced_ship or _already_hit.has(target.get_instance_id()): return if enemy_shot and weapon_kind == ItemDefinition.WeaponKind.MISSILE and target is Node2D and target.name == "PlayerShip": if try_intercept(target): return @@ -285,7 +292,15 @@ func _hit_target(target: Node) -> void: queue_free() return var hull_before := float(target.get("hull")) if target.has_method("take_research_weapon_hit") else -1.0 - if weapon_kind == ItemDefinition.WeaponKind.MISSILE and not _overpenetration_used: + if weapon_effect == "incendiary" and not _overpenetration_used: + _apply_damage_once(target) + var cloud := Node2D.new() + cloud.set_script(preload("res://scripts/game/burning_gas.gd")) + cloud.position = global_position + cloud.damage_per_second = damage*1.1 + cloud.shield_piercing = shield_piercing + get_parent().call_deferred("add_child",cloud) + elif weapon_kind == ItemDefinition.WeaponKind.MISSILE and not _overpenetration_used: _explode_at_target(target) else: _apply_damage_once(target) @@ -301,8 +316,9 @@ func _spawn_overpenetration(target: Node) -> void: shot.damage_type_override = damage_type_override shot.shield_piercing = shield_piercing shot.collision_mask = collision_mask - shot.source_ship = source_ship + shot.source_ship = source_ship if is_instance_valid(source_ship) else null shot.missile_homing = false + shot.weapon_effect = weapon_effect shot._overpenetration_used = true shot._pierced_ship = target shot._already_hit = _already_hit.duplicate() @@ -311,6 +327,9 @@ func _spawn_overpenetration(target: Node) -> void: get_parent().call_deferred("add_child",shot) func _spawn_impact_effect(is_explosion: bool) -> void: + if weapon_effect == "artillery": + preload("res://scripts/game/artillery_effect.gd").hit(get_parent(),global_position,direction) + return var effect := IMPACT_EFFECT_SCENE.instantiate() as Node2D effect.global_position = global_position get_parent().add_child(effect) @@ -319,6 +338,7 @@ func _spawn_impact_effect(is_explosion: bool) -> void: effect_color = Color(0.4, 1.0, 0.15) is_explosion = true effect.call("configure", is_explosion, effect_color) + effect.call("configure_contact",get_damage_type(),direction) func _explode_at_target(impact_target: Node) -> void: if _harmless_intercepted: return @@ -340,12 +360,13 @@ func _apply_damage_once(target: Variant) -> void: if _harmless_intercepted: return if not (target is Node): return - if target == source_ship: return + if is_instance_valid(source_ship) and target == source_ship: return if enemy_shot and target.is_in_group("asteroids"): return var instance_id: int = target.get_instance_id() if _already_hit.has(instance_id): return + preload("res://scripts/game/combat_vfx.gd").mark_hit(target,global_position,direction) if not enemy_shot: if research_disabled_system == -2: research_disabled_system = preload("res://scripts/game/alien_weapon_effects.gd").roll_disabled_system() preload("res://scripts/game/alien_weapon_effects.gd").apply_hit(target,damage,is_critical,get_damage_type(),shield_piercing,_already_hit,not _overpenetration_used and weapon_kind != ItemDefinition.WeaponKind.MISSILE,research_disabled_system,_research_damage_scale) @@ -355,7 +376,9 @@ func _apply_damage_once(target: Variant) -> void: _already_hit[instance_id] = true if enemy_shot and target is CharacterBody2D and target.has_method("take_damage"): if target.has_method("take_enemy_weapon_hit"): - target.call("take_enemy_weapon_hit",damage,is_critical,get_damage_type(),source_ship) + # Projectiles can outlive their shooter; never pass a freed Node. + var shooter: Node2D = source_ship if is_instance_valid(source_ship) else null + target.call("take_enemy_weapon_hit",damage,is_critical,get_damage_type(),shooter) else: target.call("take_damage", damage, is_critical, get_damage_type()) elif target.has_method("take_piercing_weapon_hit"): diff --git a/scripts/items/expedition_pickup.gd b/scripts/items/expedition_pickup.gd index 2d299b2..d04449b 100644 --- a/scripts/items/expedition_pickup.gd +++ b/scripts/items/expedition_pickup.gd @@ -32,7 +32,10 @@ func _physics_process(delta: float) -> void: if payload.has("map"): progress.distant_maps[str(payload.map.id)] = payload.map.duplicate(true) if payload.has("orb"): var id := str(payload.orb) - progress.orbs[id] = int(progress.orbs.get(id,0))+1 + var inventory := _player.get_node("ShipInventory") + if not inventory.add_item(preload("res://scripts/items/special_orbs.gd").create_item(id)): + _label.text = "%s\nŁadownia pełna" % preload("res://scripts/items/special_orbs.gd").NAMES[id] + return progress.emit_changed() set_physics_process(false) queue_free() diff --git a/scripts/items/item_definition.gd b/scripts/items/item_definition.gd index 7fa6805..2ff6e74 100644 --- a/scripts/items/item_definition.gd +++ b/scripts/items/item_definition.gd @@ -5,9 +5,11 @@ enum Category { WEAPON, SHIELD, ARMOR, REACTOR, ENGINE, ACTIVE_EXTENSION, COMPUT enum WeaponKind { PLASMA, MACHINE_GUN, LASER, MISSILE, GAMMA } func get_damage_type() -> int: + if damage_type_override >= 0: return damage_type_override return DamageTypes.for_weapon(weapon_kind) @export var item_id: String = "" +@export var orb_id: String = "" @export var display_name: String = "" @export var icon: Texture2D @export var category: Category = Category.ENGINE @@ -20,6 +22,9 @@ func get_damage_type() -> int: @export var fire_interval: float = 0.25 @export var weapon_cooldown: float = 0.0 @export var missile_homing: bool = true +@export var weapon_effect: String = "" +@export var damage_type_override: int = -1 +@export var ammunition_cost: int = 1 @export var laser_pulse_duration: float = 0.08 @export var projectile_speed: float = 0.0 @export var weapon_range: float = 0.0 diff --git a/scripts/items/item_generator.gd b/scripts/items/item_generator.gd index 86cb954..cc130de 100644 --- a/scripts/items/item_generator.gd +++ b/scripts/items/item_generator.gd @@ -30,11 +30,18 @@ const DEFINITIONS := [ const AFFIX_DATABASE := preload("res://resources/items/affix_database.tres") static func generate(level: int, enemy_tier: int = 0) -> ItemInstance: - return generate_for_definition(DEFINITIONS.pick_random(), level, enemy_tier) + return generate_for_definition(available_definitions().pick_random(), level, enemy_tier) + +static func available_definitions() -> Array: + var pool := DEFINITIONS.duplicate() + var lab := preload("res://scripts/planets/laboratory.gd") + for id in lab.IDS: + if GameState.captain.expedition.laboratory_unlocks.get(id,false): pool.append(lab.definition(id)) + return pool static func generate_loot(level: int, enemy_tier: int = 0, definition: ItemDefinition = null, equipment_luck_percent: float = 0.0) -> ItemInstance: var loot_level := GameState.captain.roll_loot_level(level, -1.0, equipment_luck_percent) - return generate_for_definition(definition if definition != null else DEFINITIONS.pick_random(), loot_level, enemy_tier) + return generate_for_definition(definition if definition != null else available_definitions().pick_random(), loot_level, enemy_tier) static func generate_for_definition(definition: ItemDefinition, level: int, enemy_tier: int = 0) -> ItemInstance: var item := ItemInstance.new() @@ -58,6 +65,7 @@ static func generate_for_definition(definition: ItemDefinition, level: int, enem static func roll_base_values(item: ItemInstance, level: int) -> void: var definition := item.definition + if definition.weapon_effect == "artillery": item.artillery_balance_revision = 1 item.item_level = clampi(level, 1, 1000) var scale_factor := preload("res://scripts/levels/level_balance.gd").item_stat_scale(item.item_level) item.primary_value_override = definition.primary_value * scale_factor * randf_range(0.8, 1.2) diff --git a/scripts/items/item_instance.gd b/scripts/items/item_instance.gd index 3979b6d..c599c5c 100644 --- a/scripts/items/item_instance.gd +++ b/scripts/items/item_instance.gd @@ -8,6 +8,8 @@ const EXTENSION_COOLDOWNS := preload("res://scripts/items/extension_cooldowns.gd var _instance_ability: ActiveAbility @export var passive_value: float = -1.0 @export var special_orbs: Array[String] = [] +@export var stack_count: int = 1 +@export var artillery_balance_revision: int = 0 func shield_piercing() -> float: return 0.5 if definition != null and definition.category == ItemDefinition.Category.WEAPON and "piercing" in special_orbs else 0.0 @@ -106,6 +108,9 @@ func get_damage_range() -> Vector2: # Starter weapons use the middle of the definition's roll settings. lower = roundf((definition.damage_lower_roll_min + definition.damage_lower_roll_max) * 0.5) upper = lower + roundf((definition.damage_spread_roll_min + definition.damage_spread_roll_max) * 0.5) + if definition.weapon_effect == "artillery" and artillery_balance_revision == 0 and (damage_min >= 0.0 or primary_value_override >= 0.0): + lower *= 50.0/74.0 + upper *= 50.0/74.0 var flat_bonus := 0.0 for modifier in additional_modifiers: if modifier and modifier.stat_key == "damage": @@ -171,6 +176,8 @@ func get_tooltip_text() -> String: if definition == null: return "" if definition.category == ItemDefinition.Category.CONSUMABLE: + if not definition.orb_id.is_empty(): + return "%s\nStos: %d\n\n%s\n\nDwuklik: wybierz orb. Następnie kliknij odpowiedni moduł lub broń. PPM lub Esc anuluje." % [definition.display_name,stack_count,definition.description] return "%s\n\n%s\nR • Zużywa jedną sztukę z zasobów." % [definition.display_name, definition.description] var primary_text := "%s: +%.1f%s" % [stat_name(definition.primary_stat), get_stat_total(definition.primary_stat), definition.stat_suffix] if definition.category == ItemDefinition.Category.WEAPON: @@ -186,7 +193,7 @@ func get_tooltip_text() -> String: lines.append("Typ obrażeń: %s" % DamageTypes.NAMES[get_damage_type()]) lines.append("Każdy strzał / impuls losuje obrażenia z tego zakresu.") if definition.weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN: - lines.append("Zasięg: %.0f px • Amunicja: 1 / strzał" % definition.weapon_range) + lines.append("Zasięg: %.0f px • Amunicja: %d / strzał" % [definition.weapon_range,definition.ammunition_cost]) elif definition.weapon_kind == ItemDefinition.WeaponKind.GAMMA: lines.append("Zasięg: %.0f px • Napromieniowana amunicja: 1 / strzał" % definition.weapon_range) elif definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: diff --git a/scripts/items/loot_pickup.gd b/scripts/items/loot_pickup.gd index ab7788d..1f3d949 100644 --- a/scripts/items/loot_pickup.gd +++ b/scripts/items/loot_pickup.gd @@ -63,7 +63,7 @@ func _physics_process(delta: float) -> void: if distance > ATTRACTION_RADIUS: return var inventory := _player.get_node("ShipInventory") as ShipInventory - if item.definition.item_id != "energy_bank" and inventory.get_cargo_used() >= inventory.get_cargo_capacity(): + if not inventory.can_add_item(item): _label.text = "%s\nMagazyn pełny" % item.definition.display_name return _label.text = item.definition.display_name diff --git a/scripts/items/ship_inventory.gd b/scripts/items/ship_inventory.gd index 75728a3..2e0e23e 100644 --- a/scripts/items/ship_inventory.gd +++ b/scripts/items/ship_inventory.gd @@ -35,6 +35,7 @@ var ammunition: int = STARTING_AMMUNITION var missile_ammunition: int = STARTING_MISSILE_AMMUNITION var cargo: Array[ItemInstance] = [] +var selected_orb: ItemInstance var weapon_slots: Array[ItemInstance] = [] var module_slots: Array[ItemInstance] = [] var extension_slots: Array[ItemInstance] = [] @@ -42,6 +43,7 @@ var extension_slots: Array[ItemInstance] = [] @onready var ship: CharacterBody2D = get_parent() func _ready() -> void: + process_mode = Node.PROCESS_MODE_ALWAYS add_to_group("ship_inventory") cargo.resize(CARGO_SIZE) weapon_slots.resize(WEAPON_SLOT_COUNT) @@ -64,6 +66,7 @@ func _ready() -> void: GameState.restart_inventory.clear() _convert_legacy_energy_banks() _remove_extension_addons() + migrate_orbs() _recalculate_ship_stats() inventory_changed.emit() @@ -111,11 +114,11 @@ func use_energy_bank() -> bool: inventory_changed.emit() return true -func consume_ammunition() -> bool: - if ammunition <= 0: +func consume_ammunition(amount: int = 1) -> bool: + if amount <= 0 or ammunition < amount: return false if GameState.captain.should_consume_ammunition("ordinary"): - ammunition -= 1 + ammunition -= amount ammunition_changed.emit() return true @@ -185,6 +188,12 @@ func buy_ammunition() -> bool: func add_item(item: ItemInstance) -> bool: if item == null or item.definition == null: return false + if not item.definition.orb_id.is_empty(): + for existing in cargo.slice(0,get_cargo_capacity()): + if existing != null and existing.definition.orb_id == item.definition.orb_id: + existing.stack_count += item.stack_count + inventory_changed.emit() + return true if item.definition.item_id == "energy_bank": energy_banks += 1 inventory_changed.emit() @@ -199,10 +208,58 @@ func add_item(item: ItemInstance) -> bool: return true return false +func can_add_item(item: ItemInstance) -> bool: + if item == null or item.definition == null: return false + if item.definition.item_id == "energy_bank": return true + for existing in cargo.slice(0,get_cargo_capacity()): + if existing == null: return true + if not item.definition.orb_id.is_empty() and existing.definition.orb_id == item.definition.orb_id: return true + return false + +func migrate_orbs() -> void: + var progress: Resource = GameState.captain.expedition + for id in progress.orbs.keys(): + var amount := int(progress.orbs[id]) + if amount > 0 and add_item(preload("res://scripts/items/special_orbs.gd").create_item(id,amount)): + progress.orbs.erase(id) + +func select_orb(index: int) -> bool: + var item := get_item("cargo",index) + if item == null or item.definition.orb_id.is_empty(): return false + selected_orb = item + Input.set_default_cursor_shape(Input.CURSOR_CROSS) + if get_node_or_null("OrbTargetCursor") == null: + var cursor := CanvasLayer.new() + cursor.set_script(preload("res://scripts/ui/orb_target_cursor.gd")) + cursor.inventory = self + add_child(cursor) + inventory_changed.emit() + return true + +func cancel_orb() -> void: + selected_orb = null + Input.set_default_cursor_shape(Input.CURSOR_ARROW) + inventory_changed.emit() + +func use_selected_orb(item: ItemInstance) -> bool: + if selected_orb == null or not cargo.has(selected_orb): + cancel_orb() + return false + if not preload("res://scripts/items/special_orbs.gd").apply(GameState.captain.expedition,self,item,selected_orb.definition.orb_id): return false + cancel_orb() + return true + +func _input(event: InputEvent) -> void: + if selected_orb == null: return + if (event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE) or (event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT): + cancel_orb() + get_viewport().set_input_as_handled() + func delete_cargo_item(index: int) -> bool: if not is_slot_unlocked("cargo",index): return false if index < 0 or index >= cargo.size() or cargo[index] == null: return false + if not cargo[index].definition.orb_id.is_empty(): return false cargo[index] = null GameState.captain.parts += 10.0 GameState.captain.emit_changed() @@ -268,7 +325,14 @@ func store_on_freighter(index: int, expected: ItemInstance, captain: Resource) - if not can_use_freighter_storage(captain) or not is_slot_unlocked("cargo",index): return false var item := get_item("cargo",index) if item == null or item != expected: return false - captain.expedition.freighter_storage.append(item) + var merged := false + if not item.definition.orb_id.is_empty(): + for existing in captain.expedition.freighter_storage: + if existing.definition.orb_id == item.definition.orb_id: + existing.stack_count += item.stack_count + merged = true + break + if not merged: captain.expedition.freighter_storage.append(item) cargo[index] = null captain.expedition.emit_changed() inventory_changed.emit() @@ -278,18 +342,16 @@ func retrieve_from_freighter(index: int, expected: ItemInstance, captain: Resour if not can_use_freighter_storage(captain): return false var storage: Array = captain.expedition.freighter_storage if index < 0 or index >= storage.size() or expected == null or storage[index] != expected: return false - var destination := -1 - for cell in get_cargo_capacity(): - if cargo[cell] == null: - destination = cell - break - if destination < 0: return false - cargo[destination] = expected + if not can_add_item(expected): return false + if not add_item(expected): return false storage.remove_at(index) captain.expedition.emit_changed() inventory_changed.emit() return true +func _exit_tree() -> void: + if selected_orb != null: Input.set_default_cursor_shape(Input.CURSOR_ARROW) + func get_slot_unlock_cost(kind: String) -> Dictionary: match kind: "cargo": @@ -374,6 +436,11 @@ func move_item(from_kind: String, from_index: int, to_kind: String, to_index: in var source := _get_slots(from_kind) var destination := _get_slots(to_kind) var displaced := destination[to_index] + if displaced != null and not source[from_index].definition.orb_id.is_empty() and displaced.definition.orb_id == source[from_index].definition.orb_id: + displaced.stack_count += source[from_index].stack_count + source[from_index] = null + inventory_changed.emit() + return true destination[to_index] = source[from_index] source[from_index] = displaced _remove_extension_addons() @@ -512,7 +579,11 @@ func _weapon_dps(item: ItemInstance, bonuses: Dictionary) -> float: var interval := item.get_fire_interval() if item.definition.weapon_kind == ItemDefinition.WeaponKind.MISSILE: interval = maxf(1.0, maxf(1.0, 5.0 - float(bonuses.get("missile_cooldown_reduction", 0.0))) / item.attack_speed_multiplier()) - return item.get_stat_total("damage") * damage_factor * (1.0 + chance * (critical_factor - 1.0)) / maxf(0.001, interval) + var value := item.get_stat_total("damage") * damage_factor * (1.0 + chance * (critical_factor - 1.0)) / maxf(0.001, interval) + if item.definition.weapon_effect == "radiation_laser" and item.get_damage_type() == DamageTypes.Type.RADIATION: + value *= 1.0+minf(preload("res://scripts/game/radiation_status.gd").DURATION,interval) + if item.definition.weapon_effect == "incendiary": value *= 6.5 + return value func consume_irradiated_ammunition() -> bool: if irradiated_ammunition <= 0: diff --git a/scripts/items/special_orbs.gd b/scripts/items/special_orbs.gd index 5f8498d..bad21c3 100644 --- a/scripts/items/special_orbs.gd +++ b/scripts/items/special_orbs.gd @@ -7,6 +7,26 @@ const NAMES := {"piercing":"Orb przebicia", "booster":"Orb dopalacza", "advancem const DESCRIPTIONS := {"replication":"Tworzy dokładną, niezależną kopię wyposażenia w magazynie frachtowca. Oryginał zostaje. Kopiuje poziom, addony i efekty orbów. Drop: dodatkowe 5% z bossa dalekiej wyprawy.", "electricity":"Trwale zmienia typ obrażeń broni na energetyczny (jak laser). Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "radiation":"Trwale zmienia typ obrażeń broni na radiacyjny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "kinetic":"Trwale zmienia typ obrażeń broni na kinetyczny. Nie zwiększa obrażeń ani kosztu strzału. Zastępuje poprzedni orb typu obrażeń.", "atomic_engine":"Tylko silnik. Trwale zmniejsza zużycie energii dopalacza TAB o 50%. Nie sumuje się. Orb darmowego dopalacza ma pierwszeństwo.", "polarization":"Tylko tarcze. Trafienie w tarcze odnawia do 5% maksymalnych tarcz jako energię, nie więcej niż faktycznie stracone tarcze. Tempo odzysku: do 5% pojemności generatora/s, z rezerwą na jedno takie odnowienie. Uniki i trafienia w sam kadłub nie aktywują efektu.", "piercing":"50% obrażeń broni omija tarcze i trafia bezpośrednio w kadłub. Tylko broń.", "booster":"Dopalacz pod TAB nie zużywa energii i działa również przy pustym generatorze. Tylko silnik.", "advancement":"Awansuje przedmiot o 1 poziom i ponownie losuje jego parametry oraz wartości obecnych addonów. Zachowuje ich typy, liczbę i efekty orbów. Wyniki losowania mogą być niższe. Jedna sztuka daje jeden awans. Kolejny awans wymaga nowego orba (główna progresja do 120; 1000 to limit techniczny). Nie działa na materiały zużywalne."} static func icon(id: String) -> Texture2D: return load("res://assets/icons/orbs/%s.%s" % [id, "png" if id in ["piercing", "booster", "advancement"] else "svg"]) as Texture2D +static func create_item(id: String, amount := 1) -> ItemInstance: + if id not in IDS or amount <= 0: return null + var definition := ItemDefinition.new() + definition.item_id = "orb_"+id + definition.orb_id = id + definition.display_name = NAMES[id] + definition.description = DESCRIPTIONS[id] + definition.icon = icon(id) + definition.category = ItemDefinition.Category.CONSUMABLE + var item := ItemInstance.new() + item.definition = definition + item.stack_count = amount + return item + +static func count(progress: Resource, inventory: Node, id: String) -> int: + var total := int(progress.orbs.get(id,0)) + if inventory != null: + for item in inventory.cargo.slice(0,inventory.unlocked_cargo_slots): + if item != null and item.definition.orb_id == id: total += item.stack_count + return total static func accepts(item: ItemInstance, id: String) -> bool: if id not in IDS or item == null or item.definition == null: return false if id == "advancement": return preload("res://scripts/items/item_generator.gd").can_advance(item) @@ -17,8 +37,7 @@ static func accepts(item: ItemInstance, id: String) -> bool: if id == "polarization": return item.definition.category == ItemDefinition.Category.SHIELD return item.definition.category == (ItemDefinition.Category.WEAPON if id == "piercing" else ItemDefinition.Category.ENGINE) static func apply(progress: Resource, inventory: Node, item: ItemInstance, id: String) -> bool: - if not accepts(item,id) or int(progress.orbs.get(id,0)) <= 0: return false - if inventory == null or not bool(inventory.get_parent().get("is_docked")): return false + if inventory == null or not accepts(item,id) or count(progress,inventory,id) <= 0: return false var owned: Array = inventory.cargo.slice(0,inventory.unlocked_cargo_slots) + inventory.weapon_slots.slice(0,inventory.unlocked_weapon_slots) + inventory.module_slots + inventory.extension_slots.slice(0,inventory.unlocked_extension_slots) + progress.freighter_storage if not owned.has(item): return false if id == "advancement": @@ -34,7 +53,15 @@ static func apply(progress: Resource, inventory: Node, item: ItemInstance, id: S if id in DAMAGE_ORBS: for previous in DAMAGE_ORBS: item.special_orbs.erase(previous) item.special_orbs.append(id) - progress.orbs[id] = int(progress.orbs[id])-1 + var consumed := false + for index in inventory.get_cargo_capacity(): + var orb: ItemInstance = inventory.cargo[index] + if orb == null or orb.definition.orb_id != id: continue + orb.stack_count -= 1 + if orb.stack_count <= 0: inventory.cargo[index] = null + consumed = true + break + if not consumed: progress.orbs[id] = int(progress.orbs[id])-1 item.emit_changed() inventory.call("_recalculate_ship_stats") inventory.inventory_changed.emit() diff --git a/scripts/planets/expedition_controller.gd b/scripts/planets/expedition_controller.gd index c562eff..1b39680 100644 --- a/scripts/planets/expedition_controller.gd +++ b/scripts/planets/expedition_controller.gd @@ -31,6 +31,7 @@ func _ready() -> void: get_tree().auto_accept_quit = false freighter = Node2D.new() freighter.name = "Freighter" + freighter.add_to_group("scanner_freighters") freighter.z_index = -3 # Keep the player's hull, engines and shield above the frigate. freighter.position = Vector2(480,-270) game.add_child(freighter) diff --git a/scripts/planets/freighter_screen.gd b/scripts/planets/freighter_screen.gd index bb8894c..e429f99 100644 --- a/scripts/planets/freighter_screen.gd +++ b/scripts/planets/freighter_screen.gd @@ -120,7 +120,7 @@ func _layout() -> void: var research: bool = _tab == "expansion_2" and deck.is_built(_tab) var width := minf(490 if research else (700 if storage else (440 if _tab == "hangar" else 490)),viewport_size.x-32) var construction: bool = deck.is_buildable(_tab) and not deck.is_built(_tab) - var target_height := 390 if construction else 430 if research else 420 if _tab == "hangar" else (520 if _tab == "workshop" else (600 if storage else (540 if _tab == "command" else (340 if _tab.begins_with("cabin_") else 260)))) + var target_height := 390 if construction else 430 if research else 560 if _tab == "expansion_1" else 420 if _tab == "hangar" else (520 if _tab == "workshop" else (600 if storage else (540 if _tab == "command" else (340 if _tab.begins_with("cabin_") else 260)))) var height := minf(target_height,maxf(240,viewport_size.y-140)) _drawer.position = Vector2(16 if _tab == "hangar" else viewport_size.x-width-16,maxf(90,(viewport_size.y-height)*0.5)) _drawer.size = Vector2(width,height) @@ -206,6 +206,12 @@ func _build_room() -> void: _label("Wybierz pomieszczenie na pokładzie.") return var plan: Dictionary = deck.plan(_tab) + if str(plan.kind) == "laboratory" and deck.is_built(_tab): + var laboratory := VBoxContainer.new() + laboratory.set_script(preload("res://scripts/planets/laboratory_panel.gd")) + laboratory.captain = captain + _content.add_child(laboratory) + return if str(plan.kind) == "alien_research" and deck.is_built(_tab): _build_alien_research() return diff --git a/scripts/planets/freighter_storage_slot.gd b/scripts/planets/freighter_storage_slot.gd index 3380490..3e84c81 100644 --- a/scripts/planets/freighter_storage_slot.gd +++ b/scripts/planets/freighter_storage_slot.gd @@ -42,8 +42,15 @@ func _ready() -> void: add_child(arrow) set_meta("transfer_action","← ODBIERZ" if retrieve else "ODŁÓŻ →") tooltip_text = item.get_tooltip_text()+"\nKliknij: "+("odbierz do statku" if retrieve else "przenieś na fregatę") - pressed.connect(transfer) + pressed.connect(_activate) else: disabled = true text = "×" if locked else "·" tooltip_text = "Odblokuj 1 slot ładowni w hangarze: 100 żelaza + 50 aluminium." if locked else "Wolny slot" + +func _activate() -> void: + var inventory := get_tree().get_first_node_in_group("ship_inventory") + if inventory != null and inventory.selected_orb != null: + inventory.use_selected_orb(item) + else: + transfer.call() diff --git a/scripts/planets/laboratory.gd b/scripts/planets/laboratory.gd new file mode 100644 index 0000000..9f87aca --- /dev/null +++ b/scripts/planets/laboratory.gd @@ -0,0 +1,47 @@ +extends RefCounted +const DURATION := 60.0 +const IDS := ["incendiary_missile", "radiation_laser", "artillery_cannon"] +const COSTS := { + "incendiary_missile":{"iron":45,"sulfur":60,"uranium":30,"copper":12,"mercury":6}, + "radiation_laser":{"uranium":60,"aluminium":45,"silicon":18,"copper":12}, + "artillery_cannon":{"iron":75,"titanium":45,"nickel":15,"tungsten":10}, +} + +static func definition(id: String) -> ItemDefinition: + return load("res://resources/items/%s.tres" % id) as ItemDefinition if id in IDS else null + +static func ensure_offers(progress: Resource) -> void: + if not progress.laboratory_offers.is_empty() or not progress.laboratory_project.is_empty(): return + var pool := IDS.duplicate() + for id in progress.laboratory_unlocks: pool.erase(id) + pool.shuffle() + for id in pool.slice(0,3): progress.laboratory_offers.append(id) + +static func can_start(captain: Resource, id: String) -> bool: + var progress: Resource = captain.expedition + if progress.freighter_rooms.get("expansion_1","") != "laboratory" or not progress.laboratory_project.is_empty(): return false + if id not in progress.laboratory_offers or id not in IDS or progress.laboratory_unlocks.get(id,false): return false + for key in COSTS[id]: + if int(captain.minerals.get(key,0)) < int(COSTS[id][key]): return false + return true + +static func start(captain: Resource, id: String) -> bool: + if not can_start(captain,id): return false + for key in COSTS[id]: captain.minerals[key] = int(captain.minerals.get(key,0))-int(COSTS[id][key]) + captain.expedition.laboratory_project = id + captain.expedition.laboratory_remaining = DURATION + captain.emit_changed() + captain.expedition.emit_changed() + return true + +static func advance(progress: Resource, delta: float) -> bool: + if progress.laboratory_project.is_empty() or delta <= 0.0: return false + progress.laboratory_remaining = maxf(0.0,progress.laboratory_remaining-delta) + if progress.laboratory_remaining <= 0.00001: + progress.laboratory_remaining = 0.0 + progress.laboratory_unlocks[progress.laboratory_project] = true + progress.laboratory_project = "" + progress.laboratory_offers.clear() + ensure_offers(progress) + progress.emit_changed() + return true diff --git a/scripts/planets/laboratory.gd.uid b/scripts/planets/laboratory.gd.uid new file mode 100644 index 0000000..7c0262e --- /dev/null +++ b/scripts/planets/laboratory.gd.uid @@ -0,0 +1 @@ +uid://b8reao0fpb3y diff --git a/scripts/planets/laboratory_panel.gd b/scripts/planets/laboratory_panel.gd new file mode 100644 index 0000000..1291931 --- /dev/null +++ b/scripts/planets/laboratory_panel.gd @@ -0,0 +1,73 @@ +extends VBoxContainer +const LAB := preload("res://scripts/planets/laboratory.gd") +const MENU := preload("res://scripts/planets/freighter_menu.gd") +var captain: Resource +var _signature := "" +var _timer: Label +var _bar: ProgressBar +var _buttons: Dictionary = {} + +func _ready() -> void: + LAB.ensure_offers(captain.expedition) + _build() + +func _state() -> String: + return str(captain.expedition.laboratory_project)+str(captain.expedition.laboratory_unlocks) + +func _process(_delta: float) -> void: + if _signature != _state(): _build() + if is_instance_valid(_timer): + _timer.text = "Pozostało: %d s gry" % ceili(captain.expedition.laboratory_remaining) + _bar.value = LAB.DURATION-captain.expedition.laboratory_remaining + for id in _buttons: + if is_instance_valid(_buttons[id]): _buttons[id].disabled = not LAB.can_start(captain,id) + +func _build() -> void: + for child in get_children(): + remove_child(child) + child.queue_free() + _timer = null + _buttons.clear() + _signature = _state() + var progress: Resource = captain.expedition + MENU.label(self,"PROJEKTY NOWEGO UZBROJENIA",15,MENU.GOLD) + MENU.label(self,"Wybierz projekt. Badanie trwa 60 sekund gry. Wynalazek odblokowuje nową broń w łupach wrogów.",12,MENU.MUTED) + if not progress.laboratory_project.is_empty(): + var item := LAB.definition(progress.laboratory_project) + var row := MENU.row(self) + MENU.icon(row,item.icon,64) + MENU.label(row,item.display_name,16,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL + _timer = MENU.label(self,"",14) + _bar = ProgressBar.new() + _bar.max_value = LAB.DURATION + _bar.custom_minimum_size.y = 18 + add_child(_bar) + else: + for id in progress.laboratory_offers: + var item := LAB.definition(id) + var card := MENU.card(self) + var row := MENU.row(card) + MENU.icon(row,item.icon,64) + MENU.label(row,item.display_name,15,MENU.GOLD).size_flags_horizontal = Control.SIZE_EXPAND_FILL + MENU.label(card,item.description,12) + var names := preload("res://scripts/items/crafting_recipes.gd").NAMES.duplicate() + names.merge(preload("res://scripts/planets/planet_profiles.gd").MATERIAL_NAMES) + var costs: Array[String] = [] + for key in LAB.COSTS[id]: costs.append("%s: %d / %d" % [names[key],int(captain.minerals.get(key,0)),int(LAB.COSTS[id][key])]) + MENU.label(card,"KOSZT · posiadasz / wymagane\n"+" · ".join(costs),12,MENU.MUTED) + var button := MENU.button(card,"ROZPOCZNIJ BADANIE · 60 s",_start.bind(id),true) + button.name = "Research_"+id + button.disabled = not LAB.can_start(captain,id) + _buttons[id] = button + if progress.laboratory_offers.is_empty(): MENU.label(self,"Wszystkie dostępne projekty zostały wynalezione.",14,MENU.GOLD) + if not progress.laboratory_unlocks.is_empty(): + MENU.label(self,"ODKRYTE · DOSTĘPNE W ŁUPACH",13,MENU.GOLD) + for id in LAB.IDS: + if not progress.laboratory_unlocks.get(id,false): continue + var item := LAB.definition(id) + var row := MENU.row(self) + MENU.icon(row,item.icon,32) + MENU.label(row,item.display_name,13).size_flags_horizontal = Control.SIZE_EXPAND_FILL + +func _start(id: String) -> void: + if LAB.start(captain,id): _build() diff --git a/scripts/planets/laboratory_panel.gd.uid b/scripts/planets/laboratory_panel.gd.uid new file mode 100644 index 0000000..6271223 --- /dev/null +++ b/scripts/planets/laboratory_panel.gd.uid @@ -0,0 +1 @@ +uid://cuot0ai4quqpi diff --git a/scripts/planets/orb_workshop.gd b/scripts/planets/orb_workshop.gd index 62f306d..623848a 100644 --- a/scripts/planets/orb_workshop.gd +++ b/scripts/planets/orb_workshop.gd @@ -9,7 +9,7 @@ static func build(screen: Control) -> void: var card := MENU.card(screen._content) var header := MENU.row(card) MENU.icon(header,ORBS.icon(id),48) - var title := MENU.label(header,"%s · %d szt." % [ORBS.NAMES[id],int(progress.orbs.get(id,0))],14,MENU.GOLD) + var title := MENU.label(header,"%s · %d szt." % [ORBS.NAMES[id],ORBS.count(progress,screen.inventory,id)],14,MENU.GOLD) title.size_flags_horizontal = Control.SIZE_EXPAND_FILL title.custom_minimum_size.x = 180 MENU.label(card,ORBS.DESCRIPTIONS[id],12) @@ -34,7 +34,7 @@ static func build(screen: Control) -> void: screen._refresh() ,true) apply.name = "ApplyOrb_"+id - apply.disabled = int(progress.orbs.get(id,0)) <= 0 or choices.get_selected_metadata() == null + apply.disabled = ORBS.count(progress,screen.inventory,id) <= 0 or choices.get_selected_metadata() == null static func _add_item(choices: OptionButton, item: ItemInstance, orb: String, location: String) -> void: if not ORBS.accepts(item,orb): return choices.add_item("%s · poz. %d · %s" % [item.definition.display_name,item.item_level,location]) diff --git a/scripts/planets/planet_progress.gd b/scripts/planets/planet_progress.gd index aa42b51..30710f9 100644 --- a/scripts/planets/planet_progress.gd +++ b/scripts/planets/planet_progress.gd @@ -8,6 +8,10 @@ const PROFILES := preload("res://scripts/planets/planet_profiles.gd") @export var delivered_modules: Array[Resource] = [] @export var research_records: Dictionary = {} @export var alien_skills: Dictionary = {} +@export var laboratory_unlocks: Dictionary = {} +@export var laboratory_offers: Array[String] = [] +@export var laboratory_project: String = "" +@export var laboratory_remaining: float = 0.0 @export var freighter_rooms: Dictionary = {"expansion_3":"module_storage"} @export var mining_stations: Dictionary = {} @export var distant_maps: Dictionary = {} diff --git a/scripts/ui/equipment_slot_view.gd b/scripts/ui/equipment_slot_view.gd index f4bbe64..c7b4993 100644 --- a/scripts/ui/equipment_slot_view.gd +++ b/scripts/ui/equipment_slot_view.gd @@ -84,6 +84,10 @@ func _update_salvage_visibility() -> void: return var hovered_control := get_viewport().gui_get_hovered_control() var hovered := hovered_control == self or (hovered_control != null and is_ancestor_of(hovered_control)) + var current := _inventory.get_item(slot_kind,slot_index) + if current != null and not current.definition.orb_id.is_empty(): + _delete_button.hide() + return _delete_button.visible = is_visible_in_tree() and hovered and _inventory.get_item(slot_kind, slot_index) != null and not get_viewport().gui_is_dragging() func _draw() -> void: @@ -100,6 +104,28 @@ func _refresh() -> void: if not is_node_ready() or not _inventory: return var item: ItemInstance = _inventory.get_item(slot_kind, slot_index) + var counter := get_node_or_null("StackOverlay/Count") as Label + if counter == null: + var overlay := Control.new() + overlay.name = "StackOverlay" + overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(overlay) + counter = Label.new() + counter.name = "Count" + counter.mouse_filter = Control.MOUSE_FILTER_IGNORE + counter.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT) + counter.offset_left = -42 + counter.offset_top = -21 + counter.offset_right = -4 + counter.offset_bottom = -2 + counter.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + counter.add_theme_font_size_override("font_size",13) + counter.add_theme_color_override("font_color",Color("ffe3a5")) + counter.add_theme_color_override("font_shadow_color",Color.BLACK) + counter.add_theme_constant_override("shadow_offset_x",1) + counter.add_theme_constant_override("shadow_offset_y",1) + overlay.add_child(counter) + counter.text = "×%d" % item.stack_count if item != null and not item.definition.orb_id.is_empty() else "" _apply_slot_style(item) if not _inventory.is_slot_unlocked(slot_kind,slot_index): _icon.texture = null @@ -145,8 +171,22 @@ func _apply_slot_style(item: ItemInstance) -> void: style.bg_color = Color(0.08, 0.3, 0.25, 0.85) style.shadow_color = Color(0.2, 1.0, 0.72, 0.3) style.shadow_size = 5 + if _inventory != null and _inventory.selected_orb != null: + if item == _inventory.selected_orb or preload("res://scripts/items/special_orbs.gd").accepts(item,_inventory.selected_orb.definition.orb_id): + style.border_color = Color("ffe3a5") if item == _inventory.selected_orb else Color("73efb5") + style.set_border_width_all(3) add_theme_stylebox_override("panel", style) +func _gui_input(event: InputEvent) -> void: + if _inventory == null or not event is InputEventMouseButton or not event.pressed or event.button_index != MOUSE_BUTTON_LEFT: return + var item := _inventory.get_item(slot_kind,slot_index) + if event.double_click and slot_kind == "cargo" and item != null and not item.definition.orb_id.is_empty(): + _inventory.select_orb(slot_index) + accept_event() + elif _inventory.selected_orb != null: + _inventory.use_selected_orb(item) + accept_event() + func _delete_item() -> void: if _inventory: _inventory.delete_cargo_item(slot_index) diff --git a/scripts/ui/orb_target_cursor.gd b/scripts/ui/orb_target_cursor.gd new file mode 100644 index 0000000..9716db1 --- /dev/null +++ b/scripts/ui/orb_target_cursor.gd @@ -0,0 +1,30 @@ +extends CanvasLayer +var inventory: Node +var _icon: TextureRect +var _label: Label +func _ready() -> void: + name = "OrbTargetCursor" + layer = 110 + _icon = TextureRect.new() + _icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + _icon.size = Vector2(26,26) + _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(_icon) + _label = Label.new() + _label.mouse_filter = Control.MOUSE_FILTER_IGNORE + _label.add_theme_font_size_override("font_size",12) + _label.add_theme_color_override("font_color",Color("ffdc99")) + _label.add_theme_color_override("font_shadow_color",Color.BLACK) + _label.add_theme_constant_override("shadow_offset_x",1) + _label.add_theme_constant_override("shadow_offset_y",1) + add_child(_label) +func _process(_delta: float) -> void: + if inventory.selected_orb == null or not inventory.cargo.has(inventory.selected_orb): + if inventory.selected_orb != null: inventory.cancel_orb() + queue_free() + return + _icon.texture = inventory.selected_orb.definition.icon + var point := get_viewport().get_mouse_position()+Vector2(18,18) + _icon.position = point + _label.position = point+Vector2(31,0) + _label.text = "Wybierz moduł lub broń\nPPM / Esc — anuluj" diff --git a/scripts/ui/orb_target_cursor.gd.uid b/scripts/ui/orb_target_cursor.gd.uid new file mode 100644 index 0000000..de193d3 --- /dev/null +++ b/scripts/ui/orb_target_cursor.gd.uid @@ -0,0 +1 @@ +uid://dmex3lcws6qju diff --git a/scripts/ui/scanner_overlay.gd b/scripts/ui/scanner_overlay.gd index 70534d6..3f5ceed 100644 --- a/scripts/ui/scanner_overlay.gd +++ b/scripts/ui/scanner_overlay.gd @@ -28,7 +28,7 @@ func _physics_process(_delta: float) -> void: _previous_player_position = _player_position targets.clear() var next_positions: Dictionary = {} - for group in ["enemies", "exploration_pois", "scanner_planets"]: + for group in ["enemies", "exploration_pois", "scanner_planets", "scanner_freighters"]: for target in get_tree().get_nodes_in_group(group): if player.get_parent().is_ancestor_of(target) and not target.is_queued_for_deletion() and is_target_in_range(target): if group == "scanner_planets" and (not bool(target.get("is_protected")) or not bool(player.get_parent().get("system_cleared"))): @@ -48,6 +48,7 @@ func get_scan_range() -> float: return maxf(float(player.get("base_scanner_range")), visible_radius + 150.0) + float(player.get("scanner_range")) - float(player.get("base_scanner_range")) func is_target_in_range(target: Node2D) -> bool: + if target.is_in_group("scanner_freighters"): return true return player.global_position.distance_to(target.global_position) <= get_scan_range() func _draw() -> void: @@ -64,7 +65,9 @@ func _draw() -> void: if not is_instance_valid(target) or target.is_queued_for_deletion(): continue var color := Color("ff5555") - if target.is_in_group("enemies"): + if target.is_in_group("scanner_freighters"): + color = Color.WHITE + elif target.is_in_group("enemies"): if float(target.get("hull")) <= 0: continue elif target.is_in_group("scanner_planets"): @@ -87,5 +90,8 @@ func _draw() -> void: if offset.length_squared() < 0.01: continue var point := origin + offset.normalized() * radius + if target.is_in_group("scanner_freighters"): + draw_circle(point,2.5,Color.WHITE,true,-1.0,true) + continue draw_circle(point, DOT_RADIUS + 3.0 + pulse * 2.0, Color(color, 0.08 + pulse * 0.12), true, -1.0, true) draw_circle(point, DOT_RADIUS, Color(color, 0.55 + pulse * 0.45), true, -1.0, true) diff --git a/tests/abilities/mini_boost_test.gd b/tests/abilities/mini_boost_test.gd index 25a3ade..1ea0b2c 100644 --- a/tests/abilities/mini_boost_test.gd +++ b/tests/abilities/mini_boost_test.gd @@ -10,13 +10,15 @@ func _ready() -> void: ship.get_node("WeaponController").set_physics_process(false) ship.set("energy_regeneration", 0.0) ship.set("energy", 100.0) + ship.velocity = Vector2.UP*ship.max_speed + var boost_cost: float = ship.get_mini_boost_energy_cost() var tab := InputEventKey.new() tab.physical_keycode = KEY_TAB assert(InputMap.action_has_event("ship_mini_boost", tab)) Input.action_press("ship_mini_boost") ship.call("_physics_process", 1.0) assert(ship.call("is_mini_boosting")) - assert(is_equal_approx(ship.get("energy"), 80.0)) + assert(is_equal_approx(ship.get("energy"), 100.0-boost_cost)) assert(is_equal_approx(ship.velocity.length(), ship.get("max_speed") * 2.0)) var engine := ship.get_node("EngineVisual") engine.call("_process", 0.2) @@ -41,6 +43,7 @@ func _ready() -> void: ship.call("_physics_process", 0.1) assert(not ship.call("is_mini_boosting")) ship.set("energy", 100.0) + ship.velocity = Vector2.UP*ship.max_speed Input.action_release("ship_mini_boost") ship.call("_physics_process", 0.1) Input.action_press("ship_mini_boost") @@ -56,10 +59,10 @@ func _ready() -> void: ship.call("_physics_process", 0.1) Input.action_press("ship_mini_boost") ship.call("_physics_process", 0.1) - assert(ship.call("is_mini_boosting") and ship.get("energy") == 18.0) + assert(ship.call("is_mini_boosting") and is_equal_approx(ship.get("energy"),20.0-boost_cost*0.1)) Input.action_press("ship_brake") ship.call("_physics_process", 0.1) - assert(not ship.call("is_mini_boosting") and ship.get("energy") == 18.0) + assert(not ship.call("is_mini_boosting") and is_equal_approx(ship.get("energy"),20.0-boost_cost*0.1)) Input.action_release("ship_brake") ship.call("start_boost", 2.0, 0.5) ship.set("_boost_speed", ship.get("max_speed") * 2.0) @@ -69,4 +72,4 @@ func _ready() -> void: ship.call("_physics_process", 0.1) assert(not ship.call("is_mini_boosting")) Input.action_release("ship_mini_boost") - print("MINI_BOOST_TEST: PASS (Tab mapping, double speed, energy drain, orange trail, release, exhaustion latch, braking, afterburner and docking)") + print("MINI_BOOST_TEST: PASS (Tab mapping, double speed, energy drain, chassis-colored boost trail, release, exhaustion latch, braking, afterburner and docking)") diff --git a/tests/distant_integration.gd b/tests/distant_integration.gd index 3c89cc2..4a6b619 100644 --- a/tests/distant_integration.gd +++ b/tests/distant_integration.gd @@ -89,6 +89,7 @@ func _run() -> void: check(GameState.system_level==125 and GameState.highest_system==7,"Isolated progression") check(game.get_node_or_null("CelestialBodies")==null,"No celestial bodies") check(game.has_node("PlanetExpeditions") and game.has_node("Freighter"),"Freighter available in expedition") + check(game.get_node("HUD/HUDRoot").find_child("DistantStatusPanel",true,false)==null,"No redundant expedition status window") check(game.get_node("HUD/HUDRoot").find_child("ReturnFromDistant",true,false)==null,"No return button in flight HUD") check(not game.get_node("DistantSector")._return_home(),"Return requires docking") check(game.get_node("AncientStructures").get_child_count()==14,"Decorative structures") @@ -130,9 +131,9 @@ func _run() -> void: payload.payload = {"orb":"booster"} payload.position = player.position game.add_child(payload) - var count: int = progress.orbs.booster + var count: int = preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"booster") payload._physics_process(0) - check(progress.orbs.booster==count+1,"Collect orb") + check(preload("res://scripts/items/special_orbs.gd").count(progress,inventory,"booster")==count+1,"Collect orb into cargo") await frames() for victim in get_tree().get_nodes_in_group("enemies"): victim.queue_free() await frames() @@ -146,6 +147,7 @@ func _run() -> void: var maps := 0 var orbs := 0 for pickup in get_tree().get_nodes_in_group("loot"): + if pickup.get_script() == preload("res://scripts/items/loot_pickup.gd") and not pickup.item.definition.orb_id.is_empty(): orbs += 1 if pickup.get_script() == preload("res://scripts/items/expedition_pickup.gd"): if pickup.payload.has("map"): maps += 1 if pickup.payload.has("orb"): orbs += 1 diff --git a/tests/items/combat_orbs_checks.gd b/tests/items/combat_orbs_checks.gd index 70474a5..df6f94a 100644 --- a/tests/items/combat_orbs_checks.gd +++ b/tests/items/combat_orbs_checks.gd @@ -54,7 +54,8 @@ func run(game: Node) -> Array[String]: var detached := GENERATOR.generate_for_definition(weapon.definition,1) check(not ORBS.apply(progress,inventory,detached,"replication"), "Cannot replicate unowned equipment") player.is_docked = false - check(not ORBS.apply(progress,inventory,weapon,"electricity"), "Orbs require docking") + check(ORBS.apply(progress,inventory,weapon,"electricity"), "Orbs work on owned equipment during flight") + weapon.special_orbs.erase("electricity") player.is_docked = true var original_damage := weapon.get_damage_range() for id in ORBS.DAMAGE_ORBS: diff --git a/tests/items/shield_collision_test.gd b/tests/items/shield_collision_test.gd new file mode 100644 index 0000000..3b0fb02 --- /dev/null +++ b/tests/items/shield_collision_test.gd @@ -0,0 +1,74 @@ +extends Node2D +const PROJECTILE := preload("res://scenes/game/weapon_projectile.tscn") +const INTERCEPT := preload("res://scripts/game/shield_intersections.gd") +class StraightAlien extends "res://scripts/game/enemy_ship.gd": + func _laser_aim_direction(direction: Vector2) -> Vector2: return direction +func shot(kind: int) -> Area2D: + var projectile := PROJECTILE.instantiate() as Area2D + projectile.enemy_shot = true + projectile.missile_homing = false + projectile.collision_mask = 2 + projectile.configure(Vector2(-150,0),Vector2.RIGHT,kind,1200,20,1000) + add_child(projectile) + projectile.set_physics_process(false) + return projectile +func _ready() -> void: + GameState.start_new_run() + var player := preload("res://scenes/game/player_ship.tscn").instantiate() + player.get_node("Camera2D").enabled = false + add_child(player) + player.set_physics_process(false) + player.get_node("WeaponController").set_physics_process(false) + player.position = Vector2.ZERO + player.absorption = Vector3.ZERO + await get_tree().physics_frame + await get_tree().physics_frame + for kind in [ItemDefinition.WeaponKind.PLASMA,ItemDefinition.WeaponKind.MISSILE,ItemDefinition.WeaponKind.GAMMA]: + player.shields = 100 + var projectile := shot(kind) + projectile._physics_process(0.25) + assert(projectile.is_queued_for_deletion() and is_equal_approx(projectile.position.length(),50),"Fast projectile stops on shield perimeter") + assert(player.shields==80) + var radiation := player.get_node_or_null("RadiationStatus") + if radiation != null: radiation.set_physics_process(false) + player.shields = 100 + var glancing := INTERCEPT.intercept({},Vector2(-100,40),Vector2(100,40),2,[],get_tree()) + assert(glancing.collider==player and glancing.position.is_equal_approx(Vector2(-30,40)),"Shield catches glancing hits outside hull") + var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() + enemy.set_script(StraightAlien) + enemy.position = Vector2(-250,0) + add_child(enemy) + enemy.set_physics_process(false) + enemy.weapon_kind = ItemDefinition.WeaponKind.LASER + enemy.weapon_damage = 20 + enemy._player = player + enemy._combat_target = player + enemy._fire_plasma(Vector2.RIGHT) + var found := false + for child in get_children(): + if child is Line2D and child.get_point_count()==2: + assert(child.get_point_position(1).is_equal_approx(Vector2(-50,0)),"Enemy laser ends at shield edge") + found = true + assert(found and player.shields==84) + player.shields = 0 + player.hull = 100 + var bare := shot(ItemDefinition.WeaponKind.PLASMA) + bare._physics_process(0.25) + assert(is_equal_approx(bare.position.x,-18) and player.hull==80,"Bare hull uses original collision") + player.shields = 100 + var cover := StaticBody2D.new() + cover.position = Vector2(-80,0) + cover.collision_layer = 1 + var shape := CollisionShape2D.new() + var circle := CircleShape2D.new() + circle.radius = 8 + shape.shape = circle + cover.add_child(shape) + add_child(cover) + await get_tree().physics_frame + await get_tree().physics_frame + var blocked := shot(ItemDefinition.WeaponKind.PLASMA) + blocked.collision_mask = 3 + blocked._physics_process(0.25) + assert(blocked.position.x < -80 and player.shields==100,"Closer physical cover blocks shield interception") + print("SHIELD_COLLISION_TEST: PASS (fast plasma/missiles/gamma, glancing shots, exact laser endpoint, bare hull, physical cover)") diff --git a/tests/items/shield_collision_test.gd.uid b/tests/items/shield_collision_test.gd.uid new file mode 100644 index 0000000..6206c3f --- /dev/null +++ b/tests/items/shield_collision_test.gd.uid @@ -0,0 +1 @@ +uid://cf3tu1psmh85h diff --git a/tests/items/shield_collision_test.tscn b/tests/items/shield_collision_test.tscn new file mode 100644 index 0000000..6a370c5 --- /dev/null +++ b/tests/items/shield_collision_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/items/shield_collision_test.gd" id="1"] +[node name="ShieldCollisionTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/ships/alien_shield_technologies_test.gd b/tests/ships/alien_shield_technologies_test.gd index 9d1d0c2..0529a4b 100644 --- a/tests/ships/alien_shield_technologies_test.gd +++ b/tests/ships/alien_shield_technologies_test.gd @@ -156,4 +156,15 @@ func _ready() -> void: shooter._fire_plasma(Vector2.DOWN) assert(is_equal_approx(player.shields,100-laser_damage),"Actual laser hits the player") assert(is_equal_approx(shooter.shields,1000-laser_damage*0.2),"Actual laser reflection returns to the shooter") + var orphan := preload("res://scenes/game/weapon_projectile.tscn").instantiate() + orphan.enemy_shot = true + orphan.source_ship = enemy + orphan.configure(Vector2(3000,3000),Vector2.RIGHT,ItemDefinition.WeaponKind.PLASMA,600,10,1000) + add_child(orphan) + orphan.set_physics_process(false) + enemy.free() + player.shields = 100 + orphan._physics_process(0.01) + orphan._hit_target(player) + assert(player.shields==90,"Projectile still hits safely after shooter is freed; no reflection to invalid source") print("ALIEN_SHIELD_TECHNOLOGIES_TEST: PASS (8 skills, costs, 400% overload without chains, regen lock, energy reserve, 10% first hit/60s, emergency shield, reflection)") diff --git a/tests/ships/artillery_orb_radar_test.gd b/tests/ships/artillery_orb_radar_test.gd new file mode 100644 index 0000000..030cc6d --- /dev/null +++ b/tests/ships/artillery_orb_radar_test.gd @@ -0,0 +1,99 @@ +extends Node2D +const ORBS := preload("res://scripts/items/special_orbs.gd") +const PICKUP := preload("res://scripts/items/loot_pickup.gd") +const SLOT := preload("res://scenes/ui/inventory_slot_view.tscn") + +func _ready() -> void: + GameState.start_new_run() + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + var player: CharacterBody2D = game.player + player.set_physics_process(false) + player.get_node("WeaponController").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false) + var inventory: ShipInventory = player.get_node("ShipInventory") + inventory.cargo.fill(null) + var pickup := Node2D.new() + pickup.set_script(PICKUP) + pickup.item = ORBS.create_item("piercing",2) + pickup.position = player.position + game.add_child(pickup) + pickup._physics_process(0) + assert(pickup.is_queued_for_deletion() and inventory.cargo[0].stack_count==2) + for i in range(1,inventory.get_cargo_capacity()): + var item := ItemInstance.new() + item.definition = preload("res://resources/items/hull_plating.tres") + inventory.cargo[i] = item + var same := Node2D.new() + same.set_script(PICKUP) + same.item = ORBS.create_item("piercing",3) + same.position = player.position + game.add_child(same) + same._physics_process(0) + assert(same.is_queued_for_deletion() and inventory.cargo[0].stack_count==5,"Stack pickup works even with full cargo") + var other := Node2D.new() + other.set_script(PICKUP) + other.item = ORBS.create_item("booster") + other.position = player.position + game.add_child(other) + other._physics_process(0) + assert(not other.is_queued_for_deletion(),"Different orb stays in space while cargo is full") + inventory.cargo[1] = null + other._physics_process(0) + assert(other.is_queued_for_deletion() and inventory.cargo[1].definition.orb_id=="booster") + var source := SLOT.instantiate() + source.slot_kind = "cargo" + source.slot_index = 0 + add_child(source) + var target := SLOT.instantiate() + target.slot_kind = "weapon" + target.slot_index = 0 + add_child(target) + var click := InputEventMouseButton.new() + click.button_index = MOUSE_BUTTON_LEFT + click.pressed = true + click.double_click = true + source._gui_input(click) + assert(inventory.selected_orb==inventory.cargo[0]) + click.double_click = false + target._gui_input(click) + assert("piercing" in inventory.weapon_slots[0].special_orbs and inventory.cargo[0].stack_count==4 and inventory.selected_orb==null) + assert(source.get_node("StackOverlay/Count").text=="×4") + assert(inventory.select_orb(1)) + assert(not inventory.use_selected_orb(inventory.weapon_slots[0]) and inventory.cargo[1].stack_count==1,"Invalid target never consumes an orb") + var escape := InputEventKey.new() + escape.keycode = KEY_ESCAPE + escape.pressed = true + inventory._input(escape) + assert(inventory.selected_orb==null) + var engine := ItemInstance.new() + engine.definition = preload("res://resources/items/engine_extension.tres") + inventory.module_slots[2] = engine + assert(inventory.select_orb(1) and inventory.use_selected_orb(engine)) + assert(inventory.cargo[1]==null and "booster" in engine.special_orbs) + var radar := Control.new() + radar.set_script(preload("res://scripts/ui/scanner_overlay.gd")) + radar.player = player + add_child(radar) + var freighter: Node2D = game.get_node("Freighter") + freighter.position = player.position+Vector2(1000000,1000000) + assert(radar.is_target_in_range(freighter),"Freighter ignores distance") + radar._physics_process(0) + assert(radar.targets.has(freighter)) + var cannon := ItemInstance.new() + cannon.definition = preload("res://resources/items/artillery_cannon.tres") + assert(cannon.get_stat_total("damage")==50 and cannon.definition.ammunition_cost==10) + cannon.damage_min = 74 + cannon.damage_max = 74 + assert(is_equal_approx(cannon.get_stat_total("damage"),50),"Already rolled artillery receives the balance correction") + var fx := preload("res://scripts/game/artillery_effect.gd") + fx.launch(player,player.position,Vector2.RIGHT) + assert(fx._sound.get_length()>0.1) + source.queue_free() + target.queue_free() + game.get_node("HUD")._toggle_inventory() + await get_tree().create_timer(0.4).timeout + await RenderingServer.frame_post_draw + get_viewport().get_texture().get_image().save_png("res://artifacts/orb_cargo_stacks.png") + print("ARTILLERY_ORB_RADAR_TEST: PASS (50 base damage, CC0 audio, cargo drops, full-cargo stacking, double-click target use, invalid target/cancel, stack counter, unlimited freighter radar)") diff --git a/tests/ships/artillery_orb_radar_test.gd.uid b/tests/ships/artillery_orb_radar_test.gd.uid new file mode 100644 index 0000000..08a1aee --- /dev/null +++ b/tests/ships/artillery_orb_radar_test.gd.uid @@ -0,0 +1 @@ +uid://5j8t5rsq0ds6 diff --git a/tests/ships/artillery_orb_radar_test.tscn b/tests/ships/artillery_orb_radar_test.tscn new file mode 100644 index 0000000..6f0318c --- /dev/null +++ b/tests/ships/artillery_orb_radar_test.tscn @@ -0,0 +1,4 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/ships/artillery_orb_radar_test.gd" id="1"] +[node name="ArtilleryOrbRadarTest" type="Node2D"] +script = ExtResource("1") diff --git a/tests/ships/laboratory_test.gd b/tests/ships/laboratory_test.gd new file mode 100644 index 0000000..ab41ad4 --- /dev/null +++ b/tests/ships/laboratory_test.gd @@ -0,0 +1,159 @@ +extends Node2D +const LAB := preload("res://scripts/planets/laboratory.gd") +const GEN := preload("res://scripts/items/item_generator.gd") +var player: CharacterBody2D + +class StraightLaser extends "res://scripts/game/weapon_controller.gd": + func _aim_direction_from(_origin: Vector2) -> Vector2: return Vector2.RIGHT + +func target_at(point: Vector2) -> CharacterBody2D: + var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate() + add_child(enemy) + enemy.set_physics_process(false) + enemy.position = point + enemy.max_hull = 10000 + enemy.hull = 10000 + enemy.shields = 0 + enemy.absorption = Vector3.ZERO + enemy._player = player + return enemy + +func snapshot(path: String) -> void: + for i in 8: await get_tree().process_frame + await RenderingServer.frame_post_draw + get_viewport().get_texture().get_image().save_png(path) + +func _ready() -> void: + seed(49141) + GameState.start_new_run() + var captain := GameState.captain + var progress: Resource = captain.expedition + LAB.ensure_offers(progress) + assert(progress.laboratory_offers.size()==3) + var offers: Array = progress.laboratory_offers.duplicate() + LAB.ensure_offers(progress) + assert(progress.laboratory_offers==offers,"Opening laboratory does not reroll offers") + assert(GEN.available_definitions().size()==GEN.DEFINITIONS.size()) + assert(not LAB.start(captain,offers[0]),"Laboratory must be built") + progress.freighter_rooms.expansion_1 = "laboratory" + assert(not LAB.start(captain,offers[0]),"Cannot research without materials") + for cost in LAB.COSTS.values(): + for key in cost: captain.minerals[key] = 1000 + var game := preload("res://scenes/game/game.tscn").instantiate() + add_child(game) + game.set_physics_process(false) + player = game.player + player.set_physics_process(false) + player.get_node("WeaponController").set_physics_process(false) + for enemy in get_tree().get_nodes_in_group("enemies"): enemy.set_physics_process(false) + var manager := game.get_node("PlanetExpeditions") + manager.open_freighter() + manager.screen._select_section("expansion_1") + assert(manager.screen._content.find_child("Research_incendiary_missile",true,false)!=null) + await snapshot("res://artifacts/laboratory_projects.png") + var before: Dictionary = captain.minerals.duplicate() + assert(LAB.start(captain,"incendiary_missile")) + for key in LAB.COSTS.incendiary_missile: assert(captain.minerals[key]==before[key]-LAB.COSTS.incendiary_missile[key]) + assert(not LAB.start(captain,"radiation_laser"),"Only one project at a time") + GameState.advance_play_time(30.0,game) + assert(progress.laboratory_remaining==30,"Research advances while docked without any mining stations") + assert(not progress.laboratory_unlocks.has("incendiary_missile")) + assert(ResourceSaver.save(progress,"user://laboratory_progress_test.tres")==OK) + var saved: Resource = ResourceLoader.load("user://laboratory_progress_test.tres","",ResourceLoader.CACHE_MODE_IGNORE) + assert(saved.laboratory_project=="incendiary_missile" and saved.laboratory_remaining==30 and saved.laboratory_offers==offers) + await snapshot("res://artifacts/laboratory_progress.png") + manager._leave() + get_tree().paused = true + GameState.advance_play_time(30,game) + assert(progress.laboratory_remaining==30,"Ordinary pause stops research") + get_tree().paused = false + GameState.advance_play_time(29.9,game) + assert(not progress.laboratory_unlocks.has("incendiary_missile")) + GameState.advance_play_time(0.1,game) + assert(progress.laboratory_unlocks.has("incendiary_missile") and progress.laboratory_offers.size()==2) + assert(GEN.available_definitions().has(LAB.definition("incendiary_missile"))) + for id in ["radiation_laser","artillery_cannon"]: + assert(LAB.start(captain,id)) + LAB.advance(progress,60) + assert(progress.laboratory_offers.is_empty() and progress.laboratory_unlocks.size()==3) + assert(GEN.available_definitions().size()==GEN.DEFINITIONS.size()+3) + var inventory: ShipInventory = player.get_node("ShipInventory") + var cannon := ItemInstance.new() + cannon.definition = LAB.definition("artillery_cannon") + var controller := StraightLaser.new() + player.add_child(controller) + controller.set_physics_process(false) + inventory.ammunition = 9 + assert(not controller._consume_shot_resource(cannon) and inventory.ammunition==9) + inventory.ammunition = 10 + assert(controller._consume_shot_resource(cannon) and inventory.ammunition==0) + assert(cannon.get_fire_interval()==2.2 and cannon.get_damage_type()==DamageTypes.Type.KINETIC) + var camera_offset: Vector2 = player.get_node("Camera2D").offset + controller._fire_projectile(cannon) + assert(preload("res://scripts/game/artillery_effect.gd")._sound.get_length()>0.1) + inventory.unlocked_weapon_slots = 3 + var incendiary := ItemInstance.new() + incendiary.definition = LAB.definition("incendiary_missile") + inventory.weapon_slots[2] = incendiary + inventory.missile_ammunition = 2 + assert(controller.try_fire_missile(2) and inventory.missile_ammunition==1) + assert(not controller.try_fire_missile(2) and inventory.missile_ammunition==1,"Incendiary uses shared missile cooldown") + var launched := false + for child in player.get_parent().get_children(): + if child.get_script() == preload("res://scripts/game/weapon_projectile.gd"): + child.set_physics_process(false) + if child.weapon_effect == "incendiary": + assert(child.missile_homing and child.get_damage_type()==DamageTypes.Type.KINETIC) + launched = true + assert(launched,"Incendiary launcher creates a homing missile") + player.position = Vector2.ZERO + var target := target_at(Vector2(200,-28)) + var laser := ItemInstance.new() + laser.definition = LAB.definition("radiation_laser") + laser.damage_min = 4 + laser.damage_max = 4 + inventory.weapon_slots[0] = laser + player.energy = player.max_energy + await get_tree().physics_frame + await get_tree().physics_frame + controller._fire_laser(0,laser) + assert(controller._laser_lines[0].radiation_beam and target.hull==9996) + var status := target.get_node("RadiationStatus") + status.set_physics_process(false) + status._physics_process(0.5) + assert(target.hull==9994,"Green laser deals radiation over time") + status.remaining = 0 + var missile := preload("res://scenes/game/weapon_projectile.tscn").instantiate() + missile.weapon_effect = "incendiary" + missile.configure(target.position,Vector2.RIGHT,ItemDefinition.WeaponKind.MISSILE,400,20,1000) + add_child(missile) + missile.set_physics_process(false) + var hp: float = target.hull + missile._hit_target(target) + assert(target.hull==hp-20) + await get_tree().process_frame + var clouds := get_tree().get_nodes_in_group("burning_gas") + assert(clouds.size()==1) + var gas: Node2D = clouds[0] + gas.set_physics_process(false) + gas.remaining = 5 + gas._tick_elapsed = 0 + var overlap := Node2D.new() + overlap.set_script(preload("res://scripts/game/burning_gas.gd")) + overlap.position = gas.position + overlap.damage_per_second = 10 + add_child(overlap) + overlap.set_physics_process(false) + hp = target.hull + gas._physics_process(1) + overlap._physics_process(1) + assert(is_equal_approx(target.hull,hp-22),"Overlapping clouds use only strongest damage") + target.position += Vector2(200,0) + hp = target.hull + gas._physics_process(1) + assert(target.hull==hp,"Leaving the cloud stops gas damage") + gas._physics_process(10) + assert(gas.is_queued_for_deletion(),"Cloud expires after five seconds") + await get_tree().create_timer(0.22).timeout + assert(player.get_node("Camera2D").offset.is_equal_approx(camera_offset),"Artillery camera impulse restores the original offset") + print("LABORATORY_TEST: PASS (3 persistent choices, costs, gameplay clock, pause, save, unlock-only loot pool, artillery 10 ammo, green radiation laser, homing incendiary impact and non-stacking 5s gas, camera recovery)") diff --git a/tests/ships/laboratory_test.gd.uid b/tests/ships/laboratory_test.gd.uid new file mode 100644 index 0000000..4610e6b --- /dev/null +++ b/tests/ships/laboratory_test.gd.uid @@ -0,0 +1 @@ +uid://b6gc08ngoumjg diff --git a/tests/ships/laboratory_test.tscn b/tests/ships/laboratory_test.tscn new file mode 100644 index 0000000..8f12c03 --- /dev/null +++ b/tests/ships/laboratory_test.tscn @@ -0,0 +1,5 @@ +[gd_scene load_steps=2 format=3] +[ext_resource type="Script" path="res://tests/ships/laboratory_test.gd" id="1"] +[node name="LaboratoryTest" type="Node2D"] +process_mode = 3 +script = ExtResource("1")