diff --git a/assets/icons/ui/captain_skills.svg b/assets/icons/ui/captain_skills.svg
new file mode 100644
index 0000000..185b217
--- /dev/null
+++ b/assets/icons/ui/captain_skills.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/ui/captain_skills.svg.import b/assets/icons/ui/captain_skills.svg.import
new file mode 100644
index 0000000..b4de6ae
--- /dev/null
+++ b/assets/icons/ui/captain_skills.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cn3fyvgfbwqye"
+path="res://.godot/imported/captain_skills.svg-cbdac3f3096be57c282840f44fa14935.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icons/ui/captain_skills.svg"
+dest_files=["res://.godot/imported/captain_skills.svg-cbdac3f3096be57c282840f44fa14935.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
diff --git a/assets/icons/ui/crafting.svg b/assets/icons/ui/crafting.svg
new file mode 100644
index 0000000..c71ad63
--- /dev/null
+++ b/assets/icons/ui/crafting.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/ui/crafting.svg.import b/assets/icons/ui/crafting.svg.import
new file mode 100644
index 0000000..ea0dafc
--- /dev/null
+++ b/assets/icons/ui/crafting.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cn00i5tkhcxyw"
+path="res://.godot/imported/crafting.svg-c0cc0c27f763904bea856d7dc30d600d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icons/ui/crafting.svg"
+dest_files=["res://.godot/imported/crafting.svg-c0cc0c27f763904bea856d7dc30d600d.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
diff --git a/scripts/captain/captain_progress.gd b/scripts/captain/captain_progress.gd
index e7b290c..1b0f0ed 100644
--- a/scripts/captain/captain_progress.gd
+++ b/scripts/captain/captain_progress.gd
@@ -9,9 +9,7 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd")
@export var parts: float = 0.0
@export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0}
@export var learning_points: int = 0
-@export var skills: Dictionary = {
- "piloting": 0, "gunner": 0, "mechanic": 0, "sharp_eye": 0,
-}
+@export var skills: Dictionary = {}
func get_xp_required() -> int:
return BALANCE.xp_required(level)
@@ -19,20 +17,17 @@ func get_xp_required() -> int:
func get_skill_rank(skill: String) -> int:
return clampi(int(skills.get(skill, 0)), 0, BALANCE.MAX_SKILL_RANK)
-func get_skill_bonus(skill: String) -> float:
- return BALANCE.skill_bonus(skill, get_skill_rank(skill))
+func get_skill_bonus(_skill: String) -> float:
+ return 0.0 # The old captain skill system has been retired.
func normalize_skill_ranks() -> void:
- var changed_ranks := false
- for skill in BALANCE.SKILL_NAMES:
- var previous := maxi(0, int(skills.get(skill, 0)))
- var rank := mini(previous, BALANCE.MAX_SKILL_RANK)
- learning_points += previous - rank
- if int(skills.get(skill, 0)) != rank:
- changed_ranks = true
- skills[skill] = rank
- if changed_ranks:
+ # Refund legacy ranks for the future captain skill system.
+ var refunded := get_spent_learning_points()
+ if refunded > 0:
+ learning_points += refunded
+ skills.clear()
emit_changed()
+ return
func get_spent_learning_points() -> int:
var spent := 0
@@ -56,8 +51,8 @@ func reset_skills() -> bool:
emit_changed()
return true
-func can_train(skill: String) -> bool:
- return skills.has(skill) and learning_points > 0 and get_skill_rank(skill) < BALANCE.MAX_SKILL_RANK
+func can_train(_skill: String) -> bool:
+ return false
func train_skill(skill: String) -> bool:
if not can_train(skill):
diff --git a/scripts/game/asteroid_destruction_effect.gd b/scripts/game/asteroid_destruction_effect.gd
index c739195..00a4c2a 100644
--- a/scripts/game/asteroid_destruction_effect.gd
+++ b/scripts/game/asteroid_destruction_effect.gd
@@ -1,6 +1,6 @@
extends Node2D
-const DURATION := 1.6
+const SETTLE_TIME := 8.0
var _texture: Texture2D
var _fragments: Array[Dictionary] = []
var _dust: Array[Dictionary] = []
@@ -24,24 +24,36 @@ func configure(sprite: Sprite2D) -> void:
if y > 0 and y < 4:
vertex.y += _rng.randf_range(-0.3, 0.3) * dimensions.y / 4.0
grid.append(vertex)
+ # Shared jagged cracks keep neighbouring pieces fitted together.
+ var horizontal: Array[PackedVector2Array] = []
+ var vertical: Array[PackedVector2Array] = []
+ for y in 5:
+ for x in 4:
+ horizontal.append(_fracture_edge(grid[y * 5 + x], grid[y * 5 + x + 1], y > 0 and y < 4))
+ for y in 4:
+ for x in 5:
+ vertical.append(_fracture_edge(grid[y * 5 + x], grid[(y + 1) * 5 + x], x > 0 and x < 4))
for y in 4:
for x in 4:
- var a := grid[y * 5 + x]
- var b := grid[y * 5 + x + 1]
- var c := grid[(y + 1) * 5 + x + 1]
- var d := grid[(y + 1) * 5 + x]
- for triangle in [[a, b, c], [a, c, d]]:
- var center: Vector2 = (triangle[0] + triangle[1] + triangle[2]) / 3.0
- var origin := (center - dimensions * 0.5) * sprite_scale
- var points := PackedVector2Array()
- var uv := PackedVector2Array()
- for vertex: Vector2 in triangle:
- points.append((vertex - center) * sprite_scale)
- uv.append(vertex / dimensions)
- var direction := origin.normalized() if origin.length_squared() > 0.1 else Vector2.RIGHT
- _fragments.append({"points": points, "uv": uv, "position": origin,
- "velocity": direction.rotated(_rng.randf_range(-0.35, 0.35)) * _rng.randf_range(70.0, 170.0),
- "angle": 0.0, "spin": _rng.randf_range(-4.0, 4.0)})
+ var outline := PackedVector2Array()
+ outline.append_array(_edge_vertices(horizontal[y * 4 + x], false))
+ outline.append_array(_edge_vertices(vertical[y * 5 + x + 1], false))
+ outline.append_array(_edge_vertices(horizontal[(y + 1) * 4 + x], true))
+ outline.append_array(_edge_vertices(vertical[y * 5 + x], true))
+ var center := Vector2.ZERO
+ for vertex in outline:
+ center += vertex
+ center /= outline.size()
+ var origin := (center - dimensions * 0.5) * sprite_scale
+ var points := PackedVector2Array()
+ var uv := PackedVector2Array()
+ for vertex in outline:
+ points.append((vertex - center) * sprite_scale)
+ uv.append(vertex / dimensions)
+ var direction := origin.normalized() if origin.length_squared() > 0.1 else Vector2.RIGHT
+ _fragments.append({"points": points, "uv": uv, "position": origin,
+ "velocity": direction.rotated(_rng.randf_range(-0.35, 0.35)) * _rng.randf_range(40.0, 95.0),
+ "angle": 0.0, "spin": _rng.randf_range(-4.0, 4.0)})
_dust_color = Color("a1c7df") if "ice" in _texture.resource_path else Color("b4a99a")
for index in 28:
_dust.append({"direction": Vector2.RIGHT.rotated(_rng.randf_range(0.0, TAU)),
@@ -49,22 +61,43 @@ func configure(sprite: Sprite2D) -> void:
z_index = 35
light_mask = 4
+func _fracture_edge(start: Vector2, end: Vector2, jagged: bool) -> PackedVector2Array:
+ var edge := PackedVector2Array([start])
+ if jagged:
+ var normal := (end - start).orthogonal().normalized()
+ var roughness := start.distance_to(end) * 0.13
+ var side := 1.0 if _rng.randf() > 0.5 else -1.0
+ for index in 3:
+ var along := (index + 1.0) / 4.0 + _rng.randf_range(-0.05, 0.05)
+ edge.append(start.lerp(end, along) + normal * roughness * side * _rng.randf_range(0.5, 1.0))
+ side *= -1.0
+ edge.append(end)
+ return edge
+
+func _edge_vertices(edge: PackedVector2Array, backwards: bool) -> PackedVector2Array:
+ var vertices := PackedVector2Array()
+ # The next edge supplies the end vertex, avoiding duplicate polygon points.
+ for index in edge.size() - 1:
+ vertices.append(edge[edge.size() - 1 - index] if backwards else edge[index])
+ return vertices
+
func _process(delta: float) -> void:
_elapsed += delta
- if _elapsed >= DURATION:
- queue_free()
- return
for fragment in _fragments:
fragment.position += fragment.velocity * delta
fragment.velocity *= exp(-1.2 * delta)
fragment.angle += fragment.spin * delta
+ fragment.spin *= exp(-0.7 * delta)
queue_redraw()
+ # Leave the settled debris visible without updating it every frame.
+ if _elapsed >= SETTLE_TIME:
+ _dust.clear()
+ set_process(false)
func _draw() -> void:
- var fade := 1.0 - smoothstep(0.65, DURATION, _elapsed)
for fragment in _fragments:
draw_set_transform(fragment.position, fragment.angle)
- draw_polygon(fragment.points, PackedColorArray([Color(1.0, 1.0, 1.0, fade)]), fragment.uv, _texture)
+ draw_polygon(fragment.points, PackedColorArray([Color.WHITE]), fragment.uv, _texture)
draw_set_transform(Vector2.ZERO)
var dust_fade := maxf(0.0, 1.0 - _elapsed / 0.8)
for mote in _dust:
diff --git a/scripts/game/flight_hud.gd b/scripts/game/flight_hud.gd
index a4d9710..886e20e 100644
--- a/scripts/game/flight_hud.gd
+++ b/scripts/game/flight_hud.gd
@@ -24,6 +24,89 @@ var _debug_item_button: Button
var _debug_item_status: Label
var _captain_panel: PanelContainer
var _cockpit: Control
+var _crafting_window: PanelContainer
+var _skills_window: PanelContainer
+
+func _create_navigation() -> void:
+ var navigation := HBoxContainer.new()
+ navigation.name = "TopNavigation"
+ $HUDRoot.add_child(navigation)
+ navigation.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
+ navigation.offset_left = -60
+ navigation.offset_right = 60
+ navigation.offset_top = 12
+ navigation.offset_bottom = 64
+ navigation.add_theme_constant_override("separation", 8)
+ _crafting_window = _create_window("CraftingWindow", "TWÓRZ", Vector2(440, 250))
+ var crafting := PanelContainer.new()
+ crafting.name = "CraftingPanel"
+ crafting.set_script(preload("res://scripts/ui/ship_crafting_panel.gd"))
+ crafting.set("standalone", true)
+ _crafting_window.get_node("Contents").add_child(crafting)
+ _skills_window = _create_window("CaptainSkillsWindow", "UMIEJĘTNOŚCI KAPITANA", Vector2(480, 300))
+ var empty := Control.new()
+ empty.name = "SkillsContent"
+ empty.custom_minimum_size = Vector2(440, 230)
+ _skills_window.get_node("Contents").add_child(empty)
+ for entry in [["Twórz", _crafting_window, "res://assets/icons/ui/crafting.svg", Color("efb568")], ["Umiejętności kapitana", _skills_window, "res://assets/icons/ui/captain_skills.svg", Color("b9a0ef")]]:
+ var button := Button.new()
+ button.tooltip_text = entry[0]
+ button.icon = load(entry[2]) as Texture2D
+ button.expand_icon = true
+ button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ button.custom_minimum_size = Vector2(56, 52)
+ button.add_theme_constant_override("icon_max_width", 36)
+ preload("res://scripts/ui/hud_button_style.gd").apply(button, entry[3])
+ button.add_theme_constant_override("h_separation", 0)
+ for state in ["normal", "hover", "pressed", "disabled"]:
+ var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat
+ style.set_content_margin_all(8)
+ button.add_theme_stylebox_override(state, style)
+ navigation.add_child(button)
+ button.pressed.connect(_toggle_window.bind(entry[1]))
+
+func _create_window(node_name: String, title: String, dimensions: Vector2) -> PanelContainer:
+ var window := PanelContainer.new()
+ window.name = node_name
+ var style := StyleBoxFlat.new()
+ style.bg_color = Color("0b1520fa")
+ style.border_color = Color("376779")
+ style.set_border_width_all(1)
+ style.set_corner_radius_all(8)
+ style.set_content_margin_all(16)
+ window.add_theme_stylebox_override("panel", style)
+ $HUDRoot.add_child(window)
+ window.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
+ window.offset_left = -dimensions.x * 0.5
+ window.offset_right = dimensions.x * 0.5
+ window.offset_top = -dimensions.y * 0.5
+ window.offset_bottom = dimensions.y * 0.5
+ var contents := VBoxContainer.new()
+ contents.name = "Contents"
+ contents.add_theme_constant_override("separation", 12)
+ window.add_child(contents)
+ var header := HBoxContainer.new()
+ contents.add_child(header)
+ var label := Label.new()
+ label.text = title
+ label.add_theme_font_size_override("font_size", 14)
+ label.add_theme_color_override("font_color", Color("91dbea"))
+ label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ header.add_child(label)
+ var close := Button.new()
+ close.text = "×"
+ close.tooltip_text = "Zamknij"
+ close.custom_minimum_size = Vector2(32, 28)
+ close.pressed.connect(window.hide)
+ header.add_child(close)
+ window.hide()
+ return window
+
+func _toggle_window(window: PanelContainer) -> void:
+ var opening := not window.visible
+ _crafting_window.hide()
+ _skills_window.hide()
+ window.visible = opening
func _ready() -> void:
_sidebar_open = bool(GameState.hud_panel_state.get("ship_open", false))
@@ -43,10 +126,6 @@ func _ready() -> void:
# The display font has unusually tall vertical metrics; keep debug rows compact.
debug_info.add_theme_constant_override("line_spacing", mini(0, 13 - ceili(debug_info.get_theme_font("font").get_height(10))))
_create_sidebar()
- var enemy_panel := Control.new()
- enemy_panel.name = "EnemyDrawer"
- enemy_panel.set_script(preload("res://scripts/ui/enemy_drawer.gd"))
- $HUDRoot.add_child(enemy_panel)
_inventory_clip = Control.new()
_inventory_clip.name = "InventoryDrawer"
_inventory_clip.clip_contents = true
@@ -121,23 +200,21 @@ func _create_sidebar() -> void:
equipment.reparent(contents, false)
equipment.size_flags_horizontal = Control.SIZE_EXPAND_FILL
equipment.add_theme_stylebox_override("panel", StyleBoxEmpty.new())
- var crafting := PanelContainer.new()
- crafting.name = "CraftingPanel"
- crafting.set_script(preload("res://scripts/ui/ship_crafting_panel.gd"))
- equipment.get_node("Contents").add_child(crafting)
contents.minimum_size_changed.connect(func() -> void: _layout_sidebar.call_deferred())
_sidebar_toggle = Button.new()
+ _sidebar_toggle.set_script(preload("res://scripts/ui/ship_profile.gd"))
+ _sidebar_toggle.set("player", player)
_sidebar_toggle.name = "ShipToggle"
- _sidebar_toggle.text = "STATEK ▾" if _sidebar_open else "STATEK ◀"
+ _sidebar_toggle.tooltip_text = "Zamknij okno statku" if _sidebar_open else "Otwórz statek i sloty wyposażenia"
_sidebar_toggle.add_theme_font_size_override("font_size", 12)
preload("res://scripts/ui/hud_button_style.gd").apply(_sidebar_toggle)
preload("res://scripts/ui/hud_button_style.gd").apply(inventory_button, Color("b9a0ef"))
$HUDRoot.add_child(_sidebar_toggle)
_sidebar_toggle.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
- _sidebar_toggle.offset_left = -180
+ _sidebar_toggle.offset_left = -352
_sidebar_toggle.offset_right = -12
_sidebar_toggle.offset_top = 12
- _sidebar_toggle.offset_bottom = 48
+ _sidebar_toggle.offset_bottom = 122
_sidebar_toggle.pressed.connect(_toggle_sidebar)
_cockpit = Control.new()
_cockpit.name = "Cockpit"
@@ -145,6 +222,7 @@ func _create_sidebar() -> void:
_cockpit.set("player", player)
_cockpit.set("game", get_parent())
$HUDRoot.add_child(_cockpit)
+ _create_navigation()
_layout_debug()
func _layout_debug() -> void:
@@ -165,7 +243,7 @@ func _toggle_sidebar() -> void:
GameState.hud_panel_state["ship_open"] = _sidebar_open
if _sidebar_tween != null and _sidebar_tween.is_running():
_sidebar_tween.kill()
- _sidebar_toggle.text = "STATEK ▾" if _sidebar_open else "STATEK ◀"
+ _sidebar_toggle.tooltip_text = "Zamknij okno statku" if _sidebar_open else "Otwórz statek i sloty wyposażenia"
if _sidebar_open:
_sidebar.show()
else:
@@ -181,16 +259,16 @@ func _layout_sidebar() -> void:
if _sidebar_tween != null and _sidebar_tween.is_running():
_sidebar_tween.kill()
_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 - 64.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) / EQUIPMENT_UI_SCALE)))
_cockpit.scale = Vector2.ONE * 0.85
- _cockpit.size = Vector2(minf(640.0, ($HUDRoot.size.x - 24) / 0.85), 104)
+ _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)
if _ability_bar != null:
var abilities := _ability_bar.get_parent() as Control
abilities.offset_bottom = _cockpit.position.y - $HUDRoot.size.y - 14.0
abilities.offset_top = abilities.offset_bottom - 44.0
_layout_debug()
- _sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 56)
+ _sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 136)
_sidebar.visible = _sidebar_open
_inventory_clip.size = inventory_panel.size * EQUIPMENT_UI_SCALE
_layout_inventory()
diff --git a/scripts/game/ship_energy_indicator.gd b/scripts/game/ship_energy_indicator.gd
index 054eb3c..8880023 100644
--- a/scripts/game/ship_energy_indicator.gd
+++ b/scripts/game/ship_energy_indicator.gd
@@ -20,6 +20,9 @@ func _ready() -> void:
_background.bg_color = Color("101925")
_background.border_color = Color("456277")
_background.set_border_width_all(1)
+ _background.set_corner_radius_all(4)
+ _background.shadow_color = Color(0, 0, 0, 0.5)
+ _background.shadow_size = 3
func _physics_process(_delta: float) -> void:
if is_instance_valid(player):
@@ -58,13 +61,26 @@ func _hull_ratio() -> float:
func _draw() -> void:
if not is_instance_valid(player):
return
- var color := Color("71e3ff") if _energy_ratio() >= 0.25 else Color("ffbd66")
- var bar_position := Vector2(-44, 48)
- draw_style_box(_background, Rect2(bar_position - Vector2(2, 2), Vector2(92, 10)))
- draw_rect(Rect2(bar_position, Vector2(88 * _display_ratio, 6)), color)
- var shield_bar_position := bar_position + Vector2(0, 14)
- draw_style_box(_background, Rect2(shield_bar_position - Vector2(2, 2), Vector2(92, 10)))
- draw_rect(Rect2(shield_bar_position, Vector2(88 * _display_shield_ratio, 6)), Color.WHITE)
- var hull_bar_position := bar_position + Vector2(0, 28)
- draw_style_box(_background, Rect2(hull_bar_position - Vector2(2, 2), Vector2(92, 10)))
- draw_rect(Rect2(hull_bar_position, Vector2(88 * _display_hull_ratio, 6)), Color("ef5967"))
+ _draw_bar(Vector2(-46, 58), _display_ratio, Color("efbd62"), 0)
+ _draw_bar(Vector2(-46, 72), _display_shield_ratio, Color("66cfff"), 1)
+ _draw_bar(Vector2(-46, 86), _display_hull_ratio, Color("ef6875"), 2)
+
+func _draw_bar(position: Vector2, ratio: float, color: Color, kind: int) -> void:
+ draw_style_box(_background, Rect2(position - Vector2(14, 3), Vector2(110, 12)))
+ # Ten illuminated cells, with partial cells tracking the exact current value.
+ for index in 10:
+ var cell := Rect2(position + Vector2(index * 9.2, 0), Vector2(7.6, 6))
+ draw_rect(cell, color.darkened(0.8))
+ var fraction := clampf(ratio * 10.0 - index, 0.0, 1.0)
+ if fraction > 0:
+ cell.size.x *= fraction
+ draw_rect(cell, color)
+ draw_rect(Rect2(cell.position, Vector2(cell.size.x, 2)), color.lightened(0.4))
+ var center := position + Vector2(-8, 3)
+ match kind:
+ 0:
+ draw_polyline(PackedVector2Array([center + Vector2(1, -4), center + Vector2(-2, 0), center + Vector2(2, 0), center + Vector2(-1, 4)]), color, 1.4, true)
+ 1:
+ draw_polyline(PackedVector2Array([center + Vector2(-3, -3), center + Vector2(3, -3), center + Vector2(2, 2), center + Vector2(0, 4), center + Vector2(-2, 2), center + Vector2(-3, -3)]), color, 1.2, true)
+ 2:
+ draw_polyline(PackedVector2Array([center + Vector2(0, -4), center + Vector2(3, 0), center + Vector2(2, 3), center + Vector2(-2, 3), center + Vector2(-3, 0), center + Vector2(0, -4)]), color, 1.2, true)
diff --git a/scripts/levels/celestial_body.gd b/scripts/levels/celestial_body.gd
index 52aabbb..935d9db 100644
--- a/scripts/levels/celestial_body.gd
+++ b/scripts/levels/celestial_body.gd
@@ -11,6 +11,8 @@ var _label: Label
var _sprite: Sprite2D
func _ready() -> void:
+ if not is_star:
+ add_to_group("scanner_planets")
var sprite := Sprite2D.new()
_sprite = sprite
sprite.name = "BodySprite"
diff --git a/scripts/levels/exploration_poi.gd b/scripts/levels/exploration_poi.gd
index dc30adf..bd9acd4 100644
--- a/scripts/levels/exploration_poi.gd
+++ b/scripts/levels/exploration_poi.gd
@@ -119,8 +119,8 @@ func claim() -> bool:
elif kind == "ruin":
var xp := roundi(_rng.randi_range(10, 20) * scale)
GameState.captain.add_rewards(xp, 0)
- var module := _drop_module(true)
- summary = "%d XP oraz %s (poziom %d)" % [xp, module.definition.display_name, module.item_level]
+ _drop_module(true)
+ summary = "+%d doświadczenia · +1 moduł" % xp
_show_discovery(summary)
else:
match _rng.randi_range(0, 3):
@@ -163,28 +163,29 @@ func _show_discovery(reward: String) -> void:
layer.layer = 30
layer.name = "AncientDiscoveryMessage"
get_parent().add_child(layer)
- var panel := PanelContainer.new()
- panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
- layer.add_child(panel)
- panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
- panel.offset_left = -300
- panel.offset_right = 300
- panel.offset_top = 70
var label := Label.new()
- label.custom_minimum_size = Vector2(600, 95)
- label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
+ layer.add_child(label)
+ label.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
+ label.offset_left = -300
+ label.offset_right = 300
+ label.offset_top = 145
+ label.offset_bottom = 181
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
- label.add_theme_font_size_override("font_size", 18)
- label.add_theme_color_override("font_color", Color("ffd166"))
- label.text = "Znalazłeś ślady starożytnej upadłej cywilizacji!\nZdobyłeś: " + reward
+ label.add_theme_font_size_override("font_size", 15)
+ label.add_theme_color_override("font_color", Color("d8d4b7"))
+ label.add_theme_color_override("font_shadow_color", Color(0.0, 0.0, 0.0, 0.85))
+ label.add_theme_constant_override("shadow_offset_x", 1)
+ label.add_theme_constant_override("shadow_offset_y", 2)
+ label.text = reward
if _module_left_outside:
- label.text += "\nMagazyn pełny — moduł czeka obok planety."
- panel.add_child(label)
+ label.text += " (obok planety)"
+ label.modulate.a = 0.0
var tween := layer.create_tween()
- tween.tween_interval(7.0)
- tween.tween_property(panel, "modulate:a", 0.0, 0.5)
+ tween.tween_property(label, "modulate:a", 1.0, 0.35)
+ tween.tween_interval(3.5)
+ tween.tween_property(label, "modulate:a", 0.0, 0.8)
tween.tween_callback(layer.queue_free)
func _drop_module(ancient: bool) -> ItemInstance:
diff --git a/scripts/ui/captain_panel.gd b/scripts/ui/captain_panel.gd
index 9a8d287..e669e59 100644
--- a/scripts/ui/captain_panel.gd
+++ b/scripts/ui/captain_panel.gd
@@ -12,55 +12,17 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd")
var _collapsed := false
var _collapse_button: Button
var _captain: CaptainProgress
-var _rank_labels: Dictionary = {}
-var _effect_labels: Dictionary = {}
-var _buttons: Dictionary = {}
func _ready() -> void:
_build_portrait()
_captain = GameState.captain
_captain.normalize_skill_ranks()
- for skill in BALANCE.SKILL_NAMES:
- var row := HBoxContainer.new()
- row.name = skill
- row.add_theme_constant_override("separation", 5)
- var title := Label.new()
- title.text = BALANCE.SKILL_NAMES[skill]
- title.tooltip_text = BALANCE.SKILL_HELP[skill]
- title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- title.add_theme_font_size_override("font_size", 11)
- row.add_child(title)
- var rank := Label.new()
- rank.custom_minimum_size.x = 40
- rank.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
- rank.add_theme_font_size_override("font_size", 11)
- row.add_child(rank)
- _rank_labels[skill] = rank
- var effect := Label.new()
- effect.custom_minimum_size.x = 74
- effect.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
- effect.add_theme_font_size_override("font_size", 10)
- row.add_child(effect)
- _effect_labels[skill] = effect
- var button := Button.new()
- button.name = "Train_" + skill
- button.text = "+"
- button.custom_minimum_size = Vector2(24, 22)
- button.add_theme_font_size_override("font_size", 13)
- button.tooltip_text = "Rozwiń umiejętność za 1 punkt nauki.\n" + BALANCE.SKILL_HELP[skill]
- button.pressed.connect(_train.bind(skill))
- row.add_child(button)
- _buttons[skill] = button
- $Contents/Skills.add_child(row)
_captain.changed.connect(_refresh)
player.get_node("ShipInventory").connect("inventory_changed", _refresh)
_refresh()
if bool(GameState.hud_panel_state.get("captain_collapsed", true)):
_toggle_collapsed()
-func _train(skill: String) -> void:
- _captain.train_skill(skill)
-
func _refresh() -> void:
level_label.text = "POZ. %d" % _captain.level
xp_label.text = "XP %d / %d" % [_captain.experience, _captain.get_xp_required()]
@@ -68,15 +30,6 @@ func _refresh() -> void:
xp_bar.value = _captain.experience
credits_label.text = "Kredyty: %d" % _captain.credits
points_label.text = "Punkty: %d" % _captain.learning_points
- for skill in _buttons:
- var rank := _captain.get_skill_rank(skill)
- _rank_labels[skill].text = "%d/%d" % [rank, BALANCE.MAX_SKILL_RANK]
- var suffix: String = {"piloting": "unik", "gunner": "DMG", "mechanic": "red.", "sharp_eye": "kryt"}[skill]
- var bonus := _captain.get_skill_bonus(skill)
- _effect_labels[skill].text = "%.1f%% %s" % [bonus * 100.0, suffix]
- var next_bonus := BALANCE.skill_bonus(skill, rank + 1)
- _buttons[skill].tooltip_text = BALANCE.SKILL_HELP[skill] + ("\nOsiągnięto limit 20 punktów." if rank >= BALANCE.MAX_SKILL_RANK else "\nNastępny punkt: %.1f%% → %.1f%%" % [bonus * 100.0, next_bonus * 100.0])
- _buttons[skill].disabled = not _captain.can_train(skill)
func _build_portrait() -> void:
custom_minimum_size = Vector2(320, 0)
diff --git a/scripts/ui/enemy_drawer.gd b/scripts/ui/enemy_drawer.gd
index f941204..9c3afec 100644
--- a/scripts/ui/enemy_drawer.gd
+++ b/scripts/ui/enemy_drawer.gd
@@ -1,49 +1,17 @@
extends Control
-var _panel: PanelContainer
var _label: Label
-var _toggle: Button
-var _open := false
-var _tween: Tween
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
- set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
- _panel = PanelContainer.new()
- var style := StyleBoxFlat.new()
- style.bg_color = Color("0b1520f5")
- style.border_color = Color("a54e61")
- style.set_border_width_all(1)
- style.set_content_margin_all(12)
- _panel.add_theme_stylebox_override("panel", style)
- add_child(_panel)
_label = Label.new()
- _label.add_theme_font_size_override("font_size", 12)
- _panel.add_child(_label)
- _toggle = Button.new()
- _toggle.text = "WRÓG ▾"
- preload("res://scripts/ui/hud_button_style.gd").apply(_toggle, Color("ff9aae"))
- add_child(_toggle)
- _toggle.pressed.connect(_toggle_drawer)
- _open = bool(GameState.hud_panel_state.get("enemy_open", false))
- get_viewport().size_changed.connect(_layout)
- _layout()
-
-func _layout() -> void:
- position.x = (get_viewport_rect().size.x - 390.0) * 0.5
- _panel.size = Vector2(390, 150)
- _panel.position.y = 56.0 if _open else -160.0
- _toggle.position = Vector2(130, 12)
- _toggle.size = Vector2(130, 36)
-
-func _toggle_drawer() -> void:
- _open = not _open
- GameState.hud_panel_state["enemy_open"] = _open
- _toggle.text = "WRÓG ▴" if _open else "WRÓG ▾"
- if _tween != null:
- _tween.kill()
- _tween = create_tween()
- _tween.tween_property(_panel, "position:y", 56.0 if _open else -160.0, 0.25).set_trans(Tween.TRANS_CUBIC)
+ _label.name = "EnemyDetails"
+ _label.mouse_filter = Control.MOUSE_FILTER_IGNORE
+ _label.add_theme_font_size_override("font_size", 9)
+ _label.add_theme_color_override("font_color", Color("c9acb5"))
+ _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ _label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
+ add_child(_label)
func _process(_delta: float) -> void:
var enemy: Node = null
@@ -53,8 +21,8 @@ func _process(_delta: float) -> void:
var profile := DamageTypes.enemy_profile(GameState.system_level, GameState.system_seed)
var kind := int(profile["kind"])
var names := ["Działko plazmowe", "Działko maszynowe", "Laser", "Rakiety", "Działko gamma"]
- _label.text = "WRÓG · UKŁAD %d\n%s · %s\n%s" % [GameState.system_level, names[kind], DamageTypes.NAMES[DamageTypes.for_weapon(kind)], DamageTypes.describe(profile["absorption"])]
+ _label.text = "%s\n%s" % [names[kind], DamageTypes.describe(profile["absorption"])]
if enemy != null:
- _label.text += "\nDPS: %.1f (%s)\nTarcze: %.0f / %.0f · bez regeneracji" % [enemy.get_weapon_dps(), ["zwykły", "elita", "boss"][enemy.loot_tier], enemy.shields, enemy.max_shields]
+ _label.text += "\nDPS: %.1f (%s)\nTarcze: %.0f / %.0f" % [enemy.get_weapon_dps(), ["zwykły", "elita", "boss"][enemy.loot_tier], enemy.shields, enemy.max_shields]
else:
_label.text += "\nUkład oczyszczony"
diff --git a/scripts/ui/flight_cockpit.gd b/scripts/ui/flight_cockpit.gd
index fa43c84..836eebc 100644
--- a/scripts/ui/flight_cockpit.gd
+++ b/scripts/ui/flight_cockpit.gd
@@ -6,11 +6,16 @@ const WEAPON_DISPLAY_ORDER := [0, 2, 1]
const AMBER := Color("efb568")
var player: CharacterBody2D
var game: Node2D
+var _enemy_details: Control
var _inventory: ShipInventory
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
_inventory = player.get_node("ShipInventory")
+ _enemy_details = Control.new()
+ _enemy_details.name = "EnemyDetailsPanel"
+ _enemy_details.set_script(preload("res://scripts/ui/enemy_drawer.gd"))
+ add_child(_enemy_details)
func _process(delta: float) -> void:
if is_instance_valid(player):
@@ -18,6 +23,9 @@ func _process(delta: float) -> void:
var local := get_global_transform().affine_inverse() * screen
var overlaps := Rect2(Vector2.ZERO, size).grow(36).has_point(local)
modulate.a = move_toward(modulate.a, 0.18 if overlaps else 1.0, delta * 4.0)
+ var cell := (size.x - 238) / 3.0
+ _enemy_details.position = Vector2(218.0 + 2.0 * cell, 57)
+ _enemy_details.size = Vector2(cell, 56)
queue_redraw()
func _text(value: String, point: Vector2, width: float, font_size: int, color: Color) -> void:
@@ -34,7 +42,9 @@ func _draw() -> void:
draw_line(Vector2(28, 2), Vector2(size.x - 28, 2), CYAN, 2, true)
# Put weapon cards beside the counters rather than stacking three rows.
_text("DPS: %.1f" % _inventory.get_total_dps(), Vector2(20, 81), 172, 12, CYAN)
- _text(DamageTypes.describe(player.get("absorption")), Vector2(205, 81), size.x - 225, 11, Color("a9d69a"))
+ var absorption: Vector3 = player.get("absorption")
+ for index in 3:
+ _text("%s - %.1f%%" % [DamageTypes.NAMES[index], absorption[index]], Vector2(205, 77 + index * 15), 2.0 * (size.x - 238) / 3.0, 11, Color("a9d69a"))
var cell := (size.x - 238) / 3.0
var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))]
var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"]
diff --git a/scripts/ui/planet_services_menu.gd b/scripts/ui/planet_services_menu.gd
index 91724c9..0a0f16c 100644
--- a/scripts/ui/planet_services_menu.gd
+++ b/scripts/ui/planet_services_menu.gd
@@ -26,7 +26,7 @@ func _ready() -> void:
_gamma_buy.pressed.connect(func() -> void: _inventory.buy_irradiated_ammunition())
next_button.pressed.connect(func() -> void: next_level_requested.emit())
buy_ammunition_button.pressed.connect(_buy_ammunition)
- reset_skills_button.pressed.connect(_reset_skills)
+ reset_skills_button.hide()
_missile_button = Button.new()
_missile_button.custom_minimum_size.y = 36
_missile_button.add_theme_font_size_override("font_size", 12)
diff --git a/scripts/ui/protected_planet_marker.gd b/scripts/ui/protected_planet_marker.gd
index aa2c2f4..630bd73 100644
--- a/scripts/ui/protected_planet_marker.gd
+++ b/scripts/ui/protected_planet_marker.gd
@@ -10,9 +10,6 @@ func _process(_delta: float) -> void:
func _draw() -> void:
if not is_instance_valid(game) or not is_instance_valid(game.get("protected_planet")):
return
- var planet: Node2D = game.get("protected_planet")
- var color := Color("77e8bc") if bool(game.get("system_cleared")) else Color("ffbd83")
- _draw_target(planet.global_position, str(planet.get("display_name")) + " — zamieszkana", color)
var portal: Node2D = game.get("wormhole")
if is_instance_valid(portal):
_draw_target(portal.global_position, "WORMHOLE — następny układ", Color("89e8ff"))
diff --git a/scripts/ui/scanner_overlay.gd b/scripts/ui/scanner_overlay.gd
index ef45940..70534d6 100644
--- a/scripts/ui/scanner_overlay.gd
+++ b/scripts/ui/scanner_overlay.gd
@@ -1,21 +1,45 @@
extends Control
-const FONT := preload("res://assets/fonts/font2.ttf")
+const DOT_RADIUS := 3.0
+const SCAN_RING_RADIUS := 48.0
var player: CharacterBody2D
var targets: Array[Node2D] = []
+var _elapsed := 0.0
+var _previous_player_position := Vector2.ZERO
+var _player_position := Vector2.ZERO
+var _positions: Dictionary = {}
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
+ process_physics_priority = 10
+ _player_position = player.global_position
+ _previous_player_position = _player_position
-func _process(_delta: float) -> void:
+func _process(delta: float) -> void:
+ _elapsed += delta
+ queue_redraw()
+
+func _physics_process(_delta: float) -> void:
if not is_instance_valid(player):
return
+ _previous_player_position = _player_position
+ _player_position = player.global_position
+ if _previous_player_position.distance_to(_player_position) > 600.0:
+ _previous_player_position = _player_position
targets.clear()
- for group in ["enemies", "exploration_pois"]:
+ var next_positions: Dictionary = {}
+ for group in ["enemies", "exploration_pois", "scanner_planets"]:
for target in get_tree().get_nodes_in_group(group):
- if player.get_parent().is_ancestor_of(target) and is_target_in_range(target):
+ if player.get_parent().is_ancestor_of(target) and not target.is_queued_for_deletion() and is_target_in_range(target):
+ if group == "scanner_planets" and (not bool(target.get("is_protected")) or not bool(player.get_parent().get("system_cleared"))):
+ continue
targets.append(target)
- queue_redraw()
+ var current: Vector2 = target.global_position
+ var previous: Vector2 = _positions.get(target.get_instance_id(), [current, current])[1]
+ if previous.distance_to(current) > 600.0:
+ previous = current
+ next_positions[target.get_instance_id()] = [previous, current]
+ _positions = next_positions
func get_scan_range() -> float:
var transform := get_viewport().get_canvas_transform()
@@ -29,38 +53,39 @@ func is_target_in_range(target: Node2D) -> bool:
func _draw() -> void:
if not is_instance_valid(player) or bool(player.get("_is_destroyed")) or bool(player.get("is_docked")):
return
- var bounds := Rect2(Vector2(344, 70), Vector2(maxf(100, size.x - 680), maxf(100, size.y - 230)))
- var origin := get_viewport().get_canvas_transform() * player.global_position
- var anchor := Vector2(clampf(origin.x, bounds.position.x, bounds.end.x), clampf(origin.y, bounds.position.y, bounds.end.y))
+ var canvas := get_viewport().get_canvas_transform()
+ var to_local := get_global_transform().affine_inverse()
+ # Match the rendered ship and targets, rather than their discrete physics positions.
+ var fraction := Engine.get_physics_interpolation_fraction()
+ var origin: Vector2 = to_local * (canvas * _previous_player_position.lerp(_player_position, fraction))
+ var radius := SCAN_RING_RADIUS * canvas.x.length()
+ var pulse := (sin(_elapsed * TAU * 0.9) + 1.0) * 0.5
for target in targets:
if not is_instance_valid(target) or target.is_queued_for_deletion():
continue
- var color := Color("ff7777")
- var caption := "WRÓG"
+ var color := Color("ff5555")
if target.is_in_group("enemies"):
if float(target.get("hull")) <= 0:
continue
+ elif target.is_in_group("scanner_planets"):
+ color = Color("b68aff")
else:
+ if bool(target.get("_claimed")):
+ continue
match str(target.get("kind")):
"asteroid":
- color = Color("a6c8e0")
- caption = "AST"
+ color = Color("a8adb5")
"wreck":
- color = Color("ffd166")
- caption = "WRAK"
+ color = Color("b98050")
"ruin":
- color = Color("d2a4ff")
- caption = "?"
- var screen := get_viewport().get_canvas_transform() * target.global_position
- if get_viewport_rect().has_point(screen):
+ color = Color("6be58b")
+ _:
+ continue
+ var positions: Array = _positions[target.get_instance_id()]
+ var target_position: Vector2 = positions[0].lerp(positions[1], fraction)
+ var offset: Vector2 = to_local * (canvas * target_position) - origin
+ if offset.length_squared() < 0.01:
continue
- var direction := (screen - anchor).normalized()
- var point := screen
- if not bounds.has_point(screen):
- var dx := (bounds.end.x - anchor.x) if direction.x > 0 else (anchor.x - bounds.position.x)
- var dy := (bounds.end.y - anchor.y) if direction.y > 0 else (anchor.y - bounds.position.y)
- var distance := minf(dx / maxf(absf(direction.x), 0.001), dy / maxf(absf(direction.y), 0.001))
- point = anchor + direction * distance
- var side := Vector2(-direction.y, direction.x)
- draw_colored_polygon(PackedVector2Array([point + direction * 10, point - direction * 6 + side * 6, point - direction * 6 - side * 6]), color)
- draw_string(FONT, point + Vector2(-18, 25), caption, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, color)
+ var point := origin + offset.normalized() * radius
+ draw_circle(point, DOT_RADIUS + 3.0 + pulse * 2.0, Color(color, 0.08 + pulse * 0.12), true, -1.0, true)
+ draw_circle(point, DOT_RADIUS, Color(color, 0.55 + pulse * 0.45), true, -1.0, true)
diff --git a/scripts/ui/ship_crafting_panel.gd b/scripts/ui/ship_crafting_panel.gd
index a9a06c3..61b625d 100644
--- a/scripts/ui/ship_crafting_panel.gd
+++ b/scripts/ui/ship_crafting_panel.gd
@@ -1,5 +1,6 @@
extends PanelContainer
+@export var standalone := false
var _inventory: ShipInventory
var _expanded := false
var _toggle: Button
@@ -39,8 +40,9 @@ func _ready() -> void:
_missile.pressed.connect(func() -> void: _inventory.craft_missile())
_gamma = _button("CraftGamma", "+10 NAPROMIENIOWANEJ · 1 URAN + 10 CZĘŚCI")
_gamma.pressed.connect(func() -> void: _inventory.craft_irradiated_ammunition())
- _details.hide()
- if bool(GameState.hud_panel_state.get("crafting_expanded", false)):
+ _details.visible = standalone
+ _toggle.visible = not standalone
+ if not standalone and bool(GameState.hud_panel_state.get("crafting_expanded", false)):
_toggle_expanded()
_inventory.ammunition_changed.connect(_refresh)
GameState.captain.changed.connect(_refresh)
diff --git a/scripts/ui/ship_profile.gd b/scripts/ui/ship_profile.gd
new file mode 100644
index 0000000..7c4e9d1
--- /dev/null
+++ b/scripts/ui/ship_profile.gd
@@ -0,0 +1,56 @@
+extends Button
+
+const FONT := preload("res://assets/fonts/font2.ttf")
+var player: CharacterBody2D
+var _texture: Texture2D
+var _ship_name := ""
+
+func _ready() -> void:
+ focus_mode = Control.FOCUS_NONE
+ text = ""
+ for state in ["normal", "hover", "pressed", "focus"]:
+ add_theme_stylebox_override(state, StyleBoxEmpty.new())
+ var ship := ShipCatalog.for_path(GameState.selected_ship_path)
+ _ship_name = ship["name"]
+ _texture = load(ShipCatalog.texture_path(ship)) as Texture2D
+
+func _process(_delta: float) -> void:
+ queue_redraw()
+
+func _draw() -> void:
+ if not is_instance_valid(player):
+ return
+ var portrait_center := Vector2(size.x - 54, 55)
+ var color := Color("91dbea") if is_hovered() else Color("376779")
+ var style := StyleBoxFlat.new()
+ style.bg_color = Color("091522f5")
+ style.border_color = color
+ style.set_border_width_all(1)
+ style.set_corner_radius_all(12)
+ draw_style_box(style, Rect2(0, 9, size.x - 48, 94))
+ draw_circle(portrait_center, 50, Color("0a1725"))
+ draw_arc(portrait_center, 50, 0, TAU, 64, color, 3, true)
+ draw_arc(portrait_center, 44, 0, TAU, 64, Color("efb568"), 1, true)
+ if _texture != null:
+ var dimensions := _texture.get_size()
+ var display_size := dimensions * (70.0 / maxf(dimensions.x, dimensions.y))
+ draw_texture_rect(_texture, Rect2(portrait_center - display_size * 0.5, display_size), false)
+ draw_string(FONT, Vector2(14, 29), _ship_name.to_upper(), HORIZONTAL_ALIGNMENT_LEFT, size.x - 120, 13, Color("c9e8ff"))
+ _bar(Rect2(12, 38, size.x - 118, 24), float(player.get("hull")), float(player.get("max_hull")), Color("df7158"), "KADŁUB")
+ _bar(Rect2(12, 68, size.x - 118, 24), float(player.get("shields")), float(player.get("max_shields")), Color("478ddd"), "TARCZE")
+
+func _bar(rect: Rect2, value: float, maximum: float, color: Color, caption: String) -> void:
+ var background := StyleBoxFlat.new()
+ background.bg_color = Color("060e18")
+ background.border_color = color.darkened(0.4)
+ background.set_border_width_all(1)
+ background.set_corner_radius_all(7)
+ draw_style_box(background, rect)
+ var fill_rect := rect.grow(-3)
+ fill_rect.size.x *= clampf(value / maxf(1.0, maximum), 0.0, 1.0)
+ if fill_rect.size.x > 0:
+ var fill := StyleBoxFlat.new()
+ fill.bg_color = color
+ fill.set_corner_radius_all(4)
+ draw_style_box(fill, fill_rect)
+ draw_string(FONT, rect.position + Vector2(0, 16), "%s %.0f / %.0f" % [caption, value, maximum], HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, 10, Color.WHITE)
diff --git a/scripts/ui/ship_profile.gd.uid b/scripts/ui/ship_profile.gd.uid
new file mode 100644
index 0000000..a4a619e
--- /dev/null
+++ b/scripts/ui/ship_profile.gd.uid
@@ -0,0 +1 @@
+uid://dsxp2c0jn6qlh
diff --git a/tests/ships/flight_profile_test.gd b/tests/ships/flight_profile_test.gd
new file mode 100644
index 0000000..d5bc205
--- /dev/null
+++ b/tests/ships/flight_profile_test.gd
@@ -0,0 +1,40 @@
+extends Node
+
+func _ready() -> void:
+ GameState.start_new_run()
+ GameState.captain.skills = {"piloting": 4, "gunner": 3}
+ GameState.captain.learning_points = 2
+ var game := preload("res://scenes/game/game.tscn").instantiate()
+ add_child(game)
+ await get_tree().process_frame
+ await get_tree().process_frame
+ assert(GameState.captain.learning_points == 9)
+ assert(GameState.captain.skills.is_empty())
+ GameState.captain.normalize_skill_ranks()
+ assert(GameState.captain.learning_points == 9)
+ assert(GameState.captain.get_skill_bonus("piloting") == 0.0)
+ assert(not GameState.captain.train_skill("gunner"))
+ var root := game.get_node("HUD/HUDRoot")
+ assert(not root.has_node("EnemyDrawer"))
+ assert(root.get_node("CaptainPanel/Contents/Skills").get_child_count() == 0)
+ var drawer := root.get_node("ShipDrawer") as Control
+ assert(not drawer.visible)
+ root.get_node("ShipToggle").emit_signal("pressed")
+ assert(drawer.visible)
+ assert(drawer.has_node("Scroll/Contents/ShipEquipmentPanel/Contents/EquipmentLayout/Weapon1"))
+ assert(not drawer.has_node("Scroll/Contents/ShipEquipmentPanel/Contents/CraftingPanel"))
+ var navigation := root.get_node("TopNavigation")
+ navigation.get_child(0).emit_signal("pressed")
+ assert(root.get_node("CraftingWindow").visible)
+ assert(root.get_node("CraftingWindow/Contents/CraftingPanel/Contents/CraftingDetails").visible)
+ navigation.get_child(1).emit_signal("pressed")
+ assert(not root.get_node("CraftingWindow").visible)
+ assert(root.get_node("CaptainSkillsWindow").visible)
+ assert(root.get_node("CaptainSkillsWindow/Contents/SkillsContent").get_child_count() == 0)
+ root.get_node("CaptainSkillsWindow/Contents").get_child(0).get_child(1).emit_signal("pressed")
+ assert(not root.get_node("CaptainSkillsWindow").visible)
+ assert(root.has_node("Cockpit/EnemyDetailsPanel/EnemyDetails"))
+ print("FLIGHT_PROFILE_TEST: PASS navigation, ship slots, crafting, empty skills, enemy details and legacy point refund")
+ # Keep the result available briefly for the runtime debugger.
+ await get_tree().create_timer(15.0).timeout
+ get_tree().quit()
diff --git a/tests/ships/flight_profile_test.gd.uid b/tests/ships/flight_profile_test.gd.uid
new file mode 100644
index 0000000..4faa281
--- /dev/null
+++ b/tests/ships/flight_profile_test.gd.uid
@@ -0,0 +1 @@
+uid://da1s61ut1jgjg
diff --git a/tests/ships/flight_profile_test.tscn b/tests/ships/flight_profile_test.tscn
new file mode 100644
index 0000000..619df8f
--- /dev/null
+++ b/tests/ships/flight_profile_test.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3]
+
+[ext_resource type="Script" path="res://tests/ships/flight_profile_test.gd" id="1"]
+
+[node name="FlightProfileTest" type="Node"]
+script = ExtResource("1")