Zmiany
40
artifacts/alien_shield_icons_preview.png.import
Normal file
@ -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
|
||||
BIN
artifacts/artillery_visual_preview.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
40
artifacts/artillery_visual_preview.png.import
Normal file
@ -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
|
||||
BIN
artifacts/burning_cloud_preview.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
40
artifacts/burning_cloud_preview.png.import
Normal file
@ -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
|
||||
BIN
artifacts/combat_visuals_preview.png
Normal file
|
After Width: | Height: | Size: 203 KiB |
40
artifacts/combat_visuals_preview.png.import
Normal file
@ -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
|
||||
BIN
artifacts/laboratory_icons_preview.png
Normal file
|
After Width: | Height: | Size: 397 KiB |
40
artifacts/laboratory_icons_preview.png.import
Normal file
@ -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
|
||||
BIN
artifacts/laboratory_progress.png
Normal file
|
After Width: | Height: | Size: 914 KiB |
40
artifacts/laboratory_progress.png.import
Normal file
@ -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
|
||||
BIN
artifacts/laboratory_projects.png
Normal file
|
After Width: | Height: | Size: 920 KiB |
40
artifacts/laboratory_projects.png.import
Normal file
@ -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
|
||||
BIN
artifacts/orb_cargo_stacks.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
40
artifacts/orb_cargo_stacks.png.import
Normal file
@ -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
|
||||
BIN
artifacts/ship_shapes_review.png
Normal file
|
After Width: | Height: | Size: 179 KiB |
40
artifacts/ship_shapes_review.png.import
Normal file
@ -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
|
||||
BIN
assets/icons/artillery_cannon.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
40
assets/icons/artillery_cannon.png.import
Normal file
@ -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
|
||||
BIN
assets/icons/artillery_cannon_v2.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
40
assets/icons/artillery_cannon_v2.png.import
Normal file
@ -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
|
||||
3
assets/icons/artillery_cannon_v2_prompt.md
Normal file
@ -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.
|
||||
BIN
assets/icons/incendiary_missile.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
40
assets/icons/incendiary_missile.png.import
Normal file
@ -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
|
||||
10
assets/icons/laboratory_weapon_prompts.md
Normal file
@ -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.
|
||||
BIN
assets/icons/radiation_laser.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
40
assets/icons/radiation_laser.png.import
Normal file
@ -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
|
||||
9
assets/sounds/effects/ARTILLERY_CREDITS.md
Normal file
@ -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.
|
||||
BIN
assets/sounds/effects/artillery_shot.ogg
Normal file
19
assets/sounds/effects/artillery_shot.ogg.import
Normal file
@ -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
|
||||
BIN
assets/sounds/effects/artillery_shot_reverb.ogg
Normal file
19
assets/sounds/effects/artillery_shot_reverb.ogg.import
Normal file
@ -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
|
||||
28
resources/items/artillery_cannon.tres
Normal file
@ -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
|
||||
|
||||
29
resources/items/incendiary_missile.tres
Normal file
@ -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
|
||||
|
||||
28
resources/items/radiation_laser.tres
Normal file
@ -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
|
||||
|
||||
67
scripts/game/artillery_effect.gd
Normal file
@ -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)
|
||||
1
scripts/game/artillery_effect.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dfgl3oawwf3ex
|
||||
30
scripts/game/artillery_muzzle.gdshader
Normal file
@ -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));
|
||||
}
|
||||
1
scripts/game/artillery_muzzle.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://pglyfxwskx6d
|
||||
69
scripts/game/burning_gas.gd
Normal file
@ -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))
|
||||
1
scripts/game/burning_gas.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://1y3m4xkmf0g
|
||||
60
scripts/game/burning_gas.gdshader
Normal file
@ -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));
|
||||
}
|
||||
1
scripts/game/burning_gas.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bvjtcg8rb8a0j
|
||||
23
scripts/game/combat_vfx.gd
Normal file
@ -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)
|
||||
1
scripts/game/combat_vfx.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cay75l5mk202v
|
||||
@ -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"))
|
||||
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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
_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
|
||||
_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
|
||||
|
||||
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
|
||||
_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)
|
||||
|
||||
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
|
||||
|
||||
@ -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)
|
||||
|
||||
66
scripts/game/hull_damage_visual.gd
Normal file
@ -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)
|
||||
1
scripts/game/hull_damage_visual.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://d2ntnpjk8svra
|
||||
@ -1,9 +1,10 @@
|
||||
extends Node2D
|
||||
|
||||
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))
|
||||
|
||||
@ -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))
|
||||
|
||||
18
scripts/game/radiation_aura.gd
Normal file
@ -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))
|
||||
1
scripts/game/radiation_aura.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bkn33hv2w6sas
|
||||
@ -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
|
||||
|
||||
30
scripts/game/shield_intersections.gd
Normal file
@ -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
|
||||
1
scripts/game/shield_intersections.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dc2pouqr4e0dv
|
||||
@ -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
|
||||
_time += delta
|
||||
_hit_flash_timer = maxf(0.0,_hit_flash_timer-delta)
|
||||
queue_redraw()
|
||||
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
|
||||
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))
|
||||
|
||||
@ -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()
|
||||
|
||||
27
scripts/game/thruster_plume.gdshader
Normal file
@ -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);
|
||||
}
|
||||
1
scripts/game/thruster_plume.gdshader.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://n0w1ie6ibtex
|
||||
27
scripts/game/weapon_camera_impulse.gd
Normal file
@ -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
|
||||
1
scripts/game/weapon_camera_impulse.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cbj231fonnxx7
|
||||
@ -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)
|
||||
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:
|
||||
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)
|
||||
|
||||
@ -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"):
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
47
scripts/planets/laboratory.gd
Normal file
@ -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
|
||||
1
scripts/planets/laboratory.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b8reao0fpb3y
|
||||
73
scripts/planets/laboratory_panel.gd
Normal file
@ -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()
|
||||
1
scripts/planets/laboratory_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cuot0ai4quqpi
|
||||
@ -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])
|
||||
|
||||
@ -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 = {}
|
||||
|
||||
@ -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)
|
||||
|
||||
30
scripts/ui/orb_target_cursor.gd
Normal file
@ -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"
|
||||
1
scripts/ui/orb_target_cursor.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dmex3lcws6qju
|
||||
@ -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)
|
||||
|
||||
@ -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)")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
74
tests/items/shield_collision_test.gd
Normal file
@ -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)")
|
||||
1
tests/items/shield_collision_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cf3tu1psmh85h
|
||||
4
tests/items/shield_collision_test.tscn
Normal file
@ -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")
|
||||
@ -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)")
|
||||
|
||||
99
tests/ships/artillery_orb_radar_test.gd
Normal file
@ -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)")
|
||||
1
tests/ships/artillery_orb_radar_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://5j8t5rsq0ds6
|
||||
4
tests/ships/artillery_orb_radar_test.tscn
Normal file
@ -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")
|
||||
159
tests/ships/laboratory_test.gd
Normal file
@ -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)")
|
||||
1
tests/ships/laboratory_test.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b6gc08ngoumjg
|
||||