This commit is contained in:
Tomasz Boruc 2026-10-17 16:51:07 +02:00
parent 750c1075d8
commit a0a2278421
22 changed files with 300 additions and 72 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 KiB

After

Width:  |  Height:  |  Size: 1014 KiB

View File

@ -25,3 +25,12 @@ Use case: stylized-concept. Final production square ability icon for a realistic
## power_redirect ## power_redirect
Use case: stylized-concept. Final production square ability icon for a realistic painterly space RPG. Highly polished hand-painted digital game art, cinematic material detail, strong simple silhouette readable at 40 pixels, centered composition with 12% safe margin. Dark navy outer corners, restrained luminous accent, no text, no letters, no numbers, no interface, no border. One iconic subject, not a scene. Power Redirect active skill: a heavy metallic spaceship shield emitter on the left splitting open and releasing a powerful curved orange-gold energy stream into a bright compact cyan reactor core on the right. A bold luminous flow of power visually connects the two components. Burnt orange, pale gold, electric teal highlights, realistic sci-fi machinery, strong simple two-part silhouette. Use case: stylized-concept. Final production square ability icon for a realistic painterly space RPG. Highly polished hand-painted digital game art, cinematic material detail, strong simple silhouette readable at 40 pixels, centered composition with 12% safe margin. Dark navy outer corners, restrained luminous accent, no text, no letters, no numbers, no interface, no border. One iconic subject, not a scene. Power Redirect active skill: a heavy metallic spaceship shield emitter on the left splitting open and releasing a powerful curved orange-gold energy stream into a bright compact cyan reactor core on the right. A bold luminous flow of power visually connects the two components. Burnt orange, pale gold, electric teal highlights, realistic sci-fi machinery, strong simple two-part silhouette.
## lucky (2026-10-17)
Built-in image_gen, transparent background. Saved as assets/icons/captain/lucky.png.
Final prompt: Use case: stylized-concept. Asset: single premium science fiction game captain skill inventory icon, square, transparent background. Skill LUCK: sculptural futuristic four-leaf clover made from luminous emerald crystal and brushed gold metal, with a small gold orbital ring and a few tiny golden sparks. Centered bold compact silhouette readable at 40 pixels, exquisite painted 3D game item rendering, dramatic highlights, rich gold and green, isolated cutout, no text, no letters, no frame, no backdrop. Save resulting asset for game integration.
## explorer (2026-10-17)
Built-in image_gen, transparent background. Saved as assets/icons/captain/explorer.png.
Final prompt: Use case: stylized-concept. Single premium science fiction game captain skill icon EXPLORER. Centered bold compact 3D sculptural deep space navigation compass, weathered titanium and gold ring surrounding luminous turquoise holographic miniature planet and three-point star, subtle emerald cyan trails suggesting discovery, exquisitely detailed painterly game item artwork with dramatic highlights, readable small silhouette, isolated on genuinely transparent background. Square composition, generous transparent margins, no text no numbers no frame no scenery. Match premium sci-fi inventory artwork.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://btk4ywrx737fw"
path="res://.godot/imported/explorer.png-5f686e01e5775b401dc40513f1c279ea.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/explorer.png"
dest_files=["res://.godot/imported/explorer.png-5f686e01e5775b401dc40513f1c279ea.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://ddyvf2s8nmvma"
path="res://.godot/imported/lucky.png-8c04401bc3d2099ccc953cd3aeeed557.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/captain/lucky.png"
dest_files=["res://.godot/imported/lucky.png-8c04401bc3d2099ccc953cd3aeeed557.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

@ -43,7 +43,7 @@ text = "SYSTEMY POKŁADOWE"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="EquipmentLayout" type="Control" parent="Contents"] [node name="EquipmentLayout" type="Control" parent="Contents"]
custom_minimum_size = Vector2(284, 552) custom_minimum_size = Vector2(264, 432)
layout_mode = 2 layout_mode = 2
mouse_filter = 2 mouse_filter = 2
@ -59,130 +59,130 @@ script = ExtResource("1_schematic")
[node name="Weapon1" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Weapon1" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 14.0 offset_left = 12.0
offset_top = 22.0 offset_top = 20.0
offset_right = 78.0 offset_right = 68.0
offset_bottom = 86.0 offset_bottom = 76.0
slot_kind = "weapon" slot_kind = "weapon"
slot_index = 0 slot_index = 0
overlay_slot = true overlay_slot = true
[node name="Weapon2" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Weapon2" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 206.0 offset_left = 196.0
offset_top = 22.0 offset_top = 20.0
offset_right = 270.0 offset_right = 252.0
offset_bottom = 86.0 offset_bottom = 76.0
slot_kind = "weapon" slot_kind = "weapon"
slot_index = 1 slot_index = 1
overlay_slot = true overlay_slot = true
[node name="Weapon3" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Weapon3" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 110.0 offset_left = 104.0
offset_top = 22.0 offset_top = 20.0
offset_right = 174.0 offset_right = 160.0
offset_bottom = 86.0 offset_bottom = 76.0
slot_kind = "weapon" slot_kind = "weapon"
slot_index = 2 slot_index = 2
overlay_slot = true overlay_slot = true
[node name="Module1" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Module1" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 14.0 offset_left = 12.0
offset_top = 108.0 offset_top = 96.0
offset_right = 78.0 offset_right = 68.0
offset_bottom = 172.0 offset_bottom = 152.0
slot_kind = "module" slot_kind = "module"
slot_index = 0 slot_index = 0
overlay_slot = true overlay_slot = true
[node name="Module2" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Module2" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 14.0 offset_left = 12.0
offset_top = 200.0 offset_top = 164.0
offset_right = 78.0 offset_right = 68.0
offset_bottom = 264.0 offset_bottom = 220.0
slot_kind = "module" slot_kind = "module"
slot_index = 1 slot_index = 1
overlay_slot = true overlay_slot = true
[node name="Module3" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Module3" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 14.0 offset_left = 12.0
offset_top = 292.0 offset_top = 232.0
offset_right = 78.0 offset_right = 68.0
offset_bottom = 356.0 offset_bottom = 288.0
slot_kind = "module" slot_kind = "module"
slot_index = 2 slot_index = 2
overlay_slot = true overlay_slot = true
[node name="Module4" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Module4" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 14.0 offset_left = 12.0
offset_top = 384.0 offset_top = 300.0
offset_right = 78.0 offset_right = 68.0
offset_bottom = 448.0 offset_bottom = 356.0
slot_kind = "module" slot_kind = "module"
slot_index = 3 slot_index = 3
overlay_slot = true overlay_slot = true
[node name="Module5" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Module5" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 14.0 offset_left = 12.0
offset_top = 476.0 offset_top = 368.0
offset_right = 78.0 offset_right = 68.0
offset_bottom = 540.0 offset_bottom = 424.0
slot_kind = "module" slot_kind = "module"
slot_index = 4 slot_index = 4
overlay_slot = true overlay_slot = true
[node name="Extension1" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Extension1" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 206.0 offset_left = 196.0
offset_top = 108.0 offset_top = 96.0
offset_right = 270.0 offset_right = 252.0
offset_bottom = 172.0 offset_bottom = 152.0
slot_kind = "extension" slot_kind = "extension"
slot_index = 0 slot_index = 0
overlay_slot = true overlay_slot = true
[node name="Extension2" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Extension2" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 206.0 offset_left = 196.0
offset_top = 200.0 offset_top = 164.0
offset_right = 270.0 offset_right = 252.0
offset_bottom = 264.0 offset_bottom = 220.0
slot_kind = "extension" slot_kind = "extension"
slot_index = 1 slot_index = 1
overlay_slot = true overlay_slot = true
[node name="Extension3" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Extension3" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 206.0 offset_left = 196.0
offset_top = 292.0 offset_top = 232.0
offset_right = 270.0 offset_right = 252.0
offset_bottom = 356.0 offset_bottom = 288.0
slot_kind = "extension" slot_kind = "extension"
slot_index = 2 slot_index = 2
overlay_slot = true overlay_slot = true
[node name="Extension4" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Extension4" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 206.0 offset_left = 196.0
offset_top = 384.0 offset_top = 300.0
offset_right = 270.0 offset_right = 252.0
offset_bottom = 448.0 offset_bottom = 356.0
slot_kind = "extension" slot_kind = "extension"
slot_index = 3 slot_index = 3
overlay_slot = true overlay_slot = true
[node name="Extension5" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")] [node name="Extension5" parent="Contents/EquipmentLayout" instance=ExtResource("2_slot")]
layout_mode = 0 layout_mode = 0
offset_left = 206.0 offset_left = 196.0
offset_top = 476.0 offset_top = 368.0
offset_right = 270.0 offset_right = 252.0
offset_bottom = 540.0 offset_bottom = 424.0
slot_kind = "extension" slot_kind = "extension"
slot_index = 4 slot_index = 4
overlay_slot = true overlay_slot = true

View File

@ -22,8 +22,7 @@ static func value(skill: String, rank: int) -> float:
return float(DATA[skill]["values"][clampi(rank, 1, MAX_RANK) - 1]) return float(DATA[skill]["values"][clampi(rank, 1, MAX_RANK) - 1])
static func icon(skill: String) -> Texture2D: static func icon(skill: String) -> Texture2D:
var extension := "svg" if skill in ["lucky", "explorer"] else "png" return load("res://assets/icons/captain/%s.png" % skill) as Texture2D
return load("res://assets/icons/captain/%s.%s" % [skill, extension]) as Texture2D
static func description(skill: String, rank: int) -> String: static func description(skill: String, rank: int) -> String:
var amount := roundi(value(skill, rank) * 100.0) var amount := roundi(value(skill, rank) * 100.0)

View File

@ -131,7 +131,16 @@ func get_tier_tint() -> Color:
func get_visual_size() -> float: func get_visual_size() -> float:
return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0) return 112.0 if loot_tier == 2 else (68.0 if loot_tier == 1 else 56.0)
func regenerate_defenses(delta: float) -> void:
if hull <= 0.0 or is_queued_for_deletion():
return
hull = minf(max_hull, hull + max_hull * 0.01 * delta)
shields = minf(max_shields, shields + max_shields * 0.01 * delta)
if is_instance_valid(_shield_visual):
_shield_visual.set_shield_values(shields, max_shields)
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
regenerate_defenses(delta)
_slow_remaining = maxf(0.0, _slow_remaining - delta) _slow_remaining = maxf(0.0, _slow_remaining - delta)
_weapon_timer = maxf(0.0, _weapon_timer - delta) _weapon_timer = maxf(0.0, _weapon_timer - delta)
if not is_instance_valid(_player) or bool(_player.get("_is_destroyed")): if not is_instance_valid(_player) or bool(_player.get("_is_destroyed")):

View File

@ -24,7 +24,7 @@ var _sidebar: PanelContainer
var _sidebar_toggle: Button var _sidebar_toggle: Button
var _sidebar_open := false var _sidebar_open := false
var _sidebar_tween: Tween var _sidebar_tween: Tween
const SIDEBAR_WIDTH := 306.0 const SIDEBAR_WIDTH := 286.0
const EQUIPMENT_UI_SCALE := 0.8 const EQUIPMENT_UI_SCALE := 0.8
var _debug_scroll: ScrollContainer var _debug_scroll: ScrollContainer
var _debug_actions: VBoxContainer var _debug_actions: VBoxContainer
@ -327,8 +327,13 @@ func _layout_sidebar() -> void:
return return
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 var scroll := _sidebar.get_node("Scroll") as ScrollContainer
_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))) scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
var content_height: float = scroll.get_node("Contents").get_combined_minimum_size().y + 16.0
var available_height := maxf(100.0, $HUDRoot.size.y - 148.0 - _ship_passive_reserved_height)
var panel_scale := minf(EQUIPMENT_UI_SCALE, available_height / content_height)
_sidebar.scale = Vector2.ONE * panel_scale
_sidebar.size = Vector2(maxf(SIDEBAR_WIDTH, _sidebar.get_combined_minimum_size().x), content_height)
_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)
@ -341,7 +346,7 @@ func _layout_sidebar() -> void:
_captain_skills_ui.call_deferred("_layout") _captain_skills_ui.call_deferred("_layout")
_layout_debug() _layout_debug()
if not _sidebar.has_meta("floating_window_dragged"): if not _sidebar.has_meta("floating_window_dragged"):
_sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8, 136) _sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * _sidebar.scale.x - 8, 136)
else: else:
_window_manager.call("clamp_window", _sidebar) _window_manager.call("clamp_window", _sidebar)
_sidebar.visible = _sidebar_open _sidebar.visible = _sidebar_open

View File

@ -8,6 +8,7 @@ var restart_inventory: Dictionary = {}
var system_level: int = 1 var system_level: int = 1
var system_seed: int = 0 var system_seed: int = 0
var explored_pois: Dictionary = {} var explored_pois: Dictionary = {}
var asteroid_retry_seeds: Dictionary = {}
var exploration_wreck_bonuses: Dictionary = {} var exploration_wreck_bonuses: Dictionary = {}
var hud_panel_state: Dictionary = {} var hud_panel_state: Dictionary = {}
@ -17,7 +18,12 @@ func create_system_seed() -> int:
return maxi(1, rng.randi()) return maxi(1, rng.randi())
func reset_current_system_exploration() -> void: func reset_current_system_exploration() -> void:
# Retrying after death starts a fresh attempt at this board while keeping rewards. # Keep the system and rewards, but regenerate asteroid placement and minerals.
var previous: int = asteroid_retry_seeds.get(system_seed, system_seed)
var next_seed := create_system_seed()
while next_seed == previous:
next_seed = create_system_seed()
asteroid_retry_seeds[system_seed] = next_seed
var prefix := "%d:" % system_seed var prefix := "%d:" % system_seed
for id in explored_pois.keys(): for id in explored_pois.keys():
if str(id).begins_with(prefix): if str(id).begins_with(prefix):
@ -29,6 +35,7 @@ func start_new_run() -> void:
system_seed = create_system_seed() system_seed = create_system_seed()
restart_inventory.clear() restart_inventory.clear()
explored_pois.clear() explored_pois.clear()
asteroid_retry_seeds.clear()
exploration_wreck_bonuses.clear() exploration_wreck_bonuses.clear()
hud_panel_state.clear() hud_panel_state.clear()

View File

@ -210,6 +210,20 @@ func can_move_item(from_kind: String, from_index: int, to_kind: String, to_index
if displaced != null and displaced.definition != null: if displaced != null and displaced.definition != null:
if not _accepts_item(from_kind, from_index, displaced): if not _accepts_item(from_kind, from_index, displaced):
return false return false
if from_kind == "extension" or to_kind == "extension":
var projected := extension_slots.duplicate()
if from_kind == "extension":
projected[from_index] = displaced
if to_kind == "extension":
projected[to_index] = item
var equipped_types := {}
for extension in projected:
if extension == null or extension.definition == null:
continue
var type_id: String = extension.definition.item_id
if equipped_types.has(type_id):
return false
equipped_types[type_id] = true
return true return true
func _accepts_item(kind: String, index: int, item: ItemInstance) -> bool: func _accepts_item(kind: String, index: int, item: ItemInstance) -> bool:

View File

@ -9,14 +9,17 @@ static func spawn(game: Node2D, data: Dictionary, player: CharacterBody2D) -> vo
var wreck_bonus := int(GameState.exploration_wreck_bonuses[GameState.system_seed]) var wreck_bonus := int(GameState.exploration_wreck_bonuses[GameState.system_seed])
var rng := RandomNumberGenerator.new() var rng := RandomNumberGenerator.new()
rng.seed = GameState.system_seed ^ 0x504f49 rng.seed = GameState.system_seed ^ 0x504f49
var asteroid_rng := RandomNumberGenerator.new()
asteroid_rng.seed = int(GameState.asteroid_retry_seeds.get(GameState.system_seed, GameState.system_seed)) ^ 0x41535452
var occupied: Array[Vector2] = [] var occupied: Array[Vector2] = []
var wreck_count := rng.randi_range(2, 4) var wreck_count := rng.randi_range(2, 4)
for index in wreck_count + rng.randi_range(6, 10): for index in wreck_count + rng.randi_range(6, 10):
var kind := "wreck" if index < wreck_count else "asteroid" var kind := "wreck" if index < wreck_count else "asteroid"
var point_rng := asteroid_rng if kind == "asteroid" else rng
var candidate := Vector2.ZERO var candidate := Vector2.ZERO
var found := false var found := false
for attempt in 200: for attempt in 200:
candidate = Vector2(rng.randf_range(-2300, 2300), rng.randf_range(-1500, 1500)) candidate = Vector2(point_rng.randf_range(-2300, 2300), point_rng.randf_range(-1500, 1500))
if candidate.length() < 650 or candidate.distance_to(player.position) < 450: if candidate.length() < 650 or candidate.distance_to(player.position) < 450:
continue continue
var clear := true var clear := true
@ -31,7 +34,7 @@ static func spawn(game: Node2D, data: Dictionary, player: CharacterBody2D) -> vo
break break
if found: if found:
occupied.append(candidate) occupied.append(candidate)
_add(game, player, kind, candidate, index, rng.randi()) _add(game, player, kind, candidate, index, point_rng.randi())
# Rare per system, at most one ruin on an uninhabited planet. # Rare per system, at most one ruin on an uninhabited planet.
if rng.randf() < 0.08: if rng.randf() < 0.08:
var candidates: Array[int] = [] var candidates: Array[int] = []

View File

@ -9,6 +9,7 @@ extends PanelContainer
var _inventory: ShipInventory var _inventory: ShipInventory
var _icon: TextureRect var _icon: TextureRect
var _delete_button: Button var _delete_button: Button
var _drop_highlight := false
func _ready() -> void: func _ready() -> void:
if slot_kind == "cargo": if slot_kind == "cargo":
@ -46,7 +47,7 @@ func _ready() -> void:
if _inventory: if _inventory:
_inventory.inventory_changed.connect(_refresh) _inventory.inventory_changed.connect(_refresh)
_inventory.ammunition_changed.connect(_refresh) _inventory.ammunition_changed.connect(_refresh)
custom_minimum_size = Vector2(72, 72) if slot_kind == "cargo" else Vector2(64, 64) custom_minimum_size = Vector2(72, 72) if slot_kind == "cargo" else Vector2(56, 56)
slot_label.custom_minimum_size = Vector2.ZERO slot_label.custom_minimum_size = Vector2.ZERO
slot_label.add_theme_font_size_override("font_size", 9) slot_label.add_theme_font_size_override("font_size", 9)
_icon = TextureRect.new() _icon = TextureRect.new()
@ -55,10 +56,10 @@ func _ready() -> void:
_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED _icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE _icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_icon) add_child(_icon)
if overlay_slot: if overlay_slot and slot_kind == "weapon":
var binding := Label.new() var binding := Label.new()
binding.mouse_filter = Control.MOUSE_FILTER_IGNORE binding.mouse_filter = Control.MOUSE_FILTER_IGNORE
binding.text = (["LPM", "PPM", "ŚPM"][slot_index]) if slot_kind == "weapon" else (str(slot_index + 1) if slot_kind == "extension" else ShipInventory.MODULE_NAMES[slot_index]) binding.text = ["LPM", "PPM", "ŚPM"][slot_index]
binding.position = Vector2(2, -24) binding.position = Vector2(2, -24)
binding.add_theme_font_size_override("font_size", 9) binding.add_theme_font_size_override("font_size", 9)
binding.add_theme_color_override("font_color", Color("a0b2c4")) binding.add_theme_color_override("font_color", Color("a0b2c4"))
@ -70,6 +71,10 @@ func _ready() -> void:
_refresh() _refresh()
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
var valid_drop := get_viewport().gui_is_dragging() and _can_drop_data(Vector2.ZERO, get_viewport().gui_get_drag_data())
if valid_drop != _drop_highlight:
_drop_highlight = valid_drop
_apply_slot_style(_inventory.get_item(slot_kind, slot_index) if _inventory != null else null)
_update_salvage_visibility() _update_salvage_visibility()
if slot_kind == "weapon" and _has_missile(): if slot_kind == "weapon" and _has_missile():
queue_redraw() queue_redraw()
@ -126,6 +131,12 @@ func _apply_slot_style(item: ItemInstance) -> void:
style.set_border_width_all(2) style.set_border_width_all(2)
if overlay_slot: if overlay_slot:
style.bg_color.a = 0.86 if item != null else 0.48 style.bg_color.a = 0.86 if item != null else 0.48
if _drop_highlight:
style.border_color = Color("75ffd0")
style.set_border_width_all(3)
style.bg_color = Color(0.08, 0.3, 0.25, 0.85)
style.shadow_color = Color(0.2, 1.0, 0.72, 0.3)
style.shadow_size = 5
add_theme_stylebox_override("panel", style) add_theme_stylebox_override("panel", style)
func _delete_item() -> void: func _delete_item() -> void:

View File

@ -1,5 +1,33 @@
extends "res://scripts/ui/captain_skill_button.gd" extends "res://scripts/ui/captain_skill_button.gd"
var _skill_notification := false
var _pulse_time := 0.0
var _pulse_frames: Dictionary = {}
func _process(delta: float) -> void:
if name != "OpenCaptainSkills":
return
var available := GameState.captain.learning_points > 0
if available and _pulse_frames.is_empty():
for state in ["normal", "hover", "pressed"]:
_pulse_frames[state] = get_theme_stylebox(state).duplicate()
if available:
_pulse_time += delta
var pulse := 0.5 + 0.5 * sin(_pulse_time * TAU / 1.6)
for state in _pulse_frames:
var frame := get_theme_stylebox(state) as StyleBoxFlat
frame.border_color = Color("b9a0ef").lerp(Color("ffe2a1"), pulse)
frame.shadow_color = Color(0.72, 0.5, 1.0, 0.25 + pulse * 0.4)
frame.shadow_size = int(7 + pulse * 9)
tooltip_text = "Umiejętności kapitana · punkty do wydania: %d" % GameState.captain.learning_points
elif _skill_notification:
for state in _pulse_frames:
add_theme_stylebox_override(state, _pulse_frames[state].duplicate())
_pulse_frames.clear()
_pulse_time = 0.0
tooltip_text = "Umiejętności kapitana"
_skill_notification = available
func configure_navigation(artwork: Texture2D, accent: Color, caption: String) -> void: func configure_navigation(artwork: Texture2D, accent: Color, caption: String) -> void:
configure(artwork, accent) configure(artwork, accent)
custom_minimum_size = Vector2(64, 80) custom_minimum_size = Vector2(64, 80)

View File

@ -70,9 +70,21 @@ func _ready() -> void:
assert(int(GameState.captain.minerals.get("iron", 0)) == before + 1) assert(int(GameState.captain.minerals.get("iron", 0)) == before + 1)
ore.call("_physics_process", 1.0) ore.call("_physics_process", 1.0)
assert(int(GameState.captain.minerals.get("iron", 0)) == before + 1) assert(int(GameState.captain.minerals.get("iron", 0)) == before + 1)
# Best duplicate wins; restoration is capped, not additive. # Duplicate extensions stay in cargo; replacing and moving equipped items is allowed.
recycler.passive_value = 40 recycler.passive_value = 40
equip("recycler", 3, 10) var duplicate := GENERATOR.generate_for_definition(load("res://resources/items/recycler.tres"), 1)
assert(inventory.add_item(duplicate))
var cargo_index := inventory.cargo.find(duplicate)
assert(not inventory.can_move_item("cargo", cargo_index, "extension", 3))
assert(not inventory.move_item("cargo", cargo_index, "extension", 3))
assert(inventory.extension_slots[3] == null and inventory.cargo[cargo_index] == duplicate)
assert(inventory.move_item("extension", 0, "extension", 3))
assert(inventory.move_item("extension", 3, "extension", 0))
assert(inventory.move_item("cargo", cargo_index, "extension", 0))
assert(inventory.extension_slots[0] == duplicate and inventory.cargo[cargo_index] == recycler)
assert(inventory.move_item("cargo", cargo_index, "extension", 0))
assert(inventory.move_item("extension", 0, "extension", 1))
assert(inventory.move_item("extension", 1, "extension", 0))
ship.energy = ship.max_energy * 0.9 ship.energy = ship.max_energy * 0.9
ship.shields = ship.max_shields * 0.9 ship.shields = ship.max_shields * 0.9
ship.recycle_asteroid_fragment() ship.recycle_asteroid_fragment()

View File

@ -0,0 +1,36 @@
extends Node
func _ready() -> void:
GameState.start_new_run()
var enemy := preload("res://scenes/game/enemy_ship.tscn").instantiate()
add_child(enemy)
enemy.set_physics_process(false)
enemy.max_hull = 100.0
enemy.max_shields = 200.0
enemy.hull = 9.0
enemy.shields = 0.0
assert(enemy.is_retreating())
enemy.regenerate_defenses(0.5)
assert(is_equal_approx(enemy.hull, 9.5) and is_equal_approx(enemy.shields, 1.0))
assert(enemy.is_retreating())
enemy.regenerate_defenses(0.5)
assert(is_equal_approx(enemy.hull, 10.0) and not enemy.is_retreating())
enemy.regenerate_defenses(1000.0)
assert(enemy.hull == enemy.max_hull and enemy.shields == enemy.max_shields)
enemy.hull = 0.0
enemy.regenerate_defenses(10.0)
assert(enemy.hull == 0.0)
var button := Button.new()
button.set_script(preload("res://scripts/ui/navigation_button.gd"))
button.configure_navigation(null, Color("b9a0ef"), "KAPITAN")
button.name = "OpenCaptainSkills"
add_child(button)
var original := (button.get_theme_stylebox("normal") as StyleBoxFlat).shadow_size
GameState.captain.learning_points = 1
button._process(0.4)
assert((button.get_theme_stylebox("normal") as StyleBoxFlat).shadow_size > original)
GameState.captain.learning_points = 0
button._process(0.1)
assert((button.get_theme_stylebox("normal") as StyleBoxFlat).shadow_size == original)
print("ENEMY_REPAIR_NOTIFICATION_TEST: PASS 1%/s, retreat threshold, caps, dead enemy, notification reset")
get_tree().quit()

View File

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

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/levels/enemy_repair_notification_test.gd" id="1"]
[node name="EnemyRepairNotificationTest" type="Node"]
script = ExtResource("1")

View File

@ -36,8 +36,6 @@ func _ready() -> void:
for poi in pois(game): for poi in pois(game):
if poi.get("kind") in ["asteroid", "wreck"]: if poi.get("kind") in ["asteroid", "wreck"]:
# Test reward retention independently of the debris renderer. # Test reward retention independently of the debris renderer.
if poi.get("kind") == "asteroid":
poi.set("_asteroid_sprite", null)
assert(poi.call("claim")) assert(poi.call("claim"))
var minerals_before := GameState.captain.minerals.duplicate() var minerals_before := GameState.captain.minerals.duplicate()
var seed_before := GameState.system_seed var seed_before := GameState.system_seed
@ -59,8 +57,11 @@ func _ready() -> void:
for poi in after: for poi in after:
var original: Dictionary = before[poi.get("poi_id")] var original: Dictionary = before[poi.get("poi_id")]
assert(original["kind"] == poi.get("kind")) assert(original["kind"] == poi.get("kind"))
if poi.get("kind") == "asteroid":
assert(original["position"] != poi.position)
elif int(str(poi.get("poi_id")).get_slice(":", 1)) < 1000:
assert(original["position"] == poi.position) assert(original["position"] == poi.position)
assert(original["mineral"] == poi.get("_mineral")) assert(GameState.asteroid_retry_seeds.has(seed_before))
assert(GameState.captain.minerals == minerals_before) assert(GameState.captain.minerals == minerals_before)
assert(GameState.captain.get_skill_rank("miner") == rank) assert(GameState.captain.get_skill_rank("miner") == rank)
assert(GameState.system_seed == seed_before) assert(GameState.system_seed == seed_before)
@ -70,5 +71,5 @@ func _ready() -> void:
get_tree().current_scene = self get_tree().current_scene = self
restarted.queue_free() restarted.queue_free()
await get_tree().process_frame await get_tree().process_frame
print("EXPLORATION_DEATH_RESTART_TEST: PASS (actual death and scene reload at miner ranks 0/1/2/3, exhausted boards restored, stable positions/minerals, explorer snapshot, rewards/equipment retained)") print("EXPLORATION_DEATH_RESTART_TEST: PASS (actual death and scene reload at miner ranks 0/1/2/3, exhausted boards restored, rerolled asteroid positions/minerals, explorer snapshot, rewards/equipment retained)")
get_tree().quit() get_tree().quit()

View File

@ -15,6 +15,8 @@ func _ready() -> void:
for skill in skills.IDS: for skill in skills.IDS:
if not bool(skills.DATA[skill]["active"]): if not bool(skills.DATA[skill]["active"]):
GameState.captain.skills[skill] = 1 GameState.captain.skills[skill] = 1
if "--skill-points" in OS.get_cmdline_user_args():
GameState.captain.learning_points = 1
GameState.captain.emit_changed() GameState.captain.emit_changed()
inventory.inventory_changed.emit() inventory.inventory_changed.emit()
var enemies := get_tree().get_nodes_in_group("enemies") var enemies := get_tree().get_nodes_in_group("enemies")

View File

@ -93,6 +93,13 @@ func _ready() -> void:
motion(origin + Vector2(24, 6)) motion(origin + Vector2(24, 6))
await get_tree().process_frame await get_tree().process_frame
var target: Control = hud.get("_sidebar").get_node("Scroll/Contents/ShipEquipmentPanel/Contents/EquipmentLayout/Module2") var target: Control = hud.get("_sidebar").get_node("Scroll/Contents/ShipEquipmentPanel/Contents/EquipmentLayout/Module2")
await get_tree().process_frame
assert(target.get("_drop_highlight"))
assert(not target.get_parent().get_node("Module1").get("_drop_highlight"))
assert(not target.get_parent().get_node("Extension1").get("_drop_highlight"))
var sidebar: Control = hud.get("_sidebar")
assert(not sidebar.get_node("Scroll").get_v_scroll_bar().visible)
assert(sidebar.position.y + sidebar.size.y * sidebar.scale.y <= 720.0)
var destination := target.get_global_rect().get_center() var destination := target.get_global_rect().get_center()
motion(destination) motion(destination)
await get_tree().process_frame await get_tree().process_frame