Zmiany
BIN
artifacts/captain_window_preview.png
Normal file
|
After Width: | Height: | Size: 752 KiB |
40
artifacts/captain_window_preview.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cyeugj6bwpxwe"
|
||||||
|
path="res://.godot/imported/captain_window_preview.png-fd71a3b2bc0fe3fe06fb1f7e9e2c97dc.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/captain_window_preview.png"
|
||||||
|
dest_files=["res://.godot/imported/captain_window_preview.png-fd71a3b2bc0fe3fe06fb1f7e9e2c97dc.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/item_drag_preview.png
Normal file
|
After Width: | Height: | Size: 566 KiB |
40
artifacts/item_drag_preview.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cepoalnss6c1o"
|
||||||
|
path="res://.godot/imported/item_drag_preview.png-775710079bd6bc529ba2e05a44060976.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/item_drag_preview.png"
|
||||||
|
dest_files=["res://.godot/imported/item_drag_preview.png-775710079bd6bc529ba2e05a44060976.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/passive_extensions_preview.png
Normal file
|
After Width: | Height: | Size: 996 KiB |
40
artifacts/passive_extensions_preview.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://db6g0pym7vseg"
|
||||||
|
path="res://.godot/imported/passive_extensions_preview.png-e441e432f841964ebe8fd7ee54f7d2bb.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://artifacts/passive_extensions_preview.png"
|
||||||
|
dest_files=["res://.godot/imported/passive_extensions_preview.png-e441e432f841964ebe8fd7ee54f7d2bb.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
|
||||||
21
assets/icons/extensions/passive_art_prompts.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Passive extension art
|
||||||
|
|
||||||
|
Generated with the built-in image_gen tool. Transparent PNG originals are retained in assets/icons/extensions/.
|
||||||
|
|
||||||
|
## recycler
|
||||||
|
|
||||||
|
File: recycler_art.png
|
||||||
|
|
||||||
|
Prompt: Use case: stylized-concept. Asset type: high quality inventory icon for a top-down space action RPG. A compact spaceship salvage recycler module: two curved mechanical arms feeding a small jagged asteroid into a circular energy core, cyan and emerald luminous energy loops, polished gunmetal and bronze housing. Single isolated object centered, three-quarter isometric view, bold readable silhouette at 48 pixels, detailed hand-painted realistic 3D game art, crisp metallic highlights, controlled glow confined to object. Transparent background with real alpha. Square composition with 12% empty margin, no frame, no text, no letters, no numbers, no watermark.
|
||||||
|
|
||||||
|
## virus
|
||||||
|
|
||||||
|
File: virus_art.png
|
||||||
|
|
||||||
|
Prompt: Use case: stylized-concept. Asset type: high quality inventory icon for a top-down space action RPG. A hostile cybernetic virus module for a spaceship: a menacing violet crystalline neural processor with three hooked antenna prongs and tiny branching magenta circuitry, electric purple core, polished dark titanium housing. Single isolated object centered, three-quarter isometric view, bold readable silhouette at 48 pixels, detailed hand-painted realistic 3D game art, crisp metallic highlights, controlled glow confined to object. Transparent background with real alpha. Square composition with 12% empty margin, no frame, no text, no letters, no numbers, no watermark.
|
||||||
|
|
||||||
|
## titanium_armor
|
||||||
|
|
||||||
|
File: titanium_armor_art.png
|
||||||
|
|
||||||
|
Prompt: Use case: stylized-concept. Asset type: high quality inventory icon for a top-down space action RPG. A spaceship titanium armor module: layered silver titanium armored plates forming a strong hexagonal shield, beveled edges, subtle cyan energy seams and a warm gold central reinforcing spine, heavy solid premium metal. Single isolated object centered, three-quarter isometric view, bold readable silhouette at 48 pixels, detailed hand-painted realistic 3D game art, crisp metallic highlights, controlled glow confined to object. Transparent background with real alpha. Square composition with 12% empty margin, no frame, no text, no letters, no numbers, no watermark.
|
||||||
BIN
assets/icons/extensions/recycler_art.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
40
assets/icons/extensions/recycler_art.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dphd1x0th81b7"
|
||||||
|
path="res://.godot/imported/recycler_art.png-9bd765a370eabd761a8651a4e26ad2fa.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/icons/extensions/recycler_art.png"
|
||||||
|
dest_files=["res://.godot/imported/recycler_art.png-9bd765a370eabd761a8651a4e26ad2fa.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/extensions/titanium_armor_art.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
40
assets/icons/extensions/titanium_armor_art.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://y00q03eeo73k"
|
||||||
|
path="res://.godot/imported/titanium_armor_art.png-77b741707d0dfb24fcae9ad6ad3219d4.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/icons/extensions/titanium_armor_art.png"
|
||||||
|
dest_files=["res://.godot/imported/titanium_armor_art.png-77b741707d0dfb24fcae9ad6ad3219d4.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/extensions/virus_art.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
40
assets/icons/extensions/virus_art.png.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cw4ub6vfukxj5"
|
||||||
|
path="res://.godot/imported/virus_art.png-6fdadf0dc8dadfe12432e7a72485171e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/icons/extensions/virus_art.png"
|
||||||
|
dest_files=["res://.godot/imported/virus_art.png-6fdadf0dc8dadfe12432e7a72485171e.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
|
||||||
8
assets/icons/ui/aim_cursor.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||||
|
<g fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<g stroke="#06121e" stroke-width="4"><circle cx="24" cy="24" r="7"/><path d="M24 5v9M24 34v9M5 24h9M34 24h9"/></g>
|
||||||
|
<g stroke="#a4efff" stroke-width="1.5"><circle cx="24" cy="24" r="7"/><path d="M24 5v9M24 34v9M5 24h9M34 24h9"/></g>
|
||||||
|
<path stroke="#07121e" stroke-width="3" d="M10 16v-6h6M32 10h6v6M38 32v6h-6M16 38h-6v-6"/>
|
||||||
|
<path stroke="#efbd79" stroke-width="1" d="M10 16v-6h6M32 10h6v6M38 32v6h-6M16 38h-6v-6"/>
|
||||||
|
</g><circle cx="24" cy="24" r="1.4" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 620 B |
43
assets/icons/ui/aim_cursor.svg.import
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://be3rq5fhd8dgx"
|
||||||
|
path="res://.godot/imported/aim_cursor.svg-cd2711c215cac45daa91fe6817bf504e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/icons/ui/aim_cursor.svg"
|
||||||
|
dest_files=["res://.godot/imported/aim_cursor.svg-cd2711c215cac45daa91fe6817bf504e.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
13
resources/items/recycler.tres
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[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/extensions/recycler_art.png" id="2"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
item_id = "recycler"
|
||||||
|
display_name = "Recykler"
|
||||||
|
icon = ExtResource("2")
|
||||||
|
category = 5
|
||||||
|
description = "Pasywne rozszerzenie statku. Działa po zamontowaniu. Zebrany odłamek asteroidy przywraca tarcze i energię."
|
||||||
|
passive_effect = "recycler"
|
||||||
13
resources/items/titanium_armor.tres
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[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/extensions/titanium_armor_art.png" id="2"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
item_id = "titanium_armor"
|
||||||
|
display_name = "Pancerz z tytanu"
|
||||||
|
icon = ExtResource("2")
|
||||||
|
category = 5
|
||||||
|
description = "Pasywne rozszerzenie statku. Działa po zamontowaniu. Przy niskim kadłubie pochłania 50% otrzymywanych obrażeń."
|
||||||
|
passive_effect = "titanium_armor"
|
||||||
13
resources/items/virus.tres
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[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/extensions/virus_art.png" id="2"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
item_id = "virus"
|
||||||
|
display_name = "Wirus"
|
||||||
|
icon = ExtResource("2")
|
||||||
|
category = 5
|
||||||
|
description = "Pasywne rozszerzenie statku. Działa po zamontowaniu. Uciekający wrogowie przechodzą pod kontrolę wirusa i atakują inne wrogie statki."
|
||||||
|
passive_effect = "virus"
|
||||||
@ -3,11 +3,12 @@ extends ActiveAbility
|
|||||||
@export var speed_bonus: float = 2.0
|
@export var speed_bonus: float = 2.0
|
||||||
@export var acceleration_time: float = 0.3
|
@export var acceleration_time: float = 0.3
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
ability_id = "afterburner"
|
||||||
|
cooldown = 30.0
|
||||||
|
|
||||||
func unavailable_reason(ship: CharacterBody2D) -> String:
|
func unavailable_reason(ship: CharacterBody2D) -> String:
|
||||||
if ship.call("is_boosting"):
|
return "Dopalacz już działa" if ship.call("is_boosting") else ""
|
||||||
return "Dopalacz już działa"
|
|
||||||
return "Wymagana pełna energia" if ship.get("energy") < ship.get("max_energy") else ""
|
|
||||||
|
|
||||||
func activate(ship: CharacterBody2D) -> void:
|
func activate(ship: CharacterBody2D) -> void:
|
||||||
ship.set("energy", 0.0)
|
|
||||||
ship.call("start_boost", speed_bonus, acceleration_time)
|
ship.call("start_boost", speed_bonus, acceleration_time)
|
||||||
|
|||||||
@ -30,10 +30,14 @@ func _update_position() -> void:
|
|||||||
global_position = enemy.global_position + Vector2(0, -enemy.get_visual_size() * 0.5 - 12.0)
|
global_position = enemy.global_position + Vector2(0, -enemy.get_visual_size() * 0.5 - 12.0)
|
||||||
|
|
||||||
func _tier_color() -> Color:
|
func _tier_color() -> Color:
|
||||||
|
if bool(enemy.get("virus_controlled")):
|
||||||
|
return Color("c57dff")
|
||||||
return Color("ff76d6") if enemy.loot_tier == 2 else (Color("ffc785") if enemy.loot_tier == 1 else Color("ef5967"))
|
return Color("ff76d6") if enemy.loot_tier == 2 else (Color("ffc785") if enemy.loot_tier == 1 else Color("ef5967"))
|
||||||
|
|
||||||
func _update_tier_label() -> void:
|
func _update_tier_label() -> void:
|
||||||
_tier_label.text = "BOSS" if enemy.loot_tier == 2 else ("ELITA" if enemy.loot_tier == 1 else "")
|
_tier_label.text = "BOSS" if enemy.loot_tier == 2 else ("ELITA" if enemy.loot_tier == 1 else "")
|
||||||
|
if bool(enemy.get("virus_controlled")):
|
||||||
|
_tier_label.text = "WIRUS"
|
||||||
_tier_label.add_theme_color_override("font_color", _tier_color())
|
_tier_label.add_theme_color_override("font_color", _tier_color())
|
||||||
|
|
||||||
func get_health_ratio() -> float:
|
func get_health_ratio() -> float:
|
||||||
|
|||||||
@ -40,6 +40,40 @@ var _sprite: Sprite2D
|
|||||||
var _weapon_timer := 0.0
|
var _weapon_timer := 0.0
|
||||||
var _slow_remaining := 0.0
|
var _slow_remaining := 0.0
|
||||||
var _combat_target: Node2D
|
var _combat_target: Node2D
|
||||||
|
var virus_controlled := false
|
||||||
|
var _reward_claimed := false
|
||||||
|
|
||||||
|
func set_virus_controlled(value: bool) -> void:
|
||||||
|
if virus_controlled == value or is_queued_for_deletion():
|
||||||
|
return
|
||||||
|
virus_controlled = value
|
||||||
|
_combat_target = null
|
||||||
|
_activated = true
|
||||||
|
if value:
|
||||||
|
remove_from_group("enemies")
|
||||||
|
add_to_group("virus_allies")
|
||||||
|
set_deferred("collision_layer", 64)
|
||||||
|
set_deferred("collision_mask", 5)
|
||||||
|
if not _reward_claimed:
|
||||||
|
_reward_claimed = true
|
||||||
|
_reward_captain()
|
||||||
|
_drop_loot()
|
||||||
|
else:
|
||||||
|
remove_from_group("virus_allies")
|
||||||
|
add_to_group("enemies")
|
||||||
|
set_deferred("collision_layer", 4)
|
||||||
|
set_deferred("collision_mask", 2)
|
||||||
|
if is_instance_valid(_sprite):
|
||||||
|
_sprite.modulate = Color("b986ff") if value else get_tier_tint()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
if virus_controlled:
|
||||||
|
draw_arc(Vector2.ZERO, get_visual_size() * 0.6, 0.0, TAU, 48, Color("c57dff"), 2.0, true)
|
||||||
|
for index in 3:
|
||||||
|
var marker := Vector2.from_angle(index * TAU / 3.0) * get_visual_size() * 0.67
|
||||||
|
draw_circle(marker, 2.5, Color("e0baff"))
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
tactic = randi_range(0, 1) if tactic_choice == 0 else tactic_choice - 1
|
tactic = randi_range(0, 1) if tactic_choice == 0 else tactic_choice - 1
|
||||||
@ -105,6 +139,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
move_and_slide()
|
move_and_slide()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not virus_controlled and hull > 0.0 and hull < max_hull * 0.1:
|
||||||
|
_player.call("try_control_enemy", self)
|
||||||
_combat_target = select_combat_target()
|
_combat_target = select_combat_target()
|
||||||
if _combat_target == null:
|
if _combat_target == null:
|
||||||
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
|
velocity = velocity.move_toward(Vector2.ZERO, approach_acceleration * delta)
|
||||||
@ -134,7 +170,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
_check_player_collision()
|
_check_player_collision()
|
||||||
|
|
||||||
func is_retreating() -> bool:
|
func is_retreating() -> bool:
|
||||||
return hull > 0.0 and hull < max_hull * 0.1
|
return not virus_controlled and hull > 0.0 and hull < max_hull * 0.1
|
||||||
|
|
||||||
func _drop_retreat_mine(toward_target: Vector2) -> void:
|
func _drop_retreat_mine(toward_target: Vector2) -> void:
|
||||||
var mine := Node2D.new()
|
var mine := Node2D.new()
|
||||||
@ -168,6 +204,17 @@ func get_slow_multiplier() -> float:
|
|||||||
return 0.5 if _slow_remaining > 0.0 else 1.0
|
return 0.5 if _slow_remaining > 0.0 else 1.0
|
||||||
|
|
||||||
func select_combat_target() -> Node2D:
|
func select_combat_target() -> Node2D:
|
||||||
|
if virus_controlled:
|
||||||
|
var opponent: Node2D
|
||||||
|
var nearest := INF
|
||||||
|
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||||
|
if enemy.is_queued_for_deletion() or float(enemy.get("hull")) <= 0.0:
|
||||||
|
continue
|
||||||
|
var distance := global_position.distance_squared_to(enemy.global_position)
|
||||||
|
if distance < nearest:
|
||||||
|
nearest = distance
|
||||||
|
opponent = enemy
|
||||||
|
return opponent
|
||||||
var closest: Node2D
|
var closest: Node2D
|
||||||
var closest_distance := INF
|
var closest_distance := INF
|
||||||
for flare in get_tree().get_nodes_in_group("player_flares"):
|
for flare in get_tree().get_nodes_in_group("player_flares"):
|
||||||
@ -177,6 +224,13 @@ func select_combat_target() -> Node2D:
|
|||||||
if distance <= maxf(activation_range, weapon_range) and distance < closest_distance:
|
if distance <= maxf(activation_range, weapon_range) and distance < closest_distance:
|
||||||
closest = flare
|
closest = flare
|
||||||
closest_distance = distance
|
closest_distance = distance
|
||||||
|
for ally in get_tree().get_nodes_in_group("virus_allies"):
|
||||||
|
if ally.is_queued_for_deletion() or float(ally.get("hull")) <= 0.0:
|
||||||
|
continue
|
||||||
|
var distance := global_position.distance_to(ally.global_position)
|
||||||
|
if distance <= activation_range and distance < closest_distance:
|
||||||
|
closest = ally
|
||||||
|
closest_distance = distance
|
||||||
if closest != null:
|
if closest != null:
|
||||||
return closest
|
return closest
|
||||||
if is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked")):
|
if is_instance_valid(_player) and not bool(_player.get("_is_destroyed")) and not bool(_player.call("is_cloaked")):
|
||||||
@ -191,16 +245,19 @@ func apply_mine_hit() -> void:
|
|||||||
take_damage(hull * 0.5, false, DamageTypes.Type.KINETIC, true)
|
take_damage(hull * 0.5, false, DamageTypes.Type.KINETIC, true)
|
||||||
|
|
||||||
func _check_player_collision() -> void:
|
func _check_player_collision() -> void:
|
||||||
|
if virus_controlled:
|
||||||
|
return
|
||||||
for collision_index in get_slide_collision_count():
|
for collision_index in get_slide_collision_count():
|
||||||
var collision := get_slide_collision(collision_index)
|
var collision := get_slide_collision(collision_index)
|
||||||
if collision.get_collider() == _player and _player.has_method("register_enemy_collision"):
|
if collision.get_collider() == _player and _player.has_method("register_enemy_collision"):
|
||||||
_player.call("register_enemy_collision", global_position)
|
_player.call("register_enemy_collision", global_position)
|
||||||
|
|
||||||
func _fire_plasma(direction: Vector2) -> void:
|
func _fire_plasma(direction: Vector2) -> void:
|
||||||
|
var shot_mask := 37 if virus_controlled else 115
|
||||||
var muzzle := global_position + direction * (get_visual_size() * 0.5 + 8.0)
|
var muzzle := global_position + direction * (get_visual_size() * 0.5 + 8.0)
|
||||||
if weapon_kind == ItemDefinition.WeaponKind.LASER:
|
if weapon_kind == ItemDefinition.WeaponKind.LASER:
|
||||||
var endpoint := muzzle + _laser_aim_direction(direction) * weapon_range
|
var endpoint := muzzle + _laser_aim_direction(direction) * weapon_range
|
||||||
var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, 51, [get_rid()])
|
var query := PhysicsRayQueryParameters2D.create(muzzle, endpoint, shot_mask, [get_rid()])
|
||||||
var hit := get_world_2d().direct_space_state.intersect_ray(query)
|
var hit := get_world_2d().direct_space_state.intersect_ray(query)
|
||||||
if not hit.is_empty():
|
if not hit.is_empty():
|
||||||
endpoint = hit["position"]
|
endpoint = hit["position"]
|
||||||
@ -218,7 +275,7 @@ func _fire_plasma(direction: Vector2) -> void:
|
|||||||
else:
|
else:
|
||||||
var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate()
|
var projectile := preload("res://scenes/game/weapon_projectile.tscn").instantiate()
|
||||||
projectile.enemy_shot = true
|
projectile.enemy_shot = true
|
||||||
projectile.collision_mask = 51
|
projectile.collision_mask = shot_mask
|
||||||
projectile.missile_homing = false
|
projectile.missile_homing = false
|
||||||
var speed := 430.0 if weapon_kind == ItemDefinition.WeaponKind.GAMMA else (950.0 if weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN else 600.0)
|
var speed := 430.0 if weapon_kind == ItemDefinition.WeaponKind.GAMMA else (950.0 if weapon_kind == ItemDefinition.WeaponKind.MACHINE_GUN else 600.0)
|
||||||
projectile.configure(muzzle, direction, weapon_kind, speed, weapon_damage, weapon_range)
|
projectile.configure(muzzle, direction, weapon_kind, speed, weapon_damage, weapon_range)
|
||||||
@ -246,8 +303,10 @@ func take_damage(amount: float, critical: bool = false, damage_type: int = Damag
|
|||||||
_shield_visual.flash_hit()
|
_shield_visual.flash_hit()
|
||||||
_show_damage_popup(actual, critical)
|
_show_damage_popup(actual, critical)
|
||||||
if hull <= 0.0:
|
if hull <= 0.0:
|
||||||
_reward_captain()
|
if not _reward_claimed:
|
||||||
_drop_loot()
|
_reward_claimed = true
|
||||||
|
_reward_captain()
|
||||||
|
_drop_loot()
|
||||||
_spawn_destruction_effect()
|
_spawn_destruction_effect()
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,12 @@ var _energy_bank_button: Button
|
|||||||
var _debug_expanded := false
|
var _debug_expanded := false
|
||||||
var _ability_buttons: Dictionary = {}
|
var _ability_buttons: Dictionary = {}
|
||||||
var _ability_bar: HBoxContainer
|
var _ability_bar: HBoxContainer
|
||||||
var _passive_skills: VBoxContainer
|
var _passive_skills: GridContainer
|
||||||
|
var _passive_captain_panel: PanelContainer
|
||||||
|
var _passive_ship_panel: PanelContainer
|
||||||
|
var _passive_ship_icons: GridContainer
|
||||||
|
var _passive_ship_buttons: Dictionary = {}
|
||||||
|
var _ship_passive_reserved_height := 0.0
|
||||||
var _inventory_clip: Control
|
var _inventory_clip: Control
|
||||||
var _sidebar: PanelContainer
|
var _sidebar: PanelContainer
|
||||||
var _sidebar_toggle: Button
|
var _sidebar_toggle: Button
|
||||||
@ -112,6 +117,11 @@ func _toggle_window(window: PanelContainer) -> void:
|
|||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
layer = RENDER_LAYERS.HUD
|
layer = RENDER_LAYERS.HUD
|
||||||
|
var cursor := Node.new()
|
||||||
|
cursor.name = "FlightCursor"
|
||||||
|
cursor.set_script(preload("res://scripts/ui/flight_cursor.gd"))
|
||||||
|
cursor.set("player", player)
|
||||||
|
add_child(cursor)
|
||||||
_sidebar_open = bool(GameState.hud_panel_state.get("ship_open", false))
|
_sidebar_open = bool(GameState.hud_panel_state.get("ship_open", false))
|
||||||
_debug_expanded = bool(GameState.hud_panel_state.get("debug_expanded", false))
|
_debug_expanded = bool(GameState.hud_panel_state.get("debug_expanded", false))
|
||||||
debug_info.visible = _debug_expanded
|
debug_info.visible = _debug_expanded
|
||||||
@ -166,11 +176,12 @@ func _create_sidebar() -> void:
|
|||||||
_captain_panel.position = Vector2(12, 12)
|
_captain_panel.position = Vector2(12, 12)
|
||||||
_captain_panel.size = Vector2(244, _captain_panel.get_combined_minimum_size().y)
|
_captain_panel.size = Vector2(244, _captain_panel.get_combined_minimum_size().y)
|
||||||
_captain_panel.resized.connect(_layout_debug)
|
_captain_panel.resized.connect(_layout_debug)
|
||||||
_passive_skills = VBoxContainer.new()
|
_passive_captain_panel = _create_passive_panel("PassiveCaptainPanel", "KAPITAN · PASYWNE")
|
||||||
|
_passive_skills = _passive_captain_panel.get_node("Contents/Icons") as GridContainer
|
||||||
_passive_skills.name = "PassiveCaptainSkills"
|
_passive_skills.name = "PassiveCaptainSkills"
|
||||||
_passive_skills.add_theme_constant_override("separation", 6)
|
_passive_ship_panel = _create_passive_panel("PassiveShipPanel", "STATEK · PASYWNE")
|
||||||
$HUDRoot.add_child(_passive_skills)
|
_passive_ship_icons = _passive_ship_panel.get_node("Contents/Icons") as GridContainer
|
||||||
_passive_skills.minimum_size_changed.connect(func() -> void: _layout_debug.call_deferred())
|
_passive_ship_panel.hide()
|
||||||
GameState.captain.changed.connect(_refresh_passive_skills)
|
GameState.captain.changed.connect(_refresh_passive_skills)
|
||||||
_refresh_passive_skills()
|
_refresh_passive_skills()
|
||||||
_debug_actions = VBoxContainer.new()
|
_debug_actions = VBoxContainer.new()
|
||||||
@ -255,22 +266,45 @@ func _create_sidebar() -> void:
|
|||||||
_create_navigation()
|
_create_navigation()
|
||||||
_layout_debug()
|
_layout_debug()
|
||||||
|
|
||||||
|
func _create_passive_panel(node_name: String, caption: String) -> PanelContainer:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.name = node_name
|
||||||
|
panel.set_script(preload("res://scripts/ui/passive_status_panel.gd"))
|
||||||
|
panel.set("caption", caption)
|
||||||
|
$HUDRoot.add_child(panel)
|
||||||
|
return panel
|
||||||
|
|
||||||
|
func _layout_passive_panels() -> void:
|
||||||
|
if not is_instance_valid(_cockpit):
|
||||||
|
return
|
||||||
|
var width := clampf(_cockpit.position.x - 24.0, 180.0, 260.0)
|
||||||
|
for panel in [_passive_captain_panel, _passive_ship_panel]:
|
||||||
|
if not is_instance_valid(panel):
|
||||||
|
continue
|
||||||
|
var grid: GridContainer = _passive_skills if panel == _passive_captain_panel else _passive_ship_icons
|
||||||
|
grid.columns = maxi(1, floori((width - 14.0) / 42.0))
|
||||||
|
panel.size = Vector2(width, panel.get_combined_minimum_size().y)
|
||||||
|
var x: float = 12.0 if panel == _passive_captain_panel else $HUDRoot.size.x - width - 12.0
|
||||||
|
panel.position = Vector2(x, $HUDRoot.size.y - panel.size.y - 12.0)
|
||||||
|
var reserved := _passive_ship_panel.size.y + 12.0 if _passive_ship_panel.visible else 0.0
|
||||||
|
if not is_equal_approx(reserved, _ship_passive_reserved_height):
|
||||||
|
_ship_passive_reserved_height = reserved
|
||||||
|
_layout_sidebar.call_deferred()
|
||||||
|
|
||||||
func _layout_debug() -> void:
|
func _layout_debug() -> void:
|
||||||
if not is_instance_valid(_debug_scroll) or not is_instance_valid(_captain_panel):
|
if not is_instance_valid(_debug_scroll) or not is_instance_valid(_captain_panel):
|
||||||
return
|
return
|
||||||
var top := _captain_panel.position.y + _captain_panel.size.y + 6.0
|
var top := _captain_panel.position.y + _captain_panel.size.y + 6.0
|
||||||
if is_instance_valid(_passive_skills):
|
var objectives := $HUDRoot.get_node_or_null("MissionObjectives") as Control
|
||||||
_passive_skills.position = Vector2(_captain_panel.position.x, top)
|
if is_instance_valid(objectives):
|
||||||
_passive_skills.size = Vector2(36, _passive_skills.get_combined_minimum_size().y)
|
top = maxf(top, objectives.position.y + objectives.size.y + 8.0)
|
||||||
if _passive_skills.visible:
|
|
||||||
top += _passive_skills.size.y + 8.0
|
|
||||||
debug_toggle.position = Vector2(12, top)
|
debug_toggle.position = Vector2(12, top)
|
||||||
_debug_actions.position = Vector2(18, top + 30)
|
_debug_actions.position = Vector2(18, top + 30)
|
||||||
_debug_actions.size = Vector2(310, _debug_actions.get_combined_minimum_size().y)
|
_debug_actions.size = Vector2(310, _debug_actions.get_combined_minimum_size().y)
|
||||||
_debug_actions.visible = _debug_expanded
|
_debug_actions.visible = _debug_expanded
|
||||||
var scroll_top := _debug_actions.position.y + _debug_actions.size.y + 8.0
|
var scroll_top := _debug_actions.position.y + _debug_actions.size.y + 8.0
|
||||||
_debug_scroll.position = Vector2(18, scroll_top)
|
_debug_scroll.position = Vector2(18, scroll_top)
|
||||||
_debug_scroll.size = Vector2(310, maxf(40, $HUDRoot.size.y - scroll_top - 12))
|
_debug_scroll.size = Vector2(310, maxf(40, (_passive_captain_panel.position.y - 8.0 if _passive_captain_panel.visible else $HUDRoot.size.y - 12.0) - scroll_top))
|
||||||
_debug_scroll.visible = _debug_expanded
|
_debug_scroll.visible = _debug_expanded
|
||||||
|
|
||||||
func _toggle_sidebar() -> void:
|
func _toggle_sidebar() -> void:
|
||||||
@ -294,7 +328,7 @@ func _layout_sidebar() -> void:
|
|||||||
if _sidebar_tween != null and _sidebar_tween.is_running():
|
if _sidebar_tween != null and _sidebar_tween.is_running():
|
||||||
_sidebar_tween.kill()
|
_sidebar_tween.kill()
|
||||||
_sidebar.scale = Vector2.ONE * EQUIPMENT_UI_SCALE
|
_sidebar.scale = Vector2.ONE * EQUIPMENT_UI_SCALE
|
||||||
_sidebar.size = Vector2(maxf(SIDEBAR_WIDTH, _sidebar.get_combined_minimum_size().x), minf(_sidebar.get_node("Scroll/Contents").get_combined_minimum_size().y + 16.0, maxf(100.0, ($HUDRoot.size.y - 148.0) / EQUIPMENT_UI_SCALE)))
|
_sidebar.size = Vector2(maxf(SIDEBAR_WIDTH, _sidebar.get_combined_minimum_size().x), minf(_sidebar.get_node("Scroll/Contents").get_combined_minimum_size().y + 16.0, maxf(100.0, ($HUDRoot.size.y - 148.0 - _ship_passive_reserved_height) / EQUIPMENT_UI_SCALE)))
|
||||||
_cockpit.scale = Vector2.ONE * 0.85
|
_cockpit.scale = Vector2.ONE * 0.85
|
||||||
_cockpit.size = Vector2(minf(850.0, ($HUDRoot.size.x - 24) / 0.85), 120)
|
_cockpit.size = Vector2(minf(850.0, ($HUDRoot.size.x - 24) / 0.85), 120)
|
||||||
_cockpit.position = Vector2(($HUDRoot.size.x - _cockpit.size.x * 0.85) * 0.5, $HUDRoot.size.y - _cockpit.size.y * 0.85 - 12)
|
_cockpit.position = Vector2(($HUDRoot.size.x - _cockpit.size.x * 0.85) * 0.5, $HUDRoot.size.y - _cockpit.size.y * 0.85 - 12)
|
||||||
@ -369,6 +403,8 @@ func _process(_delta: float) -> void:
|
|||||||
_update_energy_bank_button()
|
_update_energy_bank_button()
|
||||||
_layout_inventory()
|
_layout_inventory()
|
||||||
_update_ability_bar()
|
_update_ability_bar()
|
||||||
|
_layout_passive_panels()
|
||||||
|
_layout_debug()
|
||||||
if not _debug_expanded:
|
if not _debug_expanded:
|
||||||
return
|
return
|
||||||
var inventory := player.get_node("ShipInventory") as ShipInventory
|
var inventory := player.get_node("ShipInventory") as ShipInventory
|
||||||
@ -499,6 +535,7 @@ func _create_ability_bar() -> void:
|
|||||||
button.name = "Ability%d" % (index + 1)
|
button.name = "Ability%d" % (index + 1)
|
||||||
button.call("configure", null, Color("91dbea"))
|
button.call("configure", null, Color("91dbea"))
|
||||||
var shortcut := Label.new()
|
var shortcut := Label.new()
|
||||||
|
shortcut.name = "Shortcut"
|
||||||
shortcut.text = str(index + 1)
|
shortcut.text = str(index + 1)
|
||||||
shortcut.position = Vector2(4, 2)
|
shortcut.position = Vector2(4, 2)
|
||||||
shortcut.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
shortcut.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
@ -522,12 +559,32 @@ func _create_ability_bar() -> void:
|
|||||||
timer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
timer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
_ability_bar.add_child(button)
|
_ability_bar.add_child(button)
|
||||||
_ability_buttons[index] = button
|
_ability_buttons[index] = button
|
||||||
|
var passive := Button.new()
|
||||||
|
passive.name = "PassiveSlot%d" % (index + 1)
|
||||||
|
passive.set_script(preload("res://scripts/ui/captain_skill_button.gd"))
|
||||||
|
passive.call("configure", null, Color("85e5b6"))
|
||||||
|
passive.disabled = true
|
||||||
|
passive.mouse_default_cursor_shape = Control.CURSOR_HELP
|
||||||
|
_passive_ship_icons.add_child(passive)
|
||||||
|
_passive_ship_buttons[index] = passive
|
||||||
_update_ability_bar()
|
_update_ability_bar()
|
||||||
|
|
||||||
func _update_ability_bar() -> void:
|
func _update_ability_bar() -> void:
|
||||||
var any_equipped := false
|
var any_equipped := false
|
||||||
|
var any_passive := false
|
||||||
for index in _ability_buttons:
|
for index in _ability_buttons:
|
||||||
var button: Button = _ability_buttons[index]
|
var button: Button = _ability_buttons[index]
|
||||||
|
var equipped: ItemInstance = player.get_node("ShipInventory").get_item("extension", index)
|
||||||
|
var passive: Button = _passive_ship_buttons[index]
|
||||||
|
passive.visible = equipped != null and equipped.is_passive_extension()
|
||||||
|
if passive.visible:
|
||||||
|
any_passive = true
|
||||||
|
button.hide()
|
||||||
|
passive.call("configure", equipped.definition.icon, Color("85e5b6"))
|
||||||
|
passive.call("set_visual_state", 0.0, false, false)
|
||||||
|
passive.tooltip_text = equipped.get_tooltip_text()
|
||||||
|
continue
|
||||||
|
(button.get_node("Shortcut") as Label).text = str(index + 1)
|
||||||
var ability: ActiveAbility = player.call("get_active_ability", index)
|
var ability: ActiveAbility = player.call("get_active_ability", index)
|
||||||
button.visible = ability != null
|
button.visible = ability != null
|
||||||
if ability == null:
|
if ability == null:
|
||||||
@ -542,6 +599,7 @@ func _update_ability_bar() -> void:
|
|||||||
(button.get_node("Cooldown") as Label).text = str(ceili(cooldown)) if cooldown > 0.0 else ""
|
(button.get_node("Cooldown") as Label).text = str(ceili(cooldown)) if cooldown > 0.0 else ""
|
||||||
button.tooltip_text = "[%d] %s\n" % [index + 1, ability.display_name] + ability.description + ("\n" + reason if not reason.is_empty() else "\nGotowe")
|
button.tooltip_text = "[%d] %s\n" % [index + 1, ability.display_name] + ability.description + ("\n" + reason if not reason.is_empty() else "\nGotowe")
|
||||||
_ability_bar.get_parent().visible = any_equipped
|
_ability_bar.get_parent().visible = any_equipped
|
||||||
|
_passive_ship_panel.visible = any_passive
|
||||||
|
|
||||||
func _refresh_passive_skills() -> void:
|
func _refresh_passive_skills() -> void:
|
||||||
if not is_instance_valid(_passive_skills):
|
if not is_instance_valid(_passive_skills):
|
||||||
@ -562,5 +620,6 @@ func _refresh_passive_skills() -> void:
|
|||||||
icon.mouse_default_cursor_shape = Control.CURSOR_HELP
|
icon.mouse_default_cursor_shape = Control.CURSOR_HELP
|
||||||
icon.tooltip_text = "%s · poziom %d / 3\n%s" % [catalog.DATA[skill]["name"], rank, catalog.description(skill, rank)]
|
icon.tooltip_text = "%s · poziom %d / 3\n%s" % [catalog.DATA[skill]["name"], rank, catalog.description(skill, rank)]
|
||||||
_passive_skills.add_child(icon)
|
_passive_skills.add_child(icon)
|
||||||
_passive_skills.visible = _passive_skills.get_child_count() > 0
|
_passive_captain_panel.visible = _passive_skills.get_child_count() > 0
|
||||||
|
_layout_passive_panels.call_deferred()
|
||||||
_layout_debug.call_deferred()
|
_layout_debug.call_deferred()
|
||||||
|
|||||||
@ -180,6 +180,47 @@ func restore_shields_from_damage(actual_damage: float) -> void:
|
|||||||
shields = minf(max_shields, shields + actual_damage * shield_on_damage_percent / 100.0)
|
shields = minf(max_shields, shields + actual_damage * shield_on_damage_percent / 100.0)
|
||||||
_update_shield_visual()
|
_update_shield_visual()
|
||||||
|
|
||||||
|
func get_passive_extension_value(effect: String) -> float:
|
||||||
|
var best := 0.0
|
||||||
|
for item in $ShipInventory.extension_slots:
|
||||||
|
if item != null and item.is_passive_extension() and item.definition.passive_effect == effect:
|
||||||
|
best = maxf(best, item.get_passive_value())
|
||||||
|
return best
|
||||||
|
|
||||||
|
func recycle_asteroid_fragment() -> void:
|
||||||
|
if _is_destroyed:
|
||||||
|
return
|
||||||
|
var fraction := get_passive_extension_value("recycler") / 100.0
|
||||||
|
if fraction <= 0.0:
|
||||||
|
return
|
||||||
|
energy = minf(max_energy, energy + max_energy * fraction)
|
||||||
|
if not GameState.captain.is_skill_active("power_redirect"):
|
||||||
|
shields = minf(max_shields, shields + max_shields * fraction)
|
||||||
|
_update_shield_visual()
|
||||||
|
|
||||||
|
func try_control_enemy(enemy: CharacterBody2D) -> bool:
|
||||||
|
var limit := int(get_passive_extension_value("virus"))
|
||||||
|
if limit <= 0 or _is_destroyed:
|
||||||
|
return false
|
||||||
|
var controlled := 0
|
||||||
|
for ally in get_tree().get_nodes_in_group("virus_allies"):
|
||||||
|
if not ally.is_queued_for_deletion() and float(ally.get("hull")) > 0.0:
|
||||||
|
controlled += 1
|
||||||
|
if controlled >= limit:
|
||||||
|
return false
|
||||||
|
enemy.call("set_virus_controlled", true)
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _sync_virus_allies() -> void:
|
||||||
|
var remaining := int(get_passive_extension_value("virus")) if not _is_destroyed else 0
|
||||||
|
for ally in get_tree().get_nodes_in_group("virus_allies"):
|
||||||
|
if ally.is_queued_for_deletion():
|
||||||
|
continue
|
||||||
|
if remaining > 0:
|
||||||
|
remaining -= 1
|
||||||
|
else:
|
||||||
|
ally.call("set_virus_controlled", false)
|
||||||
|
|
||||||
func get_evasion_chance() -> float:
|
func get_evasion_chance() -> float:
|
||||||
return GameState.captain.get_skill_bonus("tactical_maneuver") if GameState.captain.is_skill_active("tactical_maneuver") else 0.0
|
return GameState.captain.get_skill_bonus("tactical_maneuver") if GameState.captain.is_skill_active("tactical_maneuver") else 0.0
|
||||||
|
|
||||||
@ -187,7 +228,11 @@ func get_energy_regeneration() -> float:
|
|||||||
return (energy_regeneration + get_solar_regeneration()) * GameState.captain.energy_regeneration_multiplier()
|
return (energy_regeneration + get_solar_regeneration()) * GameState.captain.energy_regeneration_multiplier()
|
||||||
|
|
||||||
func get_damage_reduction() -> float:
|
func get_damage_reduction() -> float:
|
||||||
return GameState.captain.get_skill_bonus("mechanic")
|
var reduction := GameState.captain.get_skill_bonus("mechanic")
|
||||||
|
var threshold := get_passive_extension_value("titanium_armor") / 100.0
|
||||||
|
if threshold > 0.0 and hull / maxf(1.0, max_hull) <= threshold:
|
||||||
|
reduction = 1.0 - (1.0 - reduction) * 0.5
|
||||||
|
return reduction
|
||||||
|
|
||||||
func get_weapon_damage_multiplier() -> float:
|
func get_weapon_damage_multiplier() -> float:
|
||||||
return damage_multiplier * (1.0 + GameState.captain.get_skill_bonus("gunner"))
|
return damage_multiplier * (1.0 + GameState.captain.get_skill_bonus("gunner"))
|
||||||
@ -269,6 +314,7 @@ func _restart_level_after_destruction() -> void:
|
|||||||
get_tree().reload_current_scene.call_deferred()
|
get_tree().reload_current_scene.call_deferred()
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
|
_sync_virus_allies()
|
||||||
if not _is_destroyed:
|
if not _is_destroyed:
|
||||||
GameState.captain.tick_skills(delta)
|
GameState.captain.tick_skills(delta)
|
||||||
if GameState.captain.is_skill_active("power_redirect"):
|
if GameState.captain.is_skill_active("power_redirect"):
|
||||||
|
|||||||
@ -61,6 +61,8 @@ func configure(
|
|||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
var travel_velocity := direction * projectile_speed
|
var travel_velocity := direction * projectile_speed
|
||||||
if weapon_kind == ItemDefinition.WeaponKind.MISSILE and missile_homing:
|
if weapon_kind == ItemDefinition.WeaponKind.MISSILE and missile_homing:
|
||||||
|
if is_instance_valid(homing_target) and homing_target.is_in_group("virus_allies") and not enemy_shot:
|
||||||
|
homing_target = null
|
||||||
_flight_time += delta
|
_flight_time += delta
|
||||||
if not is_instance_valid(homing_target):
|
if not is_instance_valid(homing_target):
|
||||||
_has_locked_target = false
|
_has_locked_target = false
|
||||||
@ -279,7 +281,9 @@ func _explode_at_target(impact_target: Node) -> void:
|
|||||||
var query := PhysicsShapeQueryParameters2D.new()
|
var query := PhysicsShapeQueryParameters2D.new()
|
||||||
query.shape = explosion_shape
|
query.shape = explosion_shape
|
||||||
query.transform = Transform2D(0.0, global_position)
|
query.transform = Transform2D(0.0, global_position)
|
||||||
query.collision_mask = 2 if enemy_shot else 4
|
# Converted ships fire at hostiles; hostile blasts can also hit converted ships.
|
||||||
|
var converted_shot := enemy_shot and (collision_mask & 4) != 0
|
||||||
|
query.collision_mask = 4 if converted_shot or not enemy_shot else 66
|
||||||
for result in get_world_2d().direct_space_state.intersect_shape(query, 32):
|
for result in get_world_2d().direct_space_state.intersect_shape(query, 32):
|
||||||
_apply_damage_once(result.get("collider"))
|
_apply_damage_once(result.get("collider"))
|
||||||
|
|
||||||
@ -292,7 +296,9 @@ func _apply_damage_once(target: Variant) -> void:
|
|||||||
if _already_hit.has(instance_id):
|
if _already_hit.has(instance_id):
|
||||||
return
|
return
|
||||||
_already_hit[instance_id] = true
|
_already_hit[instance_id] = true
|
||||||
if target.has_method("take_weapon_hit"):
|
if enemy_shot and target is CharacterBody2D and target.has_method("take_damage"):
|
||||||
|
target.call("take_damage", damage, is_critical, DamageTypes.for_weapon(weapon_kind))
|
||||||
|
elif target.has_method("take_weapon_hit"):
|
||||||
target.call("take_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind))
|
target.call("take_weapon_hit", damage, is_critical, DamageTypes.for_weapon(weapon_kind))
|
||||||
elif target.has_method("take_damage"):
|
elif target.has_method("take_damage"):
|
||||||
if target is CharacterBody2D:
|
if target is CharacterBody2D:
|
||||||
|
|||||||
@ -27,6 +27,7 @@ func get_damage_type() -> int:
|
|||||||
@export_range(0.0, 1.0, 0.05) var energy_damage_exponent: float = 0.5
|
@export_range(0.0, 1.0, 0.05) var energy_damage_exponent: float = 0.5
|
||||||
@export_range(0.0, 1.0, 0.01) var energy_per_addon: float = 0.05
|
@export_range(0.0, 1.0, 0.01) var energy_per_addon: float = 0.05
|
||||||
@export var active_ability: ActiveAbility
|
@export var active_ability: ActiveAbility
|
||||||
|
@export var passive_effect: String = ""
|
||||||
|
|
||||||
@export_group("Losowanie zakresu obrażeń broni (poziom 1)")
|
@export_group("Losowanie zakresu obrażeń broni (poziom 1)")
|
||||||
@export_range(1, 10000) var damage_lower_roll_min: int = 1
|
@export_range(1, 10000) var damage_lower_roll_min: int = 1
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const DEFINITIONS := [
|
const DEFINITIONS := [
|
||||||
|
preload("res://resources/items/recycler.tres"),
|
||||||
|
preload("res://resources/items/virus.tres"),
|
||||||
|
preload("res://resources/items/titanium_armor.tres"),
|
||||||
preload("res://resources/items/combat_drones.tres"),
|
preload("res://resources/items/combat_drones.tres"),
|
||||||
preload("res://resources/items/teleport.tres"),
|
preload("res://resources/items/teleport.tres"),
|
||||||
preload("res://resources/items/cloak.tres"),
|
preload("res://resources/items/cloak.tres"),
|
||||||
@ -49,7 +52,10 @@ static func generate_for_definition(definition: ItemDefinition, level: int, enem
|
|||||||
item.primary_value_override = (item.damage_min + item.damage_max) * 0.5
|
item.primary_value_override = (item.damage_min + item.damage_max) * 0.5
|
||||||
item.energy_regeneration_override = item.definition.energy_regeneration_bonus * scale_factor * randf_range(0.8, 1.2)
|
item.energy_regeneration_override = item.definition.energy_regeneration_bonus * scale_factor * randf_range(0.8, 1.2)
|
||||||
if definition.category == ItemDefinition.Category.ACTIVE_EXTENSION:
|
if definition.category == ItemDefinition.Category.ACTIVE_EXTENSION:
|
||||||
item.extension_cooldown = ItemInstance.EXTENSION_COOLDOWNS.roll_seconds()
|
if item.is_passive_extension():
|
||||||
|
item.passive_value = preload("res://scripts/items/passive_extension_rolls.gd").roll_value(definition.passive_effect)
|
||||||
|
else:
|
||||||
|
item.extension_cooldown = ItemInstance.EXTENSION_COOLDOWNS.roll_seconds()
|
||||||
return item
|
return item
|
||||||
var roll := randf()
|
var roll := randf()
|
||||||
var count := 1 if roll < 0.5 else (2 if roll < 0.75 else (3 if roll < 0.89 else (4 if roll < 0.97 else 5)))
|
var count := 1 if roll < 0.5 else (2 if roll < 0.75 else (3 if roll < 0.89 else (4 if roll < 0.97 else 5)))
|
||||||
|
|||||||
@ -6,6 +6,28 @@ const EXTENSION_COOLDOWNS := preload("res://scripts/items/extension_cooldowns.gd
|
|||||||
# Exported with the item; old items default to 30 seconds without rerolling.
|
# Exported with the item; old items default to 30 seconds without rerolling.
|
||||||
@export_range(5, 30, 1) var extension_cooldown: int = 30
|
@export_range(5, 30, 1) var extension_cooldown: int = 30
|
||||||
var _instance_ability: ActiveAbility
|
var _instance_ability: ActiveAbility
|
||||||
|
@export var passive_value: float = -1.0
|
||||||
|
|
||||||
|
func is_passive_extension() -> bool:
|
||||||
|
return definition != null and definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and not definition.passive_effect.is_empty()
|
||||||
|
|
||||||
|
func get_passive_value() -> float:
|
||||||
|
if not is_passive_extension():
|
||||||
|
return 0.0
|
||||||
|
match definition.passive_effect:
|
||||||
|
"recycler": return clampf(passive_value, 0.5, 40.0)
|
||||||
|
"virus": return clampf(floorf(passive_value), 1.0, 20.0)
|
||||||
|
"titanium_armor": return clampf(passive_value, 5.0, 50.0)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
func get_passive_description() -> String:
|
||||||
|
var value := get_passive_value()
|
||||||
|
match definition.passive_effect:
|
||||||
|
"recycler": return "Każdy zebrany odłamek asteroidy odnawia %.1f%% maksymalnych tarcz i energii." % value
|
||||||
|
"virus": return "Przejmuje uciekających wrogów (poniżej 10%% kadłuba). Zamiast uciekać atakują swoich dawnych sojuszników. Limit jednocześnie kontrolowanych: %d." % int(value)
|
||||||
|
"titanium_armor": return "Pochłania 50%% otrzymywanych obrażeń, gdy kadłub ma nie więcej niż %d%% maksimum. Próg sprawdzany przed trafieniem." % int(value)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
func get_active_ability() -> ActiveAbility:
|
func get_active_ability() -> ActiveAbility:
|
||||||
if definition == null or definition.active_ability == null:
|
if definition == null or definition.active_ability == null:
|
||||||
@ -24,6 +46,8 @@ func get_active_ability() -> ActiveAbility:
|
|||||||
description = fixed_cooldown.sub(description, "", true)
|
description = fixed_cooldown.sub(description, "", true)
|
||||||
if definition.item_id == "mine_layer":
|
if definition.item_id == "mine_layer":
|
||||||
description = "Zostawia minę na 60 s. Uzbraja się po 0,7 s. Wykrywa bliski przelot wroga. Wybuch w promieniu 140 jednostek zadaje każdemu wrogowi 50% aktualnego kadłuba bez redukcji absorpcji (tarcze pochłaniają obrażenia) i spowalnia o 50% na 5 s."
|
description = "Zostawia minę na 60 s. Uzbraja się po 0,7 s. Wykrywa bliski przelot wroga. Wybuch w promieniu 140 jednostek zadaje każdemu wrogowi 50% aktualnego kadłuba bez redukcji absorpcji (tarcze pochłaniają obrażenia) i spowalnia o 50% na 5 s."
|
||||||
|
if definition.item_id == "afterburner":
|
||||||
|
description = "Szybko rozpędza do 300% zwykłej prędkości (+200%), następnie wytraca nadmiar prędkości. Bez kosztu energii."
|
||||||
_instance_ability.description = description + "\nOdnowienie: %d s." % _instance_ability.cooldown
|
_instance_ability.description = description + "\nOdnowienie: %d s." % _instance_ability.cooldown
|
||||||
return _instance_ability
|
return _instance_ability
|
||||||
|
|
||||||
@ -122,7 +146,7 @@ func get_type_color() -> Color:
|
|||||||
ItemDefinition.Category.WEAPON:
|
ItemDefinition.Category.WEAPON:
|
||||||
return Color("ae74ef")
|
return Color("ae74ef")
|
||||||
ItemDefinition.Category.ACTIVE_EXTENSION:
|
ItemDefinition.Category.ACTIVE_EXTENSION:
|
||||||
return Color("f39c4d")
|
return Color("85e5b6") if is_passive_extension() else Color("f39c4d")
|
||||||
return Color("65ce8b")
|
return Color("65ce8b")
|
||||||
|
|
||||||
func get_tooltip_text() -> String:
|
func get_tooltip_text() -> String:
|
||||||
@ -133,6 +157,8 @@ func get_tooltip_text() -> String:
|
|||||||
var primary_text := "%s: +%.1f%s" % [stat_name(definition.primary_stat), get_stat_total(definition.primary_stat), definition.stat_suffix]
|
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:
|
if definition.category == ItemDefinition.Category.WEAPON:
|
||||||
primary_text = "Obrażenia: %s" % get_damage_range_text()
|
primary_text = "Obrażenia: %s" % get_damage_range_text()
|
||||||
|
elif is_passive_extension():
|
||||||
|
primary_text = get_passive_description() + "\nPASYWNE • Bez kosztu i cooldownu.\nDuplikaty nie sumują efektu — działa najlepszy egzemplarz."
|
||||||
elif definition.active_ability != null:
|
elif definition.active_ability != null:
|
||||||
primary_text = get_active_ability().description
|
primary_text = get_active_ability().description
|
||||||
var lines: Array[String] = [definition.display_name, "%s • Poziom %d" % [get_rarity_name(), item_level], "", definition.description, "", "CECHA GŁÓWNA", primary_text]
|
var lines: Array[String] = [definition.display_name, "%s • Poziom %d" % [get_rarity_name(), item_level], "", definition.description, "", "CECHA GŁÓWNA", primary_text]
|
||||||
|
|||||||
@ -32,6 +32,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
if distance <= PICKUP_RADIUS:
|
if distance <= PICKUP_RADIUS:
|
||||||
_collected = true
|
_collected = true
|
||||||
GameState.captain.minerals[mineral] = int(GameState.captain.minerals.get(mineral, 0)) + 1
|
GameState.captain.minerals[mineral] = int(GameState.captain.minerals.get(mineral, 0)) + 1
|
||||||
|
_player.call("recycle_asteroid_fragment")
|
||||||
GameState.captain.emit_changed()
|
GameState.captain.emit_changed()
|
||||||
var popup := preload("res://scenes/game/damage_popup.tscn").instantiate() as Label
|
var popup := preload("res://scenes/game/damage_popup.tscn").instantiate() as Label
|
||||||
popup.text = "+1 %s" % {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu", "sulfur": "siarki"}.get(mineral, mineral)
|
popup.text = "+1 %s" % {"iron": "żelaza", "aluminium": "aluminium", "titanium": "tytanu", "uranium": "uranu", "sulfur": "siarki"}.get(mineral, mineral)
|
||||||
|
|||||||
22
scripts/items/passive_extension_rolls.gd
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const CHANCES := [1.0, 0.25, 0.05, 0.01, 0.0015, 0.0002]
|
||||||
|
|
||||||
|
static func tail_chance(quality: float) -> float:
|
||||||
|
var scaled := clampf(quality, 0.0, 1.0) * 5.0
|
||||||
|
var index := mini(4, floori(scaled))
|
||||||
|
return exp(lerpf(log(CHANCES[index]), log(CHANCES[index + 1]), scaled - index))
|
||||||
|
|
||||||
|
static func rank_from_roll(steps: int, roll: float) -> int:
|
||||||
|
for rank in range(steps, 0, -1):
|
||||||
|
if roll < tail_chance(float(rank) / steps):
|
||||||
|
return rank
|
||||||
|
return 0
|
||||||
|
|
||||||
|
static func roll_value(effect: String, roll: float = -1.0) -> float:
|
||||||
|
var chance := randf() if roll < 0.0 else roll
|
||||||
|
match effect:
|
||||||
|
"recycler": return 0.5 + rank_from_roll(79, chance) * 0.5
|
||||||
|
"virus": return 1.0 + rank_from_roll(19, chance)
|
||||||
|
"titanium_armor": return 5.0 + rank_from_roll(45, chance)
|
||||||
|
return 0.0
|
||||||
1
scripts/items/passive_extension_rolls.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://qyharawiegof
|
||||||
@ -218,7 +218,7 @@ func _accepts_item(kind: String, index: int, item: ItemInstance) -> bool:
|
|||||||
if kind == "module":
|
if kind == "module":
|
||||||
return item.definition.category == MODULE_CATEGORIES[index]
|
return item.definition.category == MODULE_CATEGORIES[index]
|
||||||
if kind == "extension":
|
if kind == "extension":
|
||||||
return item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and item.definition.active_ability != null
|
return item.definition.category == ItemDefinition.Category.ACTIVE_EXTENSION and (item.definition.active_ability != null or item.is_passive_extension())
|
||||||
return kind == "cargo"
|
return kind == "cargo"
|
||||||
|
|
||||||
func get_missile_count() -> int:
|
func get_missile_count() -> int:
|
||||||
|
|||||||
@ -24,12 +24,12 @@ func _ready() -> void:
|
|||||||
_screen.hide()
|
_screen.hide()
|
||||||
get_parent().get_node("HUDRoot/FloatingWindows").set("modal_overlay", _screen)
|
get_parent().get_node("HUDRoot/FloatingWindows").set("modal_overlay", _screen)
|
||||||
var dim := ColorRect.new()
|
var dim := ColorRect.new()
|
||||||
dim.color = Color(0.015, 0.025, 0.045, 0.9)
|
dim.color = Color(0.015, 0.025, 0.045, 0.3)
|
||||||
_screen.add_child(dim)
|
_screen.add_child(dim)
|
||||||
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
_panel = PanelContainer.new()
|
_panel = PanelContainer.new()
|
||||||
_panel.name = "Panel"
|
_panel.name = "Panel"
|
||||||
_panel.add_theme_stylebox_override("panel", _style(Color("567b99"), Color("0a1422"), 22))
|
_panel.add_theme_stylebox_override("panel", _style(Color("567b99"), Color("0a1422c7"), 22))
|
||||||
_screen.add_child(_panel)
|
_screen.add_child(_panel)
|
||||||
_panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
_panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||||
_contents = VBoxContainer.new()
|
_contents = VBoxContainer.new()
|
||||||
|
|||||||
@ -343,17 +343,21 @@ func _get_drag_data(_at_position: Vector2) -> Variant:
|
|||||||
var item: ItemInstance = _inventory.get_item(slot_kind, slot_index)
|
var item: ItemInstance = _inventory.get_item(slot_kind, slot_index)
|
||||||
if item == null:
|
if item == null:
|
||||||
return null
|
return null
|
||||||
if item.definition.icon != null:
|
# Native preview lives in the viewport's base canvas, below HUD CanvasLayers.
|
||||||
var preview := TextureRect.new()
|
# Keep its drag lifecycle, but draw the centered icon in a dedicated top layer.
|
||||||
preview.texture = item.definition.icon
|
var overlay := get_tree().get_first_node_in_group("item_drag_overlay")
|
||||||
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
if overlay == null:
|
||||||
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
overlay = CanvasLayer.new()
|
||||||
preview.custom_minimum_size = Vector2(64, 64)
|
overlay.name = "ItemDragOverlay"
|
||||||
set_drag_preview(preview)
|
overlay.set_script(preload("res://scripts/ui/item_drag_overlay.gd"))
|
||||||
else:
|
var host := get_tree().current_scene
|
||||||
var preview := Label.new()
|
if host == null:
|
||||||
preview.text = item.definition.display_name
|
host = get_viewport()
|
||||||
set_drag_preview(preview)
|
host.add_child(overlay)
|
||||||
|
overlay.call("begin_drag", item)
|
||||||
|
var native_preview := Control.new()
|
||||||
|
native_preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
set_drag_preview(native_preview)
|
||||||
return {"from_kind": slot_kind, "from_index": slot_index}
|
return {"from_kind": slot_kind, "from_index": slot_index}
|
||||||
|
|
||||||
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
|
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
|
||||||
|
|||||||
24
scripts/ui/flight_cursor.gd
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
const AIM_TEXTURE := preload("res://assets/icons/ui/aim_cursor.svg")
|
||||||
|
const HOTSPOT := Vector2(24, 24)
|
||||||
|
var player: CharacterBody2D
|
||||||
|
var aiming := false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
|
||||||
|
func should_aim() -> bool:
|
||||||
|
return is_instance_valid(player) and not bool(player.get("_is_destroyed")) and not bool(player.get("is_docked")) and not get_tree().paused and not get_viewport().gui_is_dragging() and get_viewport().gui_get_hovered_control() == null
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
var next := should_aim()
|
||||||
|
if next == aiming:
|
||||||
|
return
|
||||||
|
aiming = next
|
||||||
|
# A hardware cursor keeps the hotspot exactly on the weapon aim point.
|
||||||
|
Input.set_custom_mouse_cursor(AIM_TEXTURE if aiming else null, Input.CURSOR_ARROW, HOTSPOT if aiming else Vector2.ZERO)
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
if aiming:
|
||||||
|
Input.set_custom_mouse_cursor(null, Input.CURSOR_ARROW)
|
||||||
1
scripts/ui/flight_cursor.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://ct7b1pxcenj3l
|
||||||
67
scripts/ui/item_drag_overlay.gd
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
const ICON_SIZE := Vector2(64, 64)
|
||||||
|
var _view: PanelContainer
|
||||||
|
var _picture: TextureRect
|
||||||
|
var _fallback: Label
|
||||||
|
var _started_frame := -1
|
||||||
|
var _pointer_position := Vector2.ZERO
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
layer = preload("res://scripts/ui/render_layers.gd").DRAG_PREVIEW
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
add_to_group("item_drag_overlay")
|
||||||
|
_view = PanelContainer.new()
|
||||||
|
_view.name = "DragIcon"
|
||||||
|
_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_view.size = ICON_SIZE
|
||||||
|
add_child(_view)
|
||||||
|
_picture = TextureRect.new()
|
||||||
|
_picture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
_picture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
_picture.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
|
||||||
|
_picture.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_view.add_child(_picture)
|
||||||
|
_fallback = Label.new()
|
||||||
|
_fallback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_fallback.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_fallback.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_fallback.add_theme_font_size_override("font_size", 10)
|
||||||
|
_view.add_child(_fallback)
|
||||||
|
_view.hide()
|
||||||
|
|
||||||
|
func begin_drag(item: ItemInstance) -> void:
|
||||||
|
_started_frame = Engine.get_process_frames()
|
||||||
|
_pointer_position = get_viewport().get_mouse_position()
|
||||||
|
_picture.texture = item.definition.icon
|
||||||
|
_fallback.text = item.definition.display_name if item.definition.icon == null else ""
|
||||||
|
_fallback.visible = item.definition.icon == null
|
||||||
|
var frame := StyleBoxFlat.new()
|
||||||
|
frame.bg_color = Color(0.025, 0.045, 0.065, 0.8)
|
||||||
|
frame.border_color = item.get_type_color()
|
||||||
|
frame.set_border_width_all(1)
|
||||||
|
frame.set_corner_radius_all(5)
|
||||||
|
frame.set_content_margin_all(5)
|
||||||
|
frame.shadow_color = Color(0, 0, 0, 0.45)
|
||||||
|
frame.shadow_size = 4
|
||||||
|
_view.add_theme_stylebox_override("panel", frame)
|
||||||
|
_view.size = ICON_SIZE
|
||||||
|
_view.show()
|
||||||
|
_update_position()
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if _view.visible and (event is InputEventMouseMotion or event is InputEventMouseButton):
|
||||||
|
_pointer_position = event.position
|
||||||
|
_update_position()
|
||||||
|
|
||||||
|
func _update_position() -> void:
|
||||||
|
_view.position = _pointer_position - _view.size * 0.5
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if not _view.visible:
|
||||||
|
return
|
||||||
|
if get_viewport().gui_is_dragging():
|
||||||
|
_update_position()
|
||||||
|
elif Engine.get_process_frames() > _started_frame + 1:
|
||||||
|
_view.hide()
|
||||||
|
_picture.texture = null
|
||||||
1
scripts/ui/item_drag_overlay.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://chsbheqg4n8m4
|
||||||
@ -9,6 +9,74 @@ var _ship_panel: Control
|
|||||||
var _panel_open := false
|
var _panel_open := false
|
||||||
var _launching := false
|
var _launching := false
|
||||||
var _drawer_tween: Tween
|
var _drawer_tween: Tween
|
||||||
|
var _guide: Control
|
||||||
|
var _instruction_button: Button
|
||||||
|
|
||||||
|
func _style_menu() -> void:
|
||||||
|
$Background.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
$Shade.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var atmosphere := Control.new()
|
||||||
|
atmosphere.name = "MenuAtmosphere"
|
||||||
|
atmosphere.set_script(preload("res://scripts/ui/menu_atmosphere.gd"))
|
||||||
|
add_child(atmosphere)
|
||||||
|
move_child(atmosphere, $MenuLayout.get_index())
|
||||||
|
var layout := $MenuLayout as VBoxContainer
|
||||||
|
layout.add_theme_constant_override("separation", 10)
|
||||||
|
$MenuLayout/Spacer.hide()
|
||||||
|
var eyebrow := Label.new()
|
||||||
|
eyebrow.text = "DZIENNIK POKŁADOWY / NOWA WYPRAWA"
|
||||||
|
eyebrow.add_theme_font_override("font", ThemeDB.fallback_font)
|
||||||
|
eyebrow.add_theme_font_size_override("font_size", 11)
|
||||||
|
eyebrow.add_theme_color_override("font_color", Color("91dbea"))
|
||||||
|
layout.add_child(eyebrow)
|
||||||
|
layout.move_child(eyebrow, 0)
|
||||||
|
var title := $MenuLayout/Title as Label
|
||||||
|
title.add_theme_font_size_override("font_size", 46)
|
||||||
|
title.add_theme_color_override("font_color", Color("f4ead9"))
|
||||||
|
var subtitle := Label.new()
|
||||||
|
subtitle.text = "Nieznany kosmos. Niepowtarzalny łup."
|
||||||
|
subtitle.add_theme_font_override("font", ThemeDB.fallback_font)
|
||||||
|
subtitle.add_theme_font_size_override("font_size", 15)
|
||||||
|
subtitle.add_theme_color_override("font_color", Color("a9bcc8"))
|
||||||
|
layout.add_child(subtitle)
|
||||||
|
layout.move_child(subtitle, title.get_index() + 1)
|
||||||
|
var space := Control.new()
|
||||||
|
space.custom_minimum_size.y = 10
|
||||||
|
layout.add_child(space)
|
||||||
|
layout.move_child(space, subtitle.get_index() + 1)
|
||||||
|
_instruction_button = Button.new()
|
||||||
|
_instruction_button.name = "InstructionsButton"
|
||||||
|
layout.add_child(_instruction_button)
|
||||||
|
layout.move_child(_instruction_button, start_button.get_index() + 1)
|
||||||
|
var entries := [
|
||||||
|
[start_button, "NOWA WYPRAWA", "Wybierz statek i ruszaj w nieznane", "01", true],
|
||||||
|
[_instruction_button, "INSTRUKCJA", "Pierwszy lot? Zacznij tutaj", "02", false],
|
||||||
|
[$MenuLayout/SaveButton, "ZAPIS ROZGRYWKI", "Wgraj / zapisz — wkrótce", "03", false],
|
||||||
|
[$MenuLayout/OptionsButton, "OPCJE", "Ustawienia — wkrótce", "04", false],
|
||||||
|
[exit_button, "WYJŚCIE", "Zakończ grę", "05", false],
|
||||||
|
]
|
||||||
|
for entry in entries:
|
||||||
|
var button: Button = entry[0]
|
||||||
|
button.set_script(preload("res://scripts/ui/main_menu_button.gd"))
|
||||||
|
button.call("configure", entry[1], entry[2], entry[3], entry[4])
|
||||||
|
$MenuLayout/SaveButton.disabled = true
|
||||||
|
$MenuLayout/OptionsButton.disabled = true
|
||||||
|
_guide = Control.new()
|
||||||
|
_guide.set_script(preload("res://scripts/ui/player_guide.gd"))
|
||||||
|
add_child(_guide)
|
||||||
|
_instruction_button.pressed.connect(func() -> void: _guide.call("open"))
|
||||||
|
_guide.connect("closed", _instruction_button.grab_focus)
|
||||||
|
resized.connect(_layout_menu)
|
||||||
|
_layout_menu.call_deferred()
|
||||||
|
|
||||||
|
func _layout_menu() -> void:
|
||||||
|
var layout := $MenuLayout as VBoxContainer
|
||||||
|
layout.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
|
||||||
|
var factor := minf(1.0, minf((size.y - 120.0) / 520.0, (size.x - 64.0) / 420.0))
|
||||||
|
layout.scale = Vector2.ONE * factor
|
||||||
|
layout.size = Vector2(420, layout.get_combined_minimum_size().y)
|
||||||
|
layout.position = Vector2(maxf(32.0, size.x * 0.065), maxf(68.0, (size.y - layout.size.y * factor) * 0.5))
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
start_button.pressed.connect(_start_game)
|
start_button.pressed.connect(_start_game)
|
||||||
@ -22,6 +90,7 @@ func _ready() -> void:
|
|||||||
_ship_panel.connect("close_requested", _close_ship_panel)
|
_ship_panel.connect("close_requested", _close_ship_panel)
|
||||||
resized.connect(_layout_ship_panel)
|
resized.connect(_layout_ship_panel)
|
||||||
_layout_ship_panel()
|
_layout_ship_panel()
|
||||||
|
_style_menu()
|
||||||
|
|
||||||
func _start_game() -> void:
|
func _start_game() -> void:
|
||||||
if _panel_open or _launching:
|
if _panel_open or _launching:
|
||||||
|
|||||||
55
scripts/ui/main_menu_button.gd
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
extends Button
|
||||||
|
|
||||||
|
const DISPLAY_FONT := preload("res://assets/fonts/font2.ttf")
|
||||||
|
var caption := ""
|
||||||
|
var detail := ""
|
||||||
|
var symbol := "01"
|
||||||
|
var primary := false
|
||||||
|
var _hover := 0.0
|
||||||
|
|
||||||
|
func configure(title: String, subtitle: String, marker: String, main: bool = false) -> void:
|
||||||
|
caption = title
|
||||||
|
detail = subtitle
|
||||||
|
symbol = marker
|
||||||
|
primary = main
|
||||||
|
text = ""
|
||||||
|
custom_minimum_size = Vector2(280, 72 if primary else 60)
|
||||||
|
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
for state in ["normal", "hover", "pressed", "focus", "disabled"]:
|
||||||
|
add_theme_stylebox_override(state, StyleBoxEmpty.new())
|
||||||
|
tooltip_text = title
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
var target := 1.0 if (is_hovered() or has_focus()) and not disabled else 0.0
|
||||||
|
var next := move_toward(_hover, target, delta * 6.0)
|
||||||
|
if next != _hover:
|
||||||
|
_hover = next
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var accent := Color("f1bd79") if primary else Color("86d5e8")
|
||||||
|
if disabled:
|
||||||
|
accent = Color("607180")
|
||||||
|
var inset := 2.0 if button_pressed else 0.0
|
||||||
|
var rect := Rect2(Vector2(inset, inset), size - Vector2.ONE * inset * 2)
|
||||||
|
var points := PackedVector2Array([rect.position, Vector2(rect.end.x - 14, rect.position.y), Vector2(rect.end.x, rect.position.y + 14), rect.end, Vector2(rect.position.x + 10, rect.end.y), Vector2(rect.position.x, rect.end.y - 10)])
|
||||||
|
var base := Color(0.028, 0.067, 0.10, 0.88).lerp(Color(0.09, 0.19, 0.25, 0.96), _hover)
|
||||||
|
if primary:
|
||||||
|
base = Color(0.17, 0.12, 0.075, 0.92).lerp(Color(0.29, 0.20, 0.105, 0.98), _hover)
|
||||||
|
draw_colored_polygon(points, base)
|
||||||
|
var edge := points.duplicate()
|
||||||
|
edge.append(points[0])
|
||||||
|
draw_polyline(edge, Color(accent, 0.32 + _hover * 0.6), 1.0, true)
|
||||||
|
draw_line(Vector2(1, 10), Vector2(1, size.y - 10), Color(accent, 0.85), 3.0, true)
|
||||||
|
draw_line(Vector2(16, 1), Vector2(68 + _hover * 70, 1), Color(accent, 0.7), 1.0, true)
|
||||||
|
var body_font := ThemeDB.fallback_font
|
||||||
|
draw_string(body_font, Vector2(18, size.y * 0.5 + 5), symbol, HORIZONTAL_ALIGNMENT_LEFT, -1, 14, Color(accent, 0.65))
|
||||||
|
draw_line(Vector2(47, 17), Vector2(47, size.y - 17), Color(accent, 0.22), 1)
|
||||||
|
var label_color := Color("f4ead9") if primary else Color("e0edf3")
|
||||||
|
if disabled:
|
||||||
|
label_color = Color("82909d")
|
||||||
|
draw_string(DISPLAY_FONT, Vector2(62 + _hover * 3, 29 if primary else 24), caption, HORIZONTAL_ALIGNMENT_LEFT, -1, 17 if primary else 14, label_color)
|
||||||
|
draw_string(body_font, Vector2(62, 50 if primary else 43), detail, HORIZONTAL_ALIGNMENT_LEFT, size.x - 104, 12, Color("a1acb9") if disabled else Color("a9bcc8"))
|
||||||
|
var tip := Vector2(size.x - 23 + _hover * 3, size.y * 0.5)
|
||||||
|
draw_polyline(PackedVector2Array([tip + Vector2(-5, -5), tip, tip + Vector2(-5, 5)]), Color(accent, 0.5 + _hover * 0.5), 1.5, true)
|
||||||
1
scripts/ui/main_menu_button.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://bmb2i1t505ol4
|
||||||
15
scripts/ui/menu_atmosphere.gd
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
resized.connect(queue_redraw)
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var reach := minf(size.x * 0.72, 850.0)
|
||||||
|
draw_polygon(PackedVector2Array([Vector2.ZERO, Vector2(reach, 0), Vector2(reach, size.y), Vector2(0, size.y)]), PackedColorArray([Color(0.012, 0.024, 0.042, 0.94), Color(0.012, 0.024, 0.042, 0), Color(0.012, 0.024, 0.042, 0), Color(0.012, 0.024, 0.042, 0.94)]))
|
||||||
|
draw_line(Vector2(32, 36), Vector2(90, 36), Color("91dbea"), 2)
|
||||||
|
draw_line(Vector2(32, 36), Vector2(32, 62), Color(0.57, 0.86, 0.92, 0.4), 1)
|
||||||
|
draw_line(Vector2(32, size.y - 36), Vector2(size.x - 32, size.y - 36), Color(0.57, 0.86, 0.92, 0.17), 1)
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(48, size.y - 16), "RANDOLOOT / EKSPLORUJ • WALCZ • ROZWIJAJ STATEK", HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color("91a4b7"))
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(size.x - 215, size.y - 16), "NIEZNANY UKŁAD. TWOJA WYPRAWA.", HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("829aac"))
|
||||||
1
scripts/ui/menu_atmosphere.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://ilqfl3an0hl7
|
||||||
@ -39,12 +39,8 @@ func _process(_delta: float) -> void:
|
|||||||
_objective.text = "Pokonaj bossa"
|
_objective.text = "Pokonaj bossa"
|
||||||
else:
|
else:
|
||||||
_objective.text = "Pokonaj wszystkich wrogów"
|
_objective.text = "Pokonaj wszystkich wrogów"
|
||||||
var cockpit := get_parent().get_node_or_null("Cockpit") as Control
|
var captain := get_parent().get_node_or_null("CaptainPanel") as Control
|
||||||
var available_width := cockpit.position.x - 24.0 if is_instance_valid(cockpit) else 260.0
|
if is_instance_valid(captain):
|
||||||
var panel_width := clampf(available_width, 180.0, 260.0)
|
size.x = captain.size.x
|
||||||
size.x = panel_width
|
size.y = get_combined_minimum_size().y
|
||||||
size.y = get_combined_minimum_size().y
|
position = captain.position + Vector2(0.0, captain.size.y + 8.0)
|
||||||
var bottom: float = (get_parent() as Control).size.y - 12.0
|
|
||||||
if available_width < 180.0 and is_instance_valid(cockpit):
|
|
||||||
bottom = cockpit.position.y - 64.0
|
|
||||||
position = Vector2(12.0, bottom - size.y)
|
|
||||||
|
|||||||
32
scripts/ui/passive_status_panel.gd
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
var caption := "PASYWNE"
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var frame := StyleBoxFlat.new()
|
||||||
|
frame.bg_color = Color(0.025, 0.041, 0.063, 0.55)
|
||||||
|
frame.border_color = Color(0.28, 0.48, 0.55, 0.45)
|
||||||
|
frame.set_border_width_all(1)
|
||||||
|
frame.set_corner_radius_all(6)
|
||||||
|
frame.set_content_margin_all(10)
|
||||||
|
add_theme_stylebox_override("panel", frame)
|
||||||
|
var contents := VBoxContainer.new()
|
||||||
|
contents.name = "Contents"
|
||||||
|
contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
contents.add_theme_constant_override("separation", 8)
|
||||||
|
add_child(contents)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = caption
|
||||||
|
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
title.add_theme_font_override("font", ThemeDB.fallback_font)
|
||||||
|
title.add_theme_font_size_override("font_size", 11)
|
||||||
|
title.add_theme_color_override("font_color", Color("91dbea"))
|
||||||
|
contents.add_child(title)
|
||||||
|
var icons := GridContainer.new()
|
||||||
|
icons.name = "Icons"
|
||||||
|
icons.columns = 5
|
||||||
|
icons.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
icons.add_theme_constant_override("h_separation", 6)
|
||||||
|
icons.add_theme_constant_override("v_separation", 6)
|
||||||
|
contents.add_child(icons)
|
||||||
1
scripts/ui/passive_status_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://d1pekaexu4wan
|
||||||
119
scripts/ui/player_guide.gd
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
signal closed
|
||||||
|
const BUTTON_STYLE := preload("res://scripts/ui/hud_button_style.gd")
|
||||||
|
const PAGES := [
|
||||||
|
["Pierwsza wyprawa", "01", "[font_size=26][color=#f1bd79]Twój pierwszy układ[/color][/font_size]\n\nWybierz statek przyciskiem [b]Nowa wyprawa[/b]. Każdy kadłub ma inne parametry — na początek wybierz ten, który pasuje do Twojego stylu.\n\n[color=#91dbea][b]1. Pokonaj zwykłych wrogów[/b][/color]\nOdszukuj przeciwników, rozwijaj wyposażenie i pilnuj zasobów. Aktualny cel znajdziesz w lewym dolnym rogu ekranu.\n\n[color=#91dbea][b]2. Pokonaj bossa[/b][/color]\nGdy zniszczysz pozostałych wrogów, w układzie pojawi się boss. Przygotuj zapas amunicji i sprawdź kadłub przed walką.\n\n[color=#91dbea][b]3. Znajdź zamieszkaną planetę[/b][/color]\nPo zwycięstwie podleć do zamieszkanej planety. Skorzystaj z usług i otwórz wormhole. Wleć w jego środek, żeby przejść do następnego, trudniejszego układu."],
|
||||||
|
["Sterowanie", "02", "[font_size=26][color=#f1bd79]Opanuj statek[/color][/font_size]\n\n[color=#91dbea][b]W[/b][/color] — ciąg do przodu.\n[color=#91dbea][b]S[/b][/color] — hamowanie; po zatrzymaniu lot wstecz.\n[color=#91dbea][b]A / D[/b][/color] — obracanie statku.\n[color=#91dbea][b]Q / E[/b][/color] — ruch boczny w lewo / prawo.\n[color=#91dbea][b]TAB[/b][/color] — dopalanie: większa prędkość kosztem energii.\n\n[color=#91dbea][b]Kursor myszy[/b][/color] — celowanie.\n[color=#91dbea][b]LPM / PPM / ŚPM[/b][/color] — strzelanie z trzech slotów broni.\n[color=#91dbea][b]1–5[/b][/color] — użycie zamontowanych rozszerzeń.\n[color=#91dbea][b]F1–F5[/b][/color] — aktywne umiejętności kapitana.\n[color=#91dbea][b]R[/b][/color] — użycie powerbanku, jeśli masz go w zapasach.\n\nStatek ma bezwładność. Obrót nie zatrzymuje ruchu — hamuj wcześniej i wykorzystuj ruch boczny, żeby unikać ostrzału."],
|
||||||
|
["Walka i przetrwanie", "03", "[font_size=26][color=#f1bd79]Wróć z wyprawy[/color][/font_size]\n\n[color=#91dbea][b]Tarcze, kadłub i energia[/b][/color]\nTarcze pochłaniają obrażenia przed kadłubem. Utrata całego kadłuba oznacza zniszczenie statku. Energia zasila część broni oraz dopalanie; odnawia się z czasem, szybciej przy gwieździe.\n\n[color=#91dbea][b]Dobieraj broń do zapasów[/b][/color]\nBroń energetyczna zużywa energię. Działko maszynowe potrzebuje zwykłej amunicji, gamma — napromieniowanej, a wyrzutnie — rakiet. Puste zapasy oznaczają brak strzału. Absorpcja ogranicza obrażenia danego typu.\n\n[color=#91dbea][b]Korzystaj z rozszerzeń[/b][/color]\nMiny atakują obszarowo, flary odciągają uwagę, kamuflaż pomaga zerwać kontakt, teleport przenosi w bezpieczne miejsce, a drony wspierają ostrzał. Doładowanie tarcz działa przy gwieździe. Sprawdzaj koszt i cooldown w opisie.\n\n[color=#f1bd79][b]Uważaj na uciekających przeciwników[/b][/color]\nWróg z kadłubem poniżej 10% ucieka wolniej, ale może zostawić minę. Nie leć bezmyślnie prosto za nim."],
|
||||||
|
["Łup i wytwarzanie", "04", "[font_size=26][color=#f1bd79]Zbieraj. Porównuj. Twórz.[/color][/font_size]\n\n[color=#91dbea][b]Łup zbierasz przez podlecenie[/b][/color]\nPrzedmioty w pobliżu są przyciągane do statku. Magazyn mieści 20 przedmiotów; przy pełnym magazynie zwolnij miejsce, żeby zebrać kolejny.\n\n[color=#91dbea][b]Asteroidy zostawiają fizyczne odłamki[/b][/color]\nStrzelaj do asteroid. Po zniszczeniu rozpadają się bazowo na 5–7 poszarpanych fragmentów. Każdy to jedna jednostka surowca — musisz go zebrać. Umiejętność górnika zwiększa uzysk.\n\n[color=#91dbea][b]Przeszukuj wraki[/b][/color]\nPodleć do wraku i przytrzymaj E, aby go przeszukać. Możesz znaleźć zapasy lub wyposażenie.\n\n[color=#91dbea][b]Warsztat — przycisk TWÓRZ[/b][/color]\nZ minerałów wytworzysz części, amunicję, rakiety i powerbanki. Receptura pokazuje wymagane składniki oraz ilość produktu.\n\n[color=#91dbea][b]Odzyskuj części[/b][/color]\nNajedź na zbędny przedmiot w magazynie i użyj przycisku rozłożenia. Części przydadzą się do napraw."],
|
||||||
|
["Statek i kapitan", "05", "[font_size=26][color=#f1bd79]Buduj własny styl gry[/color][/font_size]\n\n[color=#91dbea][b]Wyposażenie[/b][/color]\nKliknij panel statku w prawym górnym rogu i otwórz magazyn. Przeciągaj przedmioty do pasujących slotów: 3 bronie, 5 modułów i 5 rozszerzeń. Aktywne rozszerzenia uruchamiasz klawiszami 1–5; pasywne (oznaczone P) działają automatycznie w tych samych slotach. Najedź kursorem, żeby sprawdzić parametry i porównanie wyposażenia.\n\n[color=#91dbea][b]Nie każdy duplikat jest taki sam[/b][/color]\nBroń i moduły mogą mieć różne parametry oraz addony. Aktywne rozszerzenia losują cooldown od 30 do 5 pełnych sekund. Pasywne mają losowaną siłę efektu: Recykler odnawia tarcze i energię po zebraniu odłamka, Wirus przejmuje uciekających wrogów, a Pancerz z tytanu ogranicza obrażenia przy niskim kadłubie. Krótsze odnowienia są rzadkie — przed rozłożeniem porównaj egzemplarze. Rozszerzenia tego samego typu dzielą aktywne odnowienie.\n\n[color=#91dbea][b]Umiejętności kapitana[/b][/color]\nZa pokonanych wrogów zdobywasz doświadczenie i kredyty. Po awansie wybierasz umiejętność. Pasywne działają stale, aktywne przypisujesz do F1–F5.\n\n[color=#f1bd79][b]Okno umiejętności nie pauzuje gry.[/b][/color]\nOtwieraj je w bezpiecznym miejscu. Przed kolejnym układem uzupełnij zapasy i sprawdź wyposażenie."]
|
||||||
|
]
|
||||||
|
var _panel: PanelContainer
|
||||||
|
var _reader: RichTextLabel
|
||||||
|
var _tabs: Array[Button] = []
|
||||||
|
var _close_button: Button
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
name = "PlayerGuide"
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
var dim := ColorRect.new()
|
||||||
|
dim.color = Color(0.005, 0.012, 0.025, 0.82)
|
||||||
|
add_child(dim)
|
||||||
|
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_panel = PanelContainer.new()
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color("0b1725")
|
||||||
|
style.border_color = Color("46798a")
|
||||||
|
style.set_border_width_all(1)
|
||||||
|
style.set_corner_radius_all(10)
|
||||||
|
style.set_content_margin_all(24)
|
||||||
|
_panel.add_theme_stylebox_override("panel", style)
|
||||||
|
add_child(_panel)
|
||||||
|
var contents := VBoxContainer.new()
|
||||||
|
contents.add_theme_constant_override("separation", 18)
|
||||||
|
_panel.add_child(contents)
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
contents.add_child(header)
|
||||||
|
var heading := Label.new()
|
||||||
|
heading.text = "INSTRUKCJA / AKADEMIA PILOTA"
|
||||||
|
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
heading.add_theme_font_size_override("font_size", 16)
|
||||||
|
heading.add_theme_color_override("font_color", Color("91dbea"))
|
||||||
|
header.add_child(heading)
|
||||||
|
_close_button = Button.new()
|
||||||
|
_close_button.text = "ZAMKNIJ [ESC]"
|
||||||
|
BUTTON_STYLE.apply(_close_button)
|
||||||
|
_close_button.pressed.connect(close)
|
||||||
|
header.add_child(_close_button)
|
||||||
|
contents.add_child(HSeparator.new())
|
||||||
|
var body := HBoxContainer.new()
|
||||||
|
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
body.add_theme_constant_override("separation", 24)
|
||||||
|
contents.add_child(body)
|
||||||
|
var navigation := VBoxContainer.new()
|
||||||
|
navigation.custom_minimum_size.x = 190
|
||||||
|
navigation.add_theme_constant_override("separation", 8)
|
||||||
|
body.add_child(navigation)
|
||||||
|
for index in PAGES.size():
|
||||||
|
var button := Button.new()
|
||||||
|
button.text = "%s %s" % [PAGES[index][1], PAGES[index][0]]
|
||||||
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
button.custom_minimum_size.y = 44
|
||||||
|
BUTTON_STYLE.apply(button)
|
||||||
|
button.add_theme_font_override("font", ThemeDB.fallback_font)
|
||||||
|
button.add_theme_font_size_override("font_size", 14)
|
||||||
|
button.pressed.connect(_show_page.bind(index))
|
||||||
|
navigation.add_child(button)
|
||||||
|
_tabs.append(button)
|
||||||
|
_reader = RichTextLabel.new()
|
||||||
|
_reader.name = "GuideText"
|
||||||
|
_reader.bbcode_enabled = true
|
||||||
|
_reader.scroll_active = true
|
||||||
|
_reader.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_reader.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
_reader.add_theme_font_override("normal_font", ThemeDB.fallback_font)
|
||||||
|
_reader.add_theme_font_override("bold_font", ThemeDB.fallback_font)
|
||||||
|
_reader.add_theme_font_size_override("normal_font_size", 17)
|
||||||
|
_reader.add_theme_constant_override("line_separation", 5)
|
||||||
|
_reader.add_theme_color_override("default_color", Color("c5d4df"))
|
||||||
|
body.add_child(_reader)
|
||||||
|
var tip := Label.new()
|
||||||
|
tip.text = "Pierwszy cel: pokonaj wrogów → pokonaj bossa → znajdź zamieszkaną planetę."
|
||||||
|
tip.add_theme_font_override("font", ThemeDB.fallback_font)
|
||||||
|
tip.add_theme_font_size_override("font_size", 13)
|
||||||
|
tip.add_theme_color_override("font_color", Color("8ca5b8"))
|
||||||
|
contents.add_child(tip)
|
||||||
|
resized.connect(_layout)
|
||||||
|
_layout()
|
||||||
|
_show_page(0)
|
||||||
|
hide()
|
||||||
|
|
||||||
|
func _layout() -> void:
|
||||||
|
if _panel == null:
|
||||||
|
return
|
||||||
|
var base := Vector2(1040, 620)
|
||||||
|
var factor := minf(1.0, minf((size.x - 32) / base.x, (size.y - 32) / base.y))
|
||||||
|
_panel.size = base
|
||||||
|
_panel.scale = Vector2.ONE * factor
|
||||||
|
_panel.position = (size - base * factor) * 0.5
|
||||||
|
|
||||||
|
func _show_page(index: int) -> void:
|
||||||
|
_reader.text = PAGES[index][2]
|
||||||
|
_reader.scroll_to_line(0)
|
||||||
|
for tab_index in _tabs.size():
|
||||||
|
_tabs[tab_index].modulate = Color("f1bd79") if tab_index == index else Color.WHITE
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
show()
|
||||||
|
_show_page(0)
|
||||||
|
_close_button.grab_focus()
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
hide()
|
||||||
|
closed.emit()
|
||||||
|
|
||||||
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
|
if visible and event.is_action_pressed("ui_cancel"):
|
||||||
|
close()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
1
scripts/ui/player_guide.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://bsn4c58wliq4v
|
||||||
@ -7,3 +7,4 @@ const HUD := 20
|
|||||||
const MANAGEMENT := 40
|
const MANAGEMENT := 40
|
||||||
const DEFEAT := 50
|
const DEFEAT := 50
|
||||||
const WINDOW_Z := 100
|
const WINDOW_Z := 100
|
||||||
|
const DRAG_PREVIEW := 100
|
||||||
|
|||||||
142
tests/abilities/passive_extensions_test.gd
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
const PLAYER := preload("res://scenes/game/player_ship.tscn")
|
||||||
|
const ENEMY := preload("res://scenes/game/enemy_ship.tscn")
|
||||||
|
const GENERATOR := preload("res://scripts/items/item_generator.gd")
|
||||||
|
const ROLLS := preload("res://scripts/items/passive_extension_rolls.gd")
|
||||||
|
const ORE := preload("res://scripts/items/mineral_pickup.gd")
|
||||||
|
var ship: CharacterBody2D
|
||||||
|
var inventory: ShipInventory
|
||||||
|
|
||||||
|
func equip(id: String, slot: int, value: float) -> ItemInstance:
|
||||||
|
var item := GENERATOR.generate_for_definition(load("res://resources/items/%s.tres" % id), 1)
|
||||||
|
assert(item.is_passive_extension() and item.definition.icon != null)
|
||||||
|
item.passive_value = value
|
||||||
|
assert(inventory.add_item(item))
|
||||||
|
assert(inventory.move_item("cargo", inventory.cargo.find(item), "extension", slot))
|
||||||
|
assert(not ship.activate_ability(slot))
|
||||||
|
assert(item.get_tooltip_text().contains("PASYWNE"))
|
||||||
|
return item
|
||||||
|
|
||||||
|
func enemy_at(point: Vector2) -> CharacterBody2D:
|
||||||
|
var enemy := ENEMY.instantiate() as CharacterBody2D
|
||||||
|
enemy.position = point
|
||||||
|
add_child(enemy)
|
||||||
|
enemy.set("_player", ship)
|
||||||
|
enemy.set_physics_process(false)
|
||||||
|
enemy.set("_weapon_timer", 100.0)
|
||||||
|
return enemy
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
await get_tree().process_frame
|
||||||
|
GameState.start_new_run()
|
||||||
|
ship = PLAYER.instantiate()
|
||||||
|
ship.name = "PlayerShip"
|
||||||
|
add_child(ship)
|
||||||
|
ship.set_physics_process(false)
|
||||||
|
ship.get_node("WeaponController").set_physics_process(false)
|
||||||
|
ship.get_node("ExtensionSystems").set_physics_process(false)
|
||||||
|
inventory = ship.get_node("ShipInventory")
|
||||||
|
var recycler := equip("recycler", 0, 0.5)
|
||||||
|
var virus := equip("virus", 1, 1)
|
||||||
|
var armor := equip("titanium_armor", 2, 5)
|
||||||
|
# Per-item values are stable across resource serialization.
|
||||||
|
var encoded := var_to_bytes_with_objects(recycler)
|
||||||
|
var restored: ItemInstance = bytes_to_var_with_objects(encoded)
|
||||||
|
assert(restored.get_passive_value() == 0.5)
|
||||||
|
# Every discrete value is reachable, with strict exponential tails.
|
||||||
|
for effect in ["recycler", "virus", "titanium_armor"]:
|
||||||
|
var steps: int = {"recycler": 79, "virus": 19, "titanium_armor": 45}[effect]
|
||||||
|
var base: float = {"recycler": 0.5, "virus": 1.0, "titanium_armor": 5.0}[effect]
|
||||||
|
var step := 0.5 if effect == "recycler" else 1.0
|
||||||
|
assert(is_equal_approx(ROLLS.roll_value(effect, 0.0), base + steps * step))
|
||||||
|
assert(is_equal_approx(ROLLS.roll_value(effect, 0.99999), base))
|
||||||
|
for rank in range(steps + 1):
|
||||||
|
var high := ROLLS.tail_chance(float(rank) / steps)
|
||||||
|
var low := ROLLS.tail_chance(float(rank + 1) / steps) if rank < steps else 0.0
|
||||||
|
assert(is_equal_approx(ROLLS.roll_value(effect, (high + low) * 0.5), base + rank * step))
|
||||||
|
assert(is_equal_approx(ROLLS.tail_chance(1.0), 0.0002))
|
||||||
|
# Actual ore pickup both credits one mineral and restores resources once.
|
||||||
|
ship.energy = 0
|
||||||
|
ship.shields = 0
|
||||||
|
var before := int(GameState.captain.minerals.get("iron", 0))
|
||||||
|
var ore := Node2D.new()
|
||||||
|
ore.set_script(ORE)
|
||||||
|
add_child(ore)
|
||||||
|
ore.set_physics_process(false)
|
||||||
|
ore.call("_physics_process", 1.0)
|
||||||
|
assert(is_equal_approx(ship.energy, ship.max_energy * 0.005))
|
||||||
|
assert(is_equal_approx(ship.shields, ship.max_shields * 0.005))
|
||||||
|
assert(int(GameState.captain.minerals.get("iron", 0)) == before + 1)
|
||||||
|
ore.call("_physics_process", 1.0)
|
||||||
|
assert(int(GameState.captain.minerals.get("iron", 0)) == before + 1)
|
||||||
|
# Best duplicate wins; restoration is capped, not additive.
|
||||||
|
recycler.passive_value = 40
|
||||||
|
equip("recycler", 3, 10)
|
||||||
|
ship.energy = ship.max_energy * 0.9
|
||||||
|
ship.shields = ship.max_shields * 0.9
|
||||||
|
ship.recycle_asteroid_fragment()
|
||||||
|
assert(ship.energy == ship.max_energy and ship.shields == ship.max_shields)
|
||||||
|
assert(ship.get_passive_extension_value("recycler") == 40)
|
||||||
|
# Armor reduces incoming damage (including shields) only at its threshold.
|
||||||
|
ship.hull = ship.max_hull * 0.05
|
||||||
|
ship.shields = 50
|
||||||
|
ship.take_damage(20)
|
||||||
|
assert(is_equal_approx(ship.shields, 40))
|
||||||
|
ship.hull = ship.max_hull * 0.051
|
||||||
|
ship.shields = 50
|
||||||
|
ship.take_damage(20)
|
||||||
|
assert(is_equal_approx(ship.shields, 30))
|
||||||
|
armor.passive_value = 50
|
||||||
|
ship.hull = ship.max_hull * 0.5
|
||||||
|
assert(is_equal_approx(ship.get_damage_reduction(), 0.5))
|
||||||
|
# Virus takes fleeing ships only, respects capacity and switches allegiance.
|
||||||
|
var first := enemy_at(Vector2(200, 0))
|
||||||
|
var second := enemy_at(Vector2(320, 0))
|
||||||
|
var third := enemy_at(Vector2(450, 0))
|
||||||
|
first.hull = 1
|
||||||
|
second.hull = 1
|
||||||
|
first.call("_physics_process", 0.01)
|
||||||
|
second.call("_physics_process", 0.01)
|
||||||
|
assert(first.virus_controlled and not second.virus_controlled)
|
||||||
|
assert(not first.is_in_group("enemies") and first.is_in_group("virus_allies"))
|
||||||
|
assert(not first.is_retreating() and first.select_combat_target() != ship)
|
||||||
|
assert(second.select_combat_target() == first)
|
||||||
|
await get_tree().physics_frame
|
||||||
|
assert(first.collision_layer == 64)
|
||||||
|
# Controlled fire uses enemy damage, cannot hit the player and can hit hostiles.
|
||||||
|
first.weapon_kind = ItemDefinition.WeaponKind.MACHINE_GUN
|
||||||
|
first.call("_fire_plasma", Vector2.RIGHT)
|
||||||
|
var bullet: Node
|
||||||
|
for child in get_children():
|
||||||
|
if child is Area2D and child.get_script() == preload("res://scripts/game/weapon_projectile.gd"):
|
||||||
|
bullet = child
|
||||||
|
assert(bullet != null and bullet.collision_mask == 37 and bullet.enemy_shot)
|
||||||
|
var shields_before: float = third.shields
|
||||||
|
bullet.call("_apply_damage_once", third)
|
||||||
|
assert(third.shields < shields_before)
|
||||||
|
# Converted missile splash must never damage the player.
|
||||||
|
var player_shields: float = ship.shields
|
||||||
|
bullet.weapon_kind = ItemDefinition.WeaponKind.MISSILE
|
||||||
|
bullet.global_position = ship.global_position
|
||||||
|
bullet.call("_explode_at_target", third)
|
||||||
|
assert(ship.shields == player_shields)
|
||||||
|
# Increased capacity takes another ship; downgrade and unequip release extras.
|
||||||
|
virus.passive_value = 2
|
||||||
|
second.call("_physics_process", 0.01)
|
||||||
|
assert(second.virus_controlled)
|
||||||
|
virus.passive_value = 1
|
||||||
|
ship.call("_sync_virus_allies")
|
||||||
|
assert(get_tree().get_nodes_in_group("virus_allies").size() == 1)
|
||||||
|
assert(inventory.move_item("extension", 1, "cargo", 2))
|
||||||
|
ship.call("_sync_virus_allies")
|
||||||
|
assert(get_tree().get_nodes_in_group("virus_allies").is_empty())
|
||||||
|
assert(first.is_in_group("enemies") and second.is_in_group("enemies"))
|
||||||
|
print("PASS: passive extension rolls, persistence, pickup, armor and virus combat/lifecycle")
|
||||||
|
for sound in get_tree().root.find_children("*", "AudioStreamPlayer", true, false) + get_tree().root.find_children("*", "AudioStreamPlayer2D", true, false):
|
||||||
|
sound.stop()
|
||||||
|
sound.stream = null
|
||||||
|
preload("res://scripts/game/weapon_audio.gd")._streams.clear()
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
get_tree().quit()
|
||||||
1
tests/abilities/passive_extensions_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://phkehb3po7kf
|
||||||
4
tests/abilities/passive_extensions_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/abilities/passive_extensions_test.gd" id="1"]
|
||||||
|
[node name="PassiveExtensionsTest" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
45
tests/previews/passive_extensions_preview.gd
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
extends "res://scripts/game/game.gd"
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
super._ready()
|
||||||
|
set_physics_process(false)
|
||||||
|
player.set_physics_process(false)
|
||||||
|
player.get_node("WeaponController").set_physics_process(false)
|
||||||
|
var inventory := player.get_node("ShipInventory") as ShipInventory
|
||||||
|
var ids := ["recycler", "virus", "titanium_armor"]
|
||||||
|
for index in ids.size():
|
||||||
|
var item := preload("res://scripts/items/item_generator.gd").generate_for_definition(load("res://resources/items/%s.tres" % ids[index]), 1)
|
||||||
|
item.passive_value = [5.0, 2.0, 15.0][index]
|
||||||
|
inventory.extension_slots[index] = item
|
||||||
|
var skills := preload("res://scripts/captain/captain_skills.gd")
|
||||||
|
for skill in skills.IDS:
|
||||||
|
if not bool(skills.DATA[skill]["active"]):
|
||||||
|
GameState.captain.skills[skill] = 1
|
||||||
|
GameState.captain.emit_changed()
|
||||||
|
inventory.inventory_changed.emit()
|
||||||
|
var enemies := get_tree().get_nodes_in_group("enemies")
|
||||||
|
for enemy in enemies:
|
||||||
|
enemy.set_physics_process(false)
|
||||||
|
if enemies.size() >= 2:
|
||||||
|
enemies[0].global_position = player.global_position + Vector2(140, -50)
|
||||||
|
enemies[0].hull = 1
|
||||||
|
player.try_control_enemy(enemies[0])
|
||||||
|
enemies[1].global_position = player.global_position + Vector2(290, 15)
|
||||||
|
await get_tree().process_frame
|
||||||
|
if not $HUD.get("_sidebar_open"):
|
||||||
|
$HUD.call("_toggle_sidebar")
|
||||||
|
if "--capture-preview" in OS.get_cmdline_user_args():
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
var output := "res://artifacts/passive_extensions_preview.png"
|
||||||
|
if "--captain-window" in OS.get_cmdline_user_args():
|
||||||
|
$HUD.get("_captain_skills_ui").call("open_management")
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
output = "res://artifacts/captain_window_preview.png"
|
||||||
|
assert($HUD.get("_passive_captain_panel").position.x < 20)
|
||||||
|
assert($HUD.get("_passive_ship_panel").position.x > 900)
|
||||||
|
assert($HUD.get_node("HUDRoot/MissionObjectives").position.y > $HUD.get("_captain_panel").position.y + $HUD.get("_captain_panel").size.y)
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
get_viewport().get_texture().get_image().save_png(output)
|
||||||
|
print("PREVIEW: captured passive extension HUD")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/previews/passive_extensions_preview.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://b7n3lyntd3lqd
|
||||||
5
tests/previews/passive_extensions_preview.tscn
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[gd_scene load_steps=3 format=3]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/game/game.tscn" id="1"]
|
||||||
|
[ext_resource type="Script" path="res://tests/previews/passive_extensions_preview.gd" id="2"]
|
||||||
|
[node name="Game" instance=ExtResource("1")]
|
||||||
|
script = ExtResource("2")
|
||||||
126
tests/ships/drag_cursor_test.gd
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
const GAME := preload("res://scenes/game/game.tscn")
|
||||||
|
var held := false
|
||||||
|
var _last_mouse := Vector2.ZERO
|
||||||
|
|
||||||
|
func motion(point: Vector2) -> void:
|
||||||
|
var event := InputEventMouseMotion.new()
|
||||||
|
event.position = point
|
||||||
|
event.global_position = point
|
||||||
|
event.relative = point - _last_mouse
|
||||||
|
_last_mouse = point
|
||||||
|
event.button_mask = MOUSE_BUTTON_MASK_LEFT if held else 0
|
||||||
|
get_viewport().push_input(event, true)
|
||||||
|
|
||||||
|
func mouse(point: Vector2, down: bool) -> void:
|
||||||
|
held = down
|
||||||
|
var event := InputEventMouseButton.new()
|
||||||
|
event.position = point
|
||||||
|
event.global_position = point
|
||||||
|
event.button_index = MOUSE_BUTTON_LEFT
|
||||||
|
event.pressed = down
|
||||||
|
event.button_mask = MOUSE_BUTTON_MASK_LEFT if held else 0
|
||||||
|
get_viewport().push_input(event, true)
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.start_new_run()
|
||||||
|
var game := GAME.instantiate()
|
||||||
|
add_child(game)
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
game.set_physics_process(false)
|
||||||
|
var player := game.get_node("PlayerShip") as CharacterBody2D
|
||||||
|
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 hud := game.get_node("HUD")
|
||||||
|
hud.call("_toggle_inventory")
|
||||||
|
var drawer: Control = hud.get("_inventory_clip")
|
||||||
|
drawer.set_meta("floating_window_dragged", true)
|
||||||
|
drawer.global_position = Vector2(32, 100)
|
||||||
|
var inventory: ShipInventory = player.get_node("ShipInventory")
|
||||||
|
var item := preload("res://scripts/items/item_generator.gd").generate_for_definition(preload("res://resources/items/hull_plating.tres"), 1)
|
||||||
|
assert(inventory.add_item(item))
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
var panel: Control = hud.get("inventory_panel")
|
||||||
|
var source := panel.get_node("Contents/CargoGrid").get_child(0) as Control
|
||||||
|
var origin := source.get_global_rect().get_center()
|
||||||
|
var cursor := hud.get_node("FlightCursor")
|
||||||
|
# The aiming cursor is only active in free game space.
|
||||||
|
motion(Vector2(650, 150))
|
||||||
|
await get_tree().process_frame
|
||||||
|
cursor.call("_process", 0.0)
|
||||||
|
assert(cursor.aiming)
|
||||||
|
motion(origin)
|
||||||
|
await get_tree().process_frame
|
||||||
|
cursor.call("_process", 0.0)
|
||||||
|
assert(not cursor.aiming)
|
||||||
|
# Start a real native drag, then pass over a raised crafting window.
|
||||||
|
mouse(origin, true)
|
||||||
|
motion(origin + Vector2(24, 6))
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert(get_viewport().gui_is_dragging())
|
||||||
|
var craft: Control = hud.get("_crafting_window")
|
||||||
|
craft.position = Vector2(420, 160)
|
||||||
|
craft.show()
|
||||||
|
hud.get_node("HUDRoot/FloatingWindows").call("focus_window", craft)
|
||||||
|
motion(Vector2(660, 300))
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
var overlay := get_tree().get_first_node_in_group("item_drag_overlay") as CanvasLayer
|
||||||
|
assert(overlay != null)
|
||||||
|
assert(overlay.layer > hud.layer and overlay.layer > 50)
|
||||||
|
var icon: Control = overlay.get_node("DragIcon")
|
||||||
|
assert(icon.visible)
|
||||||
|
assert((icon.position + icon.size * 0.5).is_equal_approx(Vector2(660, 300)))
|
||||||
|
assert(icon.mouse_filter == Control.MOUSE_FILTER_IGNORE)
|
||||||
|
assert(not cursor.should_aim())
|
||||||
|
if "--capture-preview" in OS.get_cmdline_user_args():
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
get_viewport().get_texture().get_image().save_png("res://artifacts/item_drag_preview.png")
|
||||||
|
mouse(Vector2(660, 300), false)
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert(not icon.visible and inventory.cargo[0] == item)
|
||||||
|
# A subsequent drag still delivers the module to its equipment slot.
|
||||||
|
craft.hide()
|
||||||
|
hud.get_node("HUDRoot/FloatingWindows").call("focus_window", drawer)
|
||||||
|
motion(origin)
|
||||||
|
mouse(origin, true)
|
||||||
|
motion(origin + Vector2(24, 6))
|
||||||
|
await get_tree().process_frame
|
||||||
|
var target: Control = hud.get("_sidebar").get_node("Scroll/Contents/ShipEquipmentPanel/Contents/EquipmentLayout/Module2")
|
||||||
|
var destination := target.get_global_rect().get_center()
|
||||||
|
motion(destination)
|
||||||
|
await get_tree().process_frame
|
||||||
|
mouse(destination, false)
|
||||||
|
await get_tree().process_frame
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert(inventory.module_slots[1] == item and inventory.cargo[0] == null)
|
||||||
|
assert(not icon.visible)
|
||||||
|
# Modal UI, defeat, docking and pause all suppress the aiming cursor.
|
||||||
|
var skills: CanvasLayer = hud.get("_captain_skills_ui")
|
||||||
|
skills.call("open_management")
|
||||||
|
motion(Vector2(650, 350))
|
||||||
|
await get_tree().process_frame
|
||||||
|
assert(not cursor.should_aim())
|
||||||
|
skills.call("_close")
|
||||||
|
motion(Vector2(650, 150))
|
||||||
|
await get_tree().process_frame
|
||||||
|
player.set("_is_destroyed", true)
|
||||||
|
assert(not cursor.should_aim())
|
||||||
|
player.set("_is_destroyed", false)
|
||||||
|
player.set("is_docked", true)
|
||||||
|
assert(not cursor.should_aim())
|
||||||
|
player.set("is_docked", false)
|
||||||
|
get_tree().paused = true
|
||||||
|
assert(not cursor.should_aim())
|
||||||
|
get_tree().paused = false
|
||||||
|
cursor.call("_process", 0.0)
|
||||||
|
game.queue_free()
|
||||||
|
await get_tree().process_frame
|
||||||
|
print("DRAG_CURSOR_TEST: PASS centered overlay above windows, cancel/drop, UI/space cursor and cleanup")
|
||||||
|
get_tree().quit()
|
||||||
1
tests/ships/drag_cursor_test.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dn1mmac5wrvva
|
||||||
4
tests/ships/drag_cursor_test.tscn
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
[ext_resource type="Script" path="res://tests/ships/drag_cursor_test.gd" id="1"]
|
||||||
|
[node name="DragCursorTest" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||