Randoloot/scripts/ui/captain_skills_ui.gd
2026-10-16 22:36:15 +02:00

333 lines
14 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends CanvasLayer
const CATALOG := preload("res://scripts/captain/captain_skills.gd")
const BUTTON_STYLE := preload("res://scripts/ui/hud_button_style.gd")
var player: CharacterBody2D
var _captain: CaptainProgress
var _screen: Control
var _panel: PanelContainer
var _contents: VBoxContainer
var _bar: HBoxContainer
var _buttons: Dictionary = {}
var _was_paused := false
var _owns_pause := false
var _refresh_pending := false
func _ready() -> void:
name = "CaptainSkills"
layer = preload("res://scripts/ui/render_layers.gd").MANAGEMENT
process_mode = Node.PROCESS_MODE_ALWAYS
_captain = GameState.captain
_captain.normalize_equipped_skills()
_screen = Control.new()
_screen.name = "SkillManagement"
add_child(_screen)
_screen.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_screen.hide()
get_parent().get_node("HUDRoot/FloatingWindows").set("modal_overlay", _screen)
var dim := ColorRect.new()
dim.color = Color(0.015, 0.025, 0.045, 0.9)
_screen.add_child(dim)
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_panel = PanelContainer.new()
_panel.name = "Panel"
_panel.add_theme_stylebox_override("panel", _style(Color("567b99"), Color("0a1422"), 22))
_screen.add_child(_panel)
_panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
_contents = VBoxContainer.new()
_contents.add_theme_constant_override("separation", 14)
_panel.add_child(_contents)
_bar = HBoxContainer.new()
_bar.name = "CaptainActiveSkills"
_bar.add_theme_constant_override("separation", 6)
get_parent().get_node("HUDRoot").add_child(_bar)
_captain.changed.connect(_queue_refresh)
get_viewport().size_changed.connect(_layout)
_queue_refresh()
func _queue_refresh() -> void:
if not _refresh_pending:
_refresh_pending = true
_refresh.call_deferred()
func _label(text: String, font_size: int, color: Color = Color("dae8ef")) -> Label:
var label := Label.new()
label.text = text
label.add_theme_font_override("font", ThemeDB.fallback_font)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", color)
return label
func _style(border: Color, background: Color, margin: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(1)
style.set_corner_radius_all(10)
style.set_content_margin_all(margin)
return style
func _refresh() -> void:
_refresh_pending = false
_captain.prepare_skill_offers()
_rebuild_bar()
if _screen.visible:
_build_management()
func open_management() -> void:
if bool(player.get("_is_destroyed")):
return
if not _screen.visible:
_was_paused = get_tree().paused
_owns_pause = true
get_tree().paused = true
_screen.show()
_build_management()
func _close() -> void:
if not _captain.skill_offers.is_empty():
return
_screen.hide()
if _owns_pause:
get_tree().paused = _was_paused
_owns_pause = false
func _build_management() -> void:
for child in _contents.get_children():
_contents.remove_child(child)
child.queue_free()
var choosing := not _captain.skill_offers.is_empty()
var heading := HBoxContainer.new()
heading.name = "Header"
_contents.add_child(heading)
var title := _label("AWANS KAPITANA" if choosing else "UMIEJĘTNOŚCI KAPITANA", 25, Color("91dbea"))
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
heading.add_child(title)
if not choosing:
var close := Button.new()
close.name = "Close"
close.text = "WRÓĆ DO LOTU"
close.pressed.connect(_close)
heading.add_child(close)
_contents.add_child(_label("POZIOM %d · GRA WSTRZYMANA" % _captain.level, 13, Color("91aabd")))
if choosing:
_contents.add_child(_label("Wybierz jedną umiejętność. Wybór obowiązuje przez całą rozgrywkę.", 16))
var choices := HBoxContainer.new()
choices.name = "Choices"
choices.add_theme_constant_override("separation", 14)
_contents.add_child(choices)
for skill in _captain.skill_offers:
choices.add_child(_choice_card(skill))
if _captain.learning_points > 1:
_contents.add_child(_label("Pozostałe wybory po tym awansie: %d" % (_captain.learning_points - 1), 13, Color("efb568")))
_contents.add_child(HSeparator.new())
_contents.add_child(_label("POSIADANE UMIEJĘTNOŚCI", 14, Color("91dbea")))
var owned := GridContainer.new()
owned.columns = 3
owned.add_theme_constant_override("h_separation", 12)
owned.add_theme_constant_override("v_separation", 10)
_contents.add_child(owned)
for skill in CATALOG.IDS:
var rank := _captain.get_skill_rank(skill)
if rank <= 0:
continue
var row := HBoxContainer.new()
row.custom_minimum_size.x = 260
row.tooltip_text = CATALOG.description(skill, rank)
owned.add_child(row)
row.add_child(_icon(skill, 38))
var info := VBoxContainer.new()
row.add_child(info)
info.add_child(_label(CATALOG.DATA[skill]["name"], 13))
info.add_child(_label("%s · %s" % ["●".repeat(rank) + "○".repeat(3 - rank), "AKTYWNA" if CATALOG.DATA[skill]["active"] else "PASYWNA"], 11, Color(CATALOG.DATA[skill]["color"])))
if owned.get_child_count() == 0:
_contents.add_child(_label("Nie masz jeszcze umiejętności. To początek Twojej specjalizacji.", 14, Color("91aabd")))
_build_loadout()
var manager := get_parent().get_node("HUDRoot/FloatingWindows")
manager.call("register_window", _panel, _panel, heading)
manager.call("focus_window", _panel)
_layout.call_deferred()
func _build_loadout() -> void:
_contents.add_child(HSeparator.new())
_contents.add_child(_label("AKTYWNE SLOTY F1–F5 · MAKSYMALNIE 5 UMIEJĘTNOŚCI", 13, Color("91dbea")))
var row := HBoxContainer.new()
row.name = "ActiveSkillLoadout"
row.add_theme_constant_override("separation", 10)
_contents.add_child(row)
for slot in CaptainProgress.ACTIVE_SKILL_SLOTS:
var column := VBoxContainer.new()
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(column)
column.add_child(_label("F%d" % (slot + 1), 12, Color("efb568")))
var picker := OptionButton.new()
picker.name = "SlotF%d" % (slot + 1)
picker.fit_to_longest_item = false
picker.custom_minimum_size = Vector2(155, 36)
picker.add_theme_font_override("font", ThemeDB.fallback_font)
picker.add_theme_font_size_override("font_size", 12)
picker.add_theme_constant_override("icon_max_width", 26)
picker.get_popup().add_theme_constant_override("icon_max_width", 28)
picker.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
picker.add_item("Pusty slot")
picker.set_item_metadata(0, "")
for skill in CATALOG.IDS:
if CATALOG.DATA[skill]["active"] and _captain.get_skill_rank(skill) > 0:
picker.add_icon_item(CATALOG.icon(skill), CATALOG.DATA[skill]["name"])
var index := picker.item_count - 1
picker.set_item_metadata(index, skill)
if _captain.equipped_skills[slot] == skill:
picker.select(index)
picker.tooltip_text = "Wybierz aktywną umiejętność dla F%d. Pasywne działają bez zajmowania slotu." % (slot + 1)
picker.item_selected.connect(func(index: int) -> void: _captain.equip_skill(slot, str(picker.get_item_metadata(index))))
column.add_child(picker)
func _icon(skill: String, dimensions: float) -> TextureRect:
var icon := TextureRect.new()
icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
icon.texture = CATALOG.icon(skill)
icon.custom_minimum_size = Vector2.ONE * dimensions
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
return icon
func _choice_card(skill: String) -> PanelContainer:
var rank := _captain.get_skill_rank(skill)
var next_rank := rank + 1
var data: Dictionary = CATALOG.DATA[skill]
var color := Color(data["color"])
var card := PanelContainer.new()
card.name = skill
card.custom_minimum_size = Vector2(266, 340)
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card.add_theme_stylebox_override("panel", _style(color.darkened(0.4), Color("101e2e"), 16))
var body := VBoxContainer.new()
body.add_theme_constant_override("separation", 10)
card.add_child(body)
body.add_child(_label("NOWA UMIEJĘTNOŚĆ" if rank == 0 else "ULEPSZENIE %s → %s" % [str(rank), str(next_rank)], 11, color))
body.add_child(_icon(skill, 76))
var title := _label(data["name"], 19)
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
body.add_child(title)
body.add_child(_label("POZIOM %s / III · %s" % [["I", "II", "III"][next_rank - 1], "AKTYWNA" if data["active"] else "PASYWNA"], 11, color))
var description := _label(CATALOG.description(skill, next_rank), 14)
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
description.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_child(description)
if data["active"]:
body.add_child(_label("DZIAŁANIE 5 s · ODNOWIENIE 30 s", 11, Color("91aabd")))
else:
body.add_child(_label("STAŁY EFEKT", 11, Color("91aabd")))
var select := Button.new()
select.name = "Choose"
select.text = "WYBIERZ" if rank == 0 else "ULEPSZ DO %s" % ["I", "II", "III"][next_rank - 1]
select.custom_minimum_size.y = 38
select.focus_mode = Control.FOCUS_NONE
BUTTON_STYLE.apply(select, color)
select.pressed.connect(_choose.bind(skill))
body.add_child(select)
return card
func _choose(skill: String) -> void:
if not _captain.train_skill(skill):
return
# Clear any held mouse fire state before resuming the simulation.
player.get_node("WeaponController").set("_fire_buttons_down", [false, false, false])
player.get_node("WeaponController").set("_fire_just_pressed", [false, false, false])
if _captain.skill_offers.is_empty():
_close()
func _rebuild_bar() -> void:
for child in _bar.get_children():
_bar.remove_child(child)
child.queue_free()
_buttons.clear()
_captain.normalize_equipped_skills()
for index in CaptainProgress.ACTIVE_SKILL_SLOTS:
var skill := _captain.equipped_skills[index]
var column := VBoxContainer.new()
_bar.add_child(column)
var button := Button.new()
button.set_script(preload("res://scripts/ui/captain_skill_button.gd"))
button.name = "SlotF%d" % (index + 1)
button.call("configure", CATALOG.icon(skill) if not skill.is_empty() else null, Color(CATALOG.DATA[skill]["color"]) if not skill.is_empty() else Color("344657"))
button.disabled = skill.is_empty()
button.pressed.connect(_activate.bind(skill))
column.add_child(button)
var binding := _label("F%d" % (index + 1), 9)
binding.position = Vector2(4, 2)
binding.mouse_filter = Control.MOUSE_FILTER_IGNORE
binding.add_theme_color_override("font_outline_color", Color("030810"))
binding.add_theme_constant_override("outline_size", 4)
button.add_child(binding)
var status := _label("", 9)
status.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
status.mouse_filter = Control.MOUSE_FILTER_IGNORE
status.add_theme_color_override("font_outline_color", Color("030810"))
status.add_theme_constant_override("outline_size", 4)
button.add_child(status)
status.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT)
status.offset_left = -33
status.offset_top = -14
status.offset_right = -3
status.offset_bottom = -1
_buttons[index] = {"button": button, "status": status, "skill": skill, "key": KEY_F1 + index, "binding": "F%d" % (index + 1)}
_layout.call_deferred()
func _activate(skill: String) -> void:
if not get_tree().paused:
_captain.activate_skill(skill, player)
func _unhandled_key_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_ESCAPE and _screen.visible:
_close()
get_viewport().set_input_as_handled()
elif not get_tree().paused:
for index in _buttons:
if event.physical_keycode == _buttons[index]["key"] or event.keycode == _buttons[index]["key"]:
_activate(_buttons[index]["skill"])
get_viewport().set_input_as_handled()
func _process(_delta: float) -> void:
for index in _buttons:
var entry: Dictionary = _buttons[index]
var skill: String = entry["skill"]
var button: Button = entry["button"]
var status: Label = entry["status"]
if skill.is_empty():
button.tooltip_text = "[%s] Pusty slot — przypisz skill w panelu kapitana" % entry["binding"]
continue
var duration := float(_captain.skill_durations.get(skill, 0.0))
var cooldown := float(_captain.skill_cooldowns.get(skill, 0.0))
var reason := _captain.skill_unavailable_reason(skill, player)
button.disabled = get_tree().paused or not reason.is_empty()
button.call("set_visual_state", cooldown / CATALOG.COOLDOWN, duration > 0.0, button.disabled)
button.tooltip_text = "%s · poziom %d\n%s\n[%s] · Odnowienie 30 s%s" % [CATALOG.DATA[skill]["name"], _captain.get_skill_rank(skill), CATALOG.description(skill, _captain.get_skill_rank(skill)), entry["binding"], "\n" + reason if not reason.is_empty() else ""]
status.text = "%.1f" % duration if duration > 0.0 else (str(ceili(cooldown)) if cooldown > 0.0 else "")
status.modulate = Color("a9ef83") if duration > 0.0 else Color("dae8ef")
_bar.visible = not bool(player.get("_is_destroyed")) and not _screen.visible
func _layout() -> void:
if not is_inside_tree():
return
var viewport_size := get_viewport().get_visible_rect().size
var base_size := _panel.get_combined_minimum_size().max(Vector2(884, 180))
var ui_scale := minf(1.0, minf((viewport_size.x - 32) / base_size.x, (viewport_size.y - 32) / base_size.y))
_panel.size = base_size
_panel.scale = Vector2.ONE * ui_scale
if _panel.has_meta("floating_window_dragged"):
get_parent().get_node("HUDRoot/FloatingWindows").call("clamp_window", _panel)
else:
_panel.position = (viewport_size - base_size * ui_scale) * 0.5
var cockpit := get_parent().get("_cockpit") as Control
if is_instance_valid(cockpit):
_bar.position = cockpit.position + Vector2(0, -40)
else:
_bar.position = Vector2(16, viewport_size.y - 190)
func _exit_tree() -> void:
if _owns_pause and is_inside_tree():
get_tree().paused = _was_paused