ZMianu HUD

This commit is contained in:
Tomasz Boruc 2026-10-15 18:32:38 +02:00
parent 5dd71181bc
commit d2bf034d98
22 changed files with 488 additions and 204 deletions

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs><linearGradient id="badge" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#c9b7ff"/><stop offset="1" stop-color="#8970df"/></linearGradient></defs>
<path d="M32 7l20 8v17c0 12-9 20-20 25C21 52 12 44 12 32V15z" fill="#211c35" stroke="#8875c0" stroke-width="2" stroke-linejoin="round"/>
<path d="M32 14l4.3 8.7 9.6 1.4-7 6.8 1.7 9.6-8.6-4.5-8.6 4.5 1.7-9.6-7-6.8 9.6-1.4z" fill="url(#badge)" stroke="#e2d7ff" stroke-width="1.3" stroke-linejoin="round"/>
<path d="M24 44l8 5 8-5M27 50l5 3 5-3" fill="none" stroke="#b7a2f3" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

View File

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

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs><linearGradient id="metal" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#ffe2a6"/><stop offset="1" stop-color="#e9a34c"/></linearGradient></defs>
<path d="M32 7v5M32 52v5M7 32h5M52 32h5M14 14l4 4M46 46l4 4M14 50l4-4M46 18l4-4" fill="none" stroke="#b77c3e" stroke-width="3" stroke-linecap="round"/>
<circle cx="32" cy="32" r="20" fill="#251f19" stroke="#735431" stroke-width="2"/>
<path d="M45 17a12 12 0 0 0-16 15L15 46a5 5 0 0 0 7 7l14-14a12 12 0 0 0 15-16l-8 8-7-2-2-7z" fill="url(#metal)" stroke="#ffedc2" stroke-width="1.3" stroke-linejoin="round"/>
<circle cx="19" cy="49" r="2" fill="#49301b"/>
<path d="M23 43l7-7" stroke="#fff1cb" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

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

View File

@ -9,9 +9,7 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd")
@export var parts: float = 0.0 @export var parts: float = 0.0
@export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0} @export var minerals: Dictionary = {"uranium": 0, "titanium": 0, "iron": 0, "aluminium": 0}
@export var learning_points: int = 0 @export var learning_points: int = 0
@export var skills: Dictionary = { @export var skills: Dictionary = {}
"piloting": 0, "gunner": 0, "mechanic": 0, "sharp_eye": 0,
}
func get_xp_required() -> int: func get_xp_required() -> int:
return BALANCE.xp_required(level) return BALANCE.xp_required(level)
@ -19,20 +17,17 @@ func get_xp_required() -> int:
func get_skill_rank(skill: String) -> int: func get_skill_rank(skill: String) -> int:
return clampi(int(skills.get(skill, 0)), 0, BALANCE.MAX_SKILL_RANK) return clampi(int(skills.get(skill, 0)), 0, BALANCE.MAX_SKILL_RANK)
func get_skill_bonus(skill: String) -> float: func get_skill_bonus(_skill: String) -> float:
return BALANCE.skill_bonus(skill, get_skill_rank(skill)) return 0.0 # The old captain skill system has been retired.
func normalize_skill_ranks() -> void: func normalize_skill_ranks() -> void:
var changed_ranks := false # Refund legacy ranks for the future captain skill system.
for skill in BALANCE.SKILL_NAMES: var refunded := get_spent_learning_points()
var previous := maxi(0, int(skills.get(skill, 0))) if refunded > 0:
var rank := mini(previous, BALANCE.MAX_SKILL_RANK) learning_points += refunded
learning_points += previous - rank skills.clear()
if int(skills.get(skill, 0)) != rank:
changed_ranks = true
skills[skill] = rank
if changed_ranks:
emit_changed() emit_changed()
return
func get_spent_learning_points() -> int: func get_spent_learning_points() -> int:
var spent := 0 var spent := 0
@ -56,8 +51,8 @@ func reset_skills() -> bool:
emit_changed() emit_changed()
return true return true
func can_train(skill: String) -> bool: func can_train(_skill: String) -> bool:
return skills.has(skill) and learning_points > 0 and get_skill_rank(skill) < BALANCE.MAX_SKILL_RANK return false
func train_skill(skill: String) -> bool: func train_skill(skill: String) -> bool:
if not can_train(skill): if not can_train(skill):

View File

@ -1,6 +1,6 @@
extends Node2D extends Node2D
const DURATION := 1.6 const SETTLE_TIME := 8.0
var _texture: Texture2D var _texture: Texture2D
var _fragments: Array[Dictionary] = [] var _fragments: Array[Dictionary] = []
var _dust: Array[Dictionary] = [] var _dust: Array[Dictionary] = []
@ -24,24 +24,36 @@ func configure(sprite: Sprite2D) -> void:
if y > 0 and y < 4: if y > 0 and y < 4:
vertex.y += _rng.randf_range(-0.3, 0.3) * dimensions.y / 4.0 vertex.y += _rng.randf_range(-0.3, 0.3) * dimensions.y / 4.0
grid.append(vertex) 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 y in 4:
for x in 4: for x in 4:
var a := grid[y * 5 + x] var outline := PackedVector2Array()
var b := grid[y * 5 + x + 1] outline.append_array(_edge_vertices(horizontal[y * 4 + x], false))
var c := grid[(y + 1) * 5 + x + 1] outline.append_array(_edge_vertices(vertical[y * 5 + x + 1], false))
var d := grid[(y + 1) * 5 + x] outline.append_array(_edge_vertices(horizontal[(y + 1) * 4 + x], true))
for triangle in [[a, b, c], [a, c, d]]: outline.append_array(_edge_vertices(vertical[y * 5 + x], true))
var center: Vector2 = (triangle[0] + triangle[1] + triangle[2]) / 3.0 var center := Vector2.ZERO
var origin := (center - dimensions * 0.5) * sprite_scale for vertex in outline:
var points := PackedVector2Array() center += vertex
var uv := PackedVector2Array() center /= outline.size()
for vertex: Vector2 in triangle: var origin := (center - dimensions * 0.5) * sprite_scale
points.append((vertex - center) * sprite_scale) var points := PackedVector2Array()
uv.append(vertex / dimensions) var uv := PackedVector2Array()
var direction := origin.normalized() if origin.length_squared() > 0.1 else Vector2.RIGHT for vertex in outline:
_fragments.append({"points": points, "uv": uv, "position": origin, points.append((vertex - center) * sprite_scale)
"velocity": direction.rotated(_rng.randf_range(-0.35, 0.35)) * _rng.randf_range(70.0, 170.0), uv.append(vertex / dimensions)
"angle": 0.0, "spin": _rng.randf_range(-4.0, 4.0)}) 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") _dust_color = Color("a1c7df") if "ice" in _texture.resource_path else Color("b4a99a")
for index in 28: for index in 28:
_dust.append({"direction": Vector2.RIGHT.rotated(_rng.randf_range(0.0, TAU)), _dust.append({"direction": Vector2.RIGHT.rotated(_rng.randf_range(0.0, TAU)),
@ -49,22 +61,43 @@ func configure(sprite: Sprite2D) -> void:
z_index = 35 z_index = 35
light_mask = 4 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: func _process(delta: float) -> void:
_elapsed += delta _elapsed += delta
if _elapsed >= DURATION:
queue_free()
return
for fragment in _fragments: for fragment in _fragments:
fragment.position += fragment.velocity * delta fragment.position += fragment.velocity * delta
fragment.velocity *= exp(-1.2 * delta) fragment.velocity *= exp(-1.2 * delta)
fragment.angle += fragment.spin * delta fragment.angle += fragment.spin * delta
fragment.spin *= exp(-0.7 * delta)
queue_redraw() queue_redraw()
# Leave the settled debris visible without updating it every frame.
if _elapsed >= SETTLE_TIME:
_dust.clear()
set_process(false)
func _draw() -> void: func _draw() -> void:
var fade := 1.0 - smoothstep(0.65, DURATION, _elapsed)
for fragment in _fragments: for fragment in _fragments:
draw_set_transform(fragment.position, fragment.angle) 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) draw_set_transform(Vector2.ZERO)
var dust_fade := maxf(0.0, 1.0 - _elapsed / 0.8) var dust_fade := maxf(0.0, 1.0 - _elapsed / 0.8)
for mote in _dust: for mote in _dust:

View File

@ -24,6 +24,89 @@ var _debug_item_button: Button
var _debug_item_status: Label var _debug_item_status: Label
var _captain_panel: PanelContainer var _captain_panel: PanelContainer
var _cockpit: Control 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: func _ready() -> void:
_sidebar_open = bool(GameState.hud_panel_state.get("ship_open", false)) _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. # 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)))) debug_info.add_theme_constant_override("line_spacing", mini(0, 13 - ceili(debug_info.get_theme_font("font").get_height(10))))
_create_sidebar() _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 = Control.new()
_inventory_clip.name = "InventoryDrawer" _inventory_clip.name = "InventoryDrawer"
_inventory_clip.clip_contents = true _inventory_clip.clip_contents = true
@ -121,23 +200,21 @@ func _create_sidebar() -> void:
equipment.reparent(contents, false) equipment.reparent(contents, false)
equipment.size_flags_horizontal = Control.SIZE_EXPAND_FILL equipment.size_flags_horizontal = Control.SIZE_EXPAND_FILL
equipment.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) 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()) contents.minimum_size_changed.connect(func() -> void: _layout_sidebar.call_deferred())
_sidebar_toggle = Button.new() _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.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) _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(_sidebar_toggle)
preload("res://scripts/ui/hud_button_style.gd").apply(inventory_button, Color("b9a0ef")) preload("res://scripts/ui/hud_button_style.gd").apply(inventory_button, Color("b9a0ef"))
$HUDRoot.add_child(_sidebar_toggle) $HUDRoot.add_child(_sidebar_toggle)
_sidebar_toggle.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT) _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_right = -12
_sidebar_toggle.offset_top = 12 _sidebar_toggle.offset_top = 12
_sidebar_toggle.offset_bottom = 48 _sidebar_toggle.offset_bottom = 122
_sidebar_toggle.pressed.connect(_toggle_sidebar) _sidebar_toggle.pressed.connect(_toggle_sidebar)
_cockpit = Control.new() _cockpit = Control.new()
_cockpit.name = "Cockpit" _cockpit.name = "Cockpit"
@ -145,6 +222,7 @@ func _create_sidebar() -> void:
_cockpit.set("player", player) _cockpit.set("player", player)
_cockpit.set("game", get_parent()) _cockpit.set("game", get_parent())
$HUDRoot.add_child(_cockpit) $HUDRoot.add_child(_cockpit)
_create_navigation()
_layout_debug() _layout_debug()
func _layout_debug() -> void: func _layout_debug() -> void:
@ -165,7 +243,7 @@ func _toggle_sidebar() -> void:
GameState.hud_panel_state["ship_open"] = _sidebar_open GameState.hud_panel_state["ship_open"] = _sidebar_open
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_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: if _sidebar_open:
_sidebar.show() _sidebar.show()
else: else:
@ -181,16 +259,16 @@ 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 - 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.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) _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: if _ability_bar != null:
var abilities := _ability_bar.get_parent() as Control var abilities := _ability_bar.get_parent() as Control
abilities.offset_bottom = _cockpit.position.y - $HUDRoot.size.y - 14.0 abilities.offset_bottom = _cockpit.position.y - $HUDRoot.size.y - 14.0
abilities.offset_top = abilities.offset_bottom - 44.0 abilities.offset_top = abilities.offset_bottom - 44.0
_layout_debug() _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 _sidebar.visible = _sidebar_open
_inventory_clip.size = inventory_panel.size * EQUIPMENT_UI_SCALE _inventory_clip.size = inventory_panel.size * EQUIPMENT_UI_SCALE
_layout_inventory() _layout_inventory()

View File

@ -20,6 +20,9 @@ func _ready() -> void:
_background.bg_color = Color("101925") _background.bg_color = Color("101925")
_background.border_color = Color("456277") _background.border_color = Color("456277")
_background.set_border_width_all(1) _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: func _physics_process(_delta: float) -> void:
if is_instance_valid(player): if is_instance_valid(player):
@ -58,13 +61,26 @@ func _hull_ratio() -> float:
func _draw() -> void: func _draw() -> void:
if not is_instance_valid(player): if not is_instance_valid(player):
return return
var color := Color("71e3ff") if _energy_ratio() >= 0.25 else Color("ffbd66") _draw_bar(Vector2(-46, 58), _display_ratio, Color("efbd62"), 0)
var bar_position := Vector2(-44, 48) _draw_bar(Vector2(-46, 72), _display_shield_ratio, Color("66cfff"), 1)
draw_style_box(_background, Rect2(bar_position - Vector2(2, 2), Vector2(92, 10))) _draw_bar(Vector2(-46, 86), _display_hull_ratio, Color("ef6875"), 2)
draw_rect(Rect2(bar_position, Vector2(88 * _display_ratio, 6)), color)
var shield_bar_position := bar_position + Vector2(0, 14) func _draw_bar(position: Vector2, ratio: float, color: Color, kind: int) -> void:
draw_style_box(_background, Rect2(shield_bar_position - Vector2(2, 2), Vector2(92, 10))) draw_style_box(_background, Rect2(position - Vector2(14, 3), Vector2(110, 12)))
draw_rect(Rect2(shield_bar_position, Vector2(88 * _display_shield_ratio, 6)), Color.WHITE) # Ten illuminated cells, with partial cells tracking the exact current value.
var hull_bar_position := bar_position + Vector2(0, 28) for index in 10:
draw_style_box(_background, Rect2(hull_bar_position - Vector2(2, 2), Vector2(92, 10))) var cell := Rect2(position + Vector2(index * 9.2, 0), Vector2(7.6, 6))
draw_rect(Rect2(hull_bar_position, Vector2(88 * _display_hull_ratio, 6)), Color("ef5967")) 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)

View File

@ -11,6 +11,8 @@ var _label: Label
var _sprite: Sprite2D var _sprite: Sprite2D
func _ready() -> void: func _ready() -> void:
if not is_star:
add_to_group("scanner_planets")
var sprite := Sprite2D.new() var sprite := Sprite2D.new()
_sprite = sprite _sprite = sprite
sprite.name = "BodySprite" sprite.name = "BodySprite"

View File

@ -119,8 +119,8 @@ func claim() -> bool:
elif kind == "ruin": elif kind == "ruin":
var xp := roundi(_rng.randi_range(10, 20) * scale) var xp := roundi(_rng.randi_range(10, 20) * scale)
GameState.captain.add_rewards(xp, 0) GameState.captain.add_rewards(xp, 0)
var module := _drop_module(true) _drop_module(true)
summary = "%d XP oraz %s (poziom %d)" % [xp, module.definition.display_name, module.item_level] summary = "+%d doświadczenia · +1 moduł" % xp
_show_discovery(summary) _show_discovery(summary)
else: else:
match _rng.randi_range(0, 3): match _rng.randi_range(0, 3):
@ -163,28 +163,29 @@ func _show_discovery(reward: String) -> void:
layer.layer = 30 layer.layer = 30
layer.name = "AncientDiscoveryMessage" layer.name = "AncientDiscoveryMessage"
get_parent().add_child(layer) 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() var label := Label.new()
label.custom_minimum_size = Vector2(600, 95) layer.add_child(label)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART 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.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.mouse_filter = Control.MOUSE_FILTER_IGNORE label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_font_size_override("font_size", 18) label.add_theme_font_size_override("font_size", 15)
label.add_theme_color_override("font_color", Color("ffd166")) label.add_theme_color_override("font_color", Color("d8d4b7"))
label.text = "Znalazłeś ślady starożytnej upadłej cywilizacji!\nZdobyłeś: " + reward 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: if _module_left_outside:
label.text += "\nMagazyn pełny — moduł czeka obok planety." label.text += " (obok planety)"
panel.add_child(label) label.modulate.a = 0.0
var tween := layer.create_tween() var tween := layer.create_tween()
tween.tween_interval(7.0) tween.tween_property(label, "modulate:a", 1.0, 0.35)
tween.tween_property(panel, "modulate:a", 0.0, 0.5) tween.tween_interval(3.5)
tween.tween_property(label, "modulate:a", 0.0, 0.8)
tween.tween_callback(layer.queue_free) tween.tween_callback(layer.queue_free)
func _drop_module(ancient: bool) -> ItemInstance: func _drop_module(ancient: bool) -> ItemInstance:

View File

@ -12,55 +12,17 @@ const BALANCE := preload("res://scripts/captain/captain_balance.gd")
var _collapsed := false var _collapsed := false
var _collapse_button: Button var _collapse_button: Button
var _captain: CaptainProgress var _captain: CaptainProgress
var _rank_labels: Dictionary = {}
var _effect_labels: Dictionary = {}
var _buttons: Dictionary = {}
func _ready() -> void: func _ready() -> void:
_build_portrait() _build_portrait()
_captain = GameState.captain _captain = GameState.captain
_captain.normalize_skill_ranks() _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) _captain.changed.connect(_refresh)
player.get_node("ShipInventory").connect("inventory_changed", _refresh) player.get_node("ShipInventory").connect("inventory_changed", _refresh)
_refresh() _refresh()
if bool(GameState.hud_panel_state.get("captain_collapsed", true)): if bool(GameState.hud_panel_state.get("captain_collapsed", true)):
_toggle_collapsed() _toggle_collapsed()
func _train(skill: String) -> void:
_captain.train_skill(skill)
func _refresh() -> void: func _refresh() -> void:
level_label.text = "POZ. %d" % _captain.level level_label.text = "POZ. %d" % _captain.level
xp_label.text = "XP %d / %d" % [_captain.experience, _captain.get_xp_required()] xp_label.text = "XP %d / %d" % [_captain.experience, _captain.get_xp_required()]
@ -68,15 +30,6 @@ func _refresh() -> void:
xp_bar.value = _captain.experience xp_bar.value = _captain.experience
credits_label.text = "Kredyty: %d" % _captain.credits credits_label.text = "Kredyty: %d" % _captain.credits
points_label.text = "Punkty: %d" % _captain.learning_points 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: func _build_portrait() -> void:
custom_minimum_size = Vector2(320, 0) custom_minimum_size = Vector2(320, 0)

View File

@ -1,49 +1,17 @@
extends Control extends Control
var _panel: PanelContainer
var _label: Label var _label: Label
var _toggle: Button
var _open := false
var _tween: Tween
func _ready() -> void: func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE 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 = Label.new()
_label.add_theme_font_size_override("font_size", 12) _label.name = "EnemyDetails"
_panel.add_child(_label) _label.mouse_filter = Control.MOUSE_FILTER_IGNORE
_toggle = Button.new() _label.add_theme_font_size_override("font_size", 9)
_toggle.text = "WRÓG ▾" _label.add_theme_color_override("font_color", Color("c9acb5"))
preload("res://scripts/ui/hud_button_style.gd").apply(_toggle, Color("ff9aae")) _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
add_child(_toggle) _label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_toggle.pressed.connect(_toggle_drawer) add_child(_label)
_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)
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
var enemy: Node = null 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 profile := DamageTypes.enemy_profile(GameState.system_level, GameState.system_seed)
var kind := int(profile["kind"]) var kind := int(profile["kind"])
var names := ["Działko plazmowe", "Działko maszynowe", "Laser", "Rakiety", "Działko gamma"] 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: 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: else:
_label.text += "\nUkład oczyszczony" _label.text += "\nUkład oczyszczony"

View File

@ -6,11 +6,16 @@ const WEAPON_DISPLAY_ORDER := [0, 2, 1]
const AMBER := Color("efb568") const AMBER := Color("efb568")
var player: CharacterBody2D var player: CharacterBody2D
var game: Node2D var game: Node2D
var _enemy_details: Control
var _inventory: ShipInventory var _inventory: ShipInventory
func _ready() -> void: func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE mouse_filter = Control.MOUSE_FILTER_IGNORE
_inventory = player.get_node("ShipInventory") _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: func _process(delta: float) -> void:
if is_instance_valid(player): if is_instance_valid(player):
@ -18,6 +23,9 @@ func _process(delta: float) -> void:
var local := get_global_transform().affine_inverse() * screen var local := get_global_transform().affine_inverse() * screen
var overlaps := Rect2(Vector2.ZERO, size).grow(36).has_point(local) 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) 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() queue_redraw()
func _text(value: String, point: Vector2, width: float, font_size: int, color: Color) -> void: 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) draw_line(Vector2(28, 2), Vector2(size.x - 28, 2), CYAN, 2, true)
# Put weapon cards beside the counters rather than stacking three rows. # 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("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 cell := (size.x - 238) / 3.0
var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))] var values := [_inventory.missile_ammunition, _inventory.ammunition, int(game.call("get_remaining_enemies"))]
var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"] var captions := ["RAKIETY", "AMUNICJA", "WROGOWIE"]

View File

@ -26,7 +26,7 @@ func _ready() -> void:
_gamma_buy.pressed.connect(func() -> void: _inventory.buy_irradiated_ammunition()) _gamma_buy.pressed.connect(func() -> void: _inventory.buy_irradiated_ammunition())
next_button.pressed.connect(func() -> void: next_level_requested.emit()) next_button.pressed.connect(func() -> void: next_level_requested.emit())
buy_ammunition_button.pressed.connect(_buy_ammunition) buy_ammunition_button.pressed.connect(_buy_ammunition)
reset_skills_button.pressed.connect(_reset_skills) reset_skills_button.hide()
_missile_button = Button.new() _missile_button = Button.new()
_missile_button.custom_minimum_size.y = 36 _missile_button.custom_minimum_size.y = 36
_missile_button.add_theme_font_size_override("font_size", 12) _missile_button.add_theme_font_size_override("font_size", 12)

View File

@ -10,9 +10,6 @@ func _process(_delta: float) -> void:
func _draw() -> void: func _draw() -> void:
if not is_instance_valid(game) or not is_instance_valid(game.get("protected_planet")): if not is_instance_valid(game) or not is_instance_valid(game.get("protected_planet")):
return 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") var portal: Node2D = game.get("wormhole")
if is_instance_valid(portal): if is_instance_valid(portal):
_draw_target(portal.global_position, "WORMHOLE — następny układ", Color("89e8ff")) _draw_target(portal.global_position, "WORMHOLE — następny układ", Color("89e8ff"))

View File

@ -1,21 +1,45 @@
extends Control 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 player: CharacterBody2D
var targets: Array[Node2D] = [] 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: func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE 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): if not is_instance_valid(player):
return 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() 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): 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) 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: func get_scan_range() -> float:
var transform := get_viewport().get_canvas_transform() var transform := get_viewport().get_canvas_transform()
@ -29,38 +53,39 @@ func is_target_in_range(target: Node2D) -> bool:
func _draw() -> void: func _draw() -> void:
if not is_instance_valid(player) or bool(player.get("_is_destroyed")) or bool(player.get("is_docked")): if not is_instance_valid(player) or bool(player.get("_is_destroyed")) or bool(player.get("is_docked")):
return return
var bounds := Rect2(Vector2(344, 70), Vector2(maxf(100, size.x - 680), maxf(100, size.y - 230))) var canvas := get_viewport().get_canvas_transform()
var origin := get_viewport().get_canvas_transform() * player.global_position var to_local := get_global_transform().affine_inverse()
var anchor := Vector2(clampf(origin.x, bounds.position.x, bounds.end.x), clampf(origin.y, bounds.position.y, bounds.end.y)) # 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: for target in targets:
if not is_instance_valid(target) or target.is_queued_for_deletion(): if not is_instance_valid(target) or target.is_queued_for_deletion():
continue continue
var color := Color("ff7777") var color := Color("ff5555")
var caption := "WRÓG"
if target.is_in_group("enemies"): if target.is_in_group("enemies"):
if float(target.get("hull")) <= 0: if float(target.get("hull")) <= 0:
continue continue
elif target.is_in_group("scanner_planets"):
color = Color("b68aff")
else: else:
if bool(target.get("_claimed")):
continue
match str(target.get("kind")): match str(target.get("kind")):
"asteroid": "asteroid":
color = Color("a6c8e0") color = Color("a8adb5")
caption = "AST"
"wreck": "wreck":
color = Color("ffd166") color = Color("b98050")
caption = "WRAK"
"ruin": "ruin":
color = Color("d2a4ff") color = Color("6be58b")
caption = "?" _:
var screen := get_viewport().get_canvas_transform() * target.global_position continue
if get_viewport_rect().has_point(screen): 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 continue
var direction := (screen - anchor).normalized() var point := origin + offset.normalized() * radius
var point := screen draw_circle(point, DOT_RADIUS + 3.0 + pulse * 2.0, Color(color, 0.08 + pulse * 0.12), true, -1.0, true)
if not bounds.has_point(screen): draw_circle(point, DOT_RADIUS, Color(color, 0.55 + pulse * 0.45), true, -1.0, true)
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)

View File

@ -1,5 +1,6 @@
extends PanelContainer extends PanelContainer
@export var standalone := false
var _inventory: ShipInventory var _inventory: ShipInventory
var _expanded := false var _expanded := false
var _toggle: Button var _toggle: Button
@ -39,8 +40,9 @@ func _ready() -> void:
_missile.pressed.connect(func() -> void: _inventory.craft_missile()) _missile.pressed.connect(func() -> void: _inventory.craft_missile())
_gamma = _button("CraftGamma", "+10 NAPROMIENIOWANEJ · 1 URAN + 10 CZĘŚCI") _gamma = _button("CraftGamma", "+10 NAPROMIENIOWANEJ · 1 URAN + 10 CZĘŚCI")
_gamma.pressed.connect(func() -> void: _inventory.craft_irradiated_ammunition()) _gamma.pressed.connect(func() -> void: _inventory.craft_irradiated_ammunition())
_details.hide() _details.visible = standalone
if bool(GameState.hud_panel_state.get("crafting_expanded", false)): _toggle.visible = not standalone
if not standalone and bool(GameState.hud_panel_state.get("crafting_expanded", false)):
_toggle_expanded() _toggle_expanded()
_inventory.ammunition_changed.connect(_refresh) _inventory.ammunition_changed.connect(_refresh)
GameState.captain.changed.connect(_refresh) GameState.captain.changed.connect(_refresh)

View File

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

View File

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

View File

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

View File

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

View File

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