This commit is contained in:
Tomasz Boruc 2026-10-17 16:20:14 +02:00
parent 9aa2dfebd6
commit 750c1075d8
57 changed files with 1350 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

View 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

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View 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

View 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

View 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

View 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"

View 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"

View 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"

View File

@ -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)

View File

@ -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:

View File

@ -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,6 +303,8 @@ 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:
if not _reward_claimed:
_reward_claimed = true
_reward_captain() _reward_captain()
_drop_loot() _drop_loot()
_spawn_destruction_effect() _spawn_destruction_effect()

View File

@ -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()

View File

@ -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"):

View File

@ -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:

View File

@ -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

View File

@ -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,6 +52,9 @@ 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:
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() item.extension_cooldown = ItemInstance.EXTENSION_COOLDOWNS.roll_seconds()
return item return item
var roll := randf() var roll := randf()

View File

@ -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]

View File

@ -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)

View 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

View File

@ -0,0 +1 @@
uid://qyharawiegof

View File

@ -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:

View File

@ -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()

View File

@ -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:

View 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)

View File

@ -0,0 +1 @@
uid://ct7b1pxcenj3l

View 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

View File

@ -0,0 +1 @@
uid://chsbheqg4n8m4

View File

@ -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:

View 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)

View File

@ -0,0 +1 @@
uid://bmb2i1t505ol4

View 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"))

View File

@ -0,0 +1 @@
uid://ilqfl3an0hl7

View File

@ -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
var bottom: float = (get_parent() as Control).size.y - 12.0 position = captain.position + Vector2(0.0, captain.size.y + 8.0)
if available_width < 180.0 and is_instance_valid(cockpit):
bottom = cockpit.position.y - 64.0
position = Vector2(12.0, bottom - size.y)

View 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)

View File

@ -0,0 +1 @@
uid://d1pekaexu4wan

119
scripts/ui/player_guide.gd Normal file
View 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()

View File

@ -0,0 +1 @@
uid://bsn4c58wliq4v

View File

@ -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

View 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()

View File

@ -0,0 +1 @@
uid://phkehb3po7kf

View 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")

View 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()

View File

@ -0,0 +1 @@
uid://b7n3lyntd3lqd

View 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")

View 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()

View File

@ -0,0 +1 @@
uid://dn1mmac5wrvva

View 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")