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 _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.3) _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("0a1422c7"), 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: _screen.show() _build_management() func _close() -> void: _screen.hide() 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) 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" % _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)