# SkillUI (P6) —— 技能窗:主动 / 辅助(被动·支援) / 坐骑 三个分类页 + 等级 + 加点。 # # var sk := preload("res://ui/skill_ui.gd").new() # add_child(sk) # sk.setup(m2client, skill_table, ui_manager) # sk.set_job("WARRIOR") # sk.toggle() # K 键 # # 每行:名字 + Lv X(/M/G/P) + [+](被动 / CANNOT_LEVEL_UP 无加点)+ [被动]/[切换] 标签。 # 点名字 -> drag_skill_id(quickbar 落点读)。`skills_changed` 刷新。 # §3.10:[+] 的显隐逐行对齐 uicharacter.py RefreshSkillPlusButtonList / # __RefreshSkillPlusButton / CanShowPlusButton —— 技能组门(get_skill_group()==0)、 # 三个技能点来源分开取、skillGrade==0、马术页 skillLevel<20、主动页 CanLevelUpSkill、 # 辅助页 HIDE_SUPPORT_SKILL_POINT 整页隐藏。升级结果仍由 GC_SKILL_LEVEL 回包驱动。 extends Node const MobileTouchButton = preload("res://ui/mobile/mobile_touch_button.gd") const TABS := ["主动", "辅助", "坐骑"] const MASTER_SUFFIX := ["", " M", " G", " P"] # master_type 0..3 # uicharacter.py:21 HIDE_SUPPORT_SKILL_POINT —— 本 migration 资源为 True,辅助页整页不出加号。 const HIDE_SUPPORT_SKILL_POINT := true # uicharacter.py __RefreshSkillPlusButton 的 HORSE 分支硬编码 `if skillLevel < 20`。 const RIDING_SKILL_MAX_LEVEL := 20 var client: Node var table: RefCounted # SkillTable var ui: CanvasLayer # UiManager var job := "WARRIOR" var drag_skill_id := 0 var _tab := 0 var _mobile_mode := false var _mobile_quickbar: Node var _mobile_hint: Label var _mobile_drop_slots: Array[MobileTouchButton] = [] var _mobile_drag_quickbar_slot := -1 var _win: Control var _rows := {} # skill_id -> {lv, up} func setup(m2client: Node, skill_table: RefCounted, ui_manager: CanvasLayer) -> void: client = m2client table = skill_table ui = ui_manager if client.has_signal("skills_changed"): client.skills_changed.connect(refresh) func set_job(j: String) -> void: job = j if is_open(): _rebuild() func set_mobile_mode(enabled: bool) -> void: _mobile_mode = enabled if _mobile_mode and _win: close() func set_mobile_quickbar(quickbar: Node) -> void: _mobile_quickbar = quickbar _refresh_mobile_quickbar_targets() func is_open() -> bool: return _win != null and is_instance_valid(_win) func toggle() -> void: if is_open(): close() else: open() func close() -> void: if is_open(): ui.close(_win) _win = null _rows.clear() _mobile_drop_slots.clear() _mobile_drag_quickbar_slot = -1 func _cat_key() -> String: match _tab: 1: return "SUPPORT" 2: return "HORSE" _: return job func open() -> void: if is_open() or table == null: return _win = Control.new() _win.name = "MobileSkillWindow" if _mobile_mode else "SkillWindow" if _mobile_mode: _win.set_anchors_preset(Control.PRESET_TOP_LEFT) _win.position = Vector2.ZERO _win.size = Vector2(760, 340) _win.set_meta("mobile_title", "技能") else: _win.set_anchors_preset(Control.PRESET_CENTER_RIGHT) _win.position = Vector2(-300, -240) _win.size = Vector2(290, 470) _win.set_meta("is_titlebar", true) var panel := Panel.new() panel.set_anchors_preset(Control.PRESET_FULL_RECT) var sb := StyleBoxFlat.new() sb.bg_color = Color(0.08, 0.09, 0.12, 0.95) sb.set_corner_radius_all(4) panel.add_theme_stylebox_override("panel", sb) _win.add_child(panel) var title := Label.new() title.text = "技能" title.position = Vector2(12, 8) if not _mobile_mode else Vector2(14, 48) title.visible = not _mobile_mode if _mobile_mode: title.add_theme_font_size_override("font_size", 15) _win.add_child(title) # 分类页签 var tabrow := HBoxContainer.new() tabrow.name = "tabs" tabrow.position = Vector2(10, 30) if not _mobile_mode else Vector2(14, 48) if _mobile_mode: tabrow.size = Vector2(360, 40) tabrow.add_theme_constant_override("separation", 6) _win.add_child(tabrow) for i in TABS.size(): var b := Button.new() b.text = TABS[i] b.toggle_mode = true b.button_pressed = (i == _tab) if _mobile_mode: b.custom_minimum_size = Vector2(110, 38) b.add_theme_font_size_override("font_size", 12) var ti: int = i b.pressed.connect(func(): _switch_tab(ti)) tabrow.add_child(b) var vb := VBoxContainer.new() vb.name = "list" vb.position = Vector2(10, 60) if not _mobile_mode else Vector2(0, 0) vb.custom_minimum_size = Vector2(270, 0) if not _mobile_mode else Vector2(720, 0) vb.add_theme_constant_override("separation", 2) if _mobile_mode: var scroll := ScrollContainer.new() scroll.name = "SkillScroll" scroll.position = Vector2(14, 92) scroll.size = Vector2(732, 214) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED scroll.mouse_filter = Control.MOUSE_FILTER_STOP scroll.add_child(vb) _win.add_child(scroll) _mobile_hint = Label.new() _mobile_hint.position = Vector2(14, 310) _mobile_hint.size = Vector2(720, 22) _mobile_hint.add_theme_font_size_override("font_size", 10) _mobile_hint.add_theme_color_override("font_color", Color(0.62, 0.72, 0.84)) _mobile_hint.text = "可用点数与技能等级由服务器实时确认" _win.add_child(_mobile_hint) _build_mobile_quickbar_targets() else: _win.add_child(vb) ui.open(_win) _rebuild() func _switch_tab(i: int) -> void: _tab = i if is_open(): var tabrow: HBoxContainer = _win.get_node("tabs") for j in tabrow.get_child_count(): (tabrow.get_child(j) as Button).button_pressed = (j == i) _rebuild() func _rebuild() -> void: var vb: VBoxContainer = _win.get_node_or_null("list") as VBoxContainer if vb == null and _mobile_mode: vb = _win.get_node_or_null("SkillScroll/list") as VBoxContainer if vb == null: return for c in vb.get_children(): c.queue_free() _rows.clear() var ids: Array = table.for_category(_cat_key()) if ids.is_empty(): var e := Label.new() e.text = "(无)" vb.add_child(e) return for id in ids: var row := HBoxContainer.new() row.custom_minimum_size = Vector2(720, 46) if _mobile_mode else Vector2(268, 26) var nm: Control if _mobile_mode: var skill_name := MobileTouchButton.new() skill_name.setup(table.name_of(id), Color(0.34, 0.58, 0.88, 0.88)) skill_name.set_rect_style() skill_name.set_gesture_enabled(true) skill_name.custom_minimum_size = Vector2(300, 42) skill_name.size = Vector2(300, 42) var skill_button := skill_name skill_button.long_pressed.connect(func(): _mobile_begin_skill_drag(id, skill_button)) skill_button.drag_changed.connect(func(_delta: Vector2): _mobile_update_skill_drag(id, skill_button)) skill_button.drag_released.connect(func(_delta: Vector2): _mobile_end_skill_drag(id, skill_button)) nm = skill_name else: var skill_label := Label.new() skill_label.text = table.name_of(id) skill_label.custom_minimum_size = Vector2(140, 0) skill_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER skill_label.add_theme_font_size_override("font_size", 12) nm = skill_label row.add_child(nm) var lv := Label.new() lv.name = "lv" lv.custom_minimum_size = Vector2(100, 42) if _mobile_mode else Vector2(58, 0) lv.vertical_alignment = VERTICAL_ALIGNMENT_CENTER lv.add_theme_font_size_override("font_size", 13 if _mobile_mode else 12) row.add_child(lv) var tag := Label.new() tag.custom_minimum_size = Vector2(76, 42) if _mobile_mode else Vector2(40, 0) tag.vertical_alignment = VERTICAL_ALIGNMENT_CENTER tag.add_theme_font_size_override("font_size", 10) if table.is_passive(id): tag.text = "被动" tag.modulate = Color(0.6, 0.8, 1.0) elif table.is_toggle(id): tag.text = "切换" tag.modulate = Color(0.9, 0.8, 0.5) row.add_child(tag) var sid: int = id if table.can_level_up(id) and not table.is_passive(id): var up := Button.new() up.text = "+" up.custom_minimum_size = Vector2(48, 42) if _mobile_mode else Vector2(28, 22) if _mobile_mode: up.add_theme_font_size_override("font_size", 18) up.pressed.connect(func(): client.skill_up(sid)) row.add_child(up) _rows[id] = {"lv": lv, "up": up} else: _rows[id] = {"lv": lv, "up": null} if not _mobile_mode: nm.gui_input.connect(func(e: InputEvent): if e is InputEventMouseButton and e.pressed and e.button_index == MOUSE_BUTTON_LEFT: drag_skill_id = sid if _mobile_hint: _mobile_hint.text = "已选择:%s;点击快捷栏按钮自动放入空位" % table.name_of(sid) nm.accept_event()) if _mobile_mode: var assign := Button.new() assign.text = "快捷栏" assign.custom_minimum_size = Vector2(92, 42) assign.add_theme_font_size_override("font_size", 11) assign.pressed.connect(func(): _mobile_assign_skill(sid)) row.add_child(assign) vb.add_child(row) refresh() _refresh_mobile_quickbar_targets() func _build_mobile_quickbar_targets() -> void: var title := Label.new() title.text = "快捷栏" title.position = Vector2(390, 48) title.size = Vector2(60, 38) title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER title.add_theme_font_size_override("font_size", 12) title.add_theme_color_override("font_color", Color(0.95, 0.82, 0.54)) title.mouse_filter = Control.MOUSE_FILTER_IGNORE _win.add_child(title) for i in 8: var target := MobileTouchButton.new() target.setup("—", Color(0.42, 0.65, 0.92, 0.9)) target.set_rect_style() target.set_gesture_enabled(true) target.position = Vector2(448 + i * 38, 48) target.size = Vector2(34, 38) target.custom_minimum_size = Vector2(34, 38) var slot := i target.pressed.connect(func(): _mobile_drop_on_quickbar(slot)) target.long_pressed.connect(func(): _mobile_begin_quickbar_drag(slot)) target.drag_released.connect(func(_delta: Vector2): _mobile_end_quickbar_drag(slot, target)) _win.add_child(target) _mobile_drop_slots.append(target) func _mobile_begin_skill_drag(skill_id: int, _button: MobileTouchButton) -> void: drag_skill_id = skill_id if _mobile_hint: _mobile_hint.text = "拖到上方快捷栏槽位,松手放入" func _mobile_update_skill_drag(skill_id: int, _button: MobileTouchButton) -> void: if drag_skill_id == skill_id and _mobile_hint: _mobile_hint.text = "拖到上方快捷栏槽位,松手放入" func _mobile_end_skill_drag(skill_id: int, button: MobileTouchButton) -> void: if drag_skill_id != skill_id: return var target := _mobile_drop_slot_at(button.drag_end_position()) if target >= 0 and _mobile_quickbar and _mobile_quickbar.has_method("assign"): if bool(_mobile_quickbar.assign(target, "skill", skill_id)): drag_skill_id = 0 _refresh_mobile_quickbar_targets() if _mobile_hint: _mobile_hint.text = "已放入快捷栏第 %d 格" % (target + 1) return if _mobile_hint: _mobile_hint.text = "已选择:%s;可拖到快捷栏" % table.name_of(skill_id) func _mobile_drop_on_quickbar(slot: int) -> void: if _mobile_quickbar == null or not _mobile_quickbar.has_method("assign"): return if drag_skill_id != 0: if bool(_mobile_quickbar.assign(slot, "skill", drag_skill_id)): drag_skill_id = 0 _refresh_mobile_quickbar_targets() if _mobile_hint: _mobile_hint.text = "已放入快捷栏第 %d 格" % (slot + 1) func _mobile_begin_quickbar_drag(slot: int) -> void: _mobile_drag_quickbar_slot = slot if _mobile_hint: _mobile_hint.text = "拖到另一个快捷栏槽位以交换" func _mobile_end_quickbar_drag(slot: int, button: MobileTouchButton) -> void: if _mobile_drag_quickbar_slot != slot: return var target := _mobile_drop_slot_at(button.drag_end_position()) if target >= 0 and target != slot and _mobile_quickbar \ and _mobile_quickbar.has_method("mobile_swap"): _mobile_quickbar.mobile_swap(slot, target) _refresh_mobile_quickbar_targets() if _mobile_hint: _mobile_hint.text = "已交换快捷栏位置" _mobile_drag_quickbar_slot = -1 func _mobile_drop_slot_at(position: Vector2) -> int: for i in _mobile_drop_slots.size(): var slot := _mobile_drop_slots[i] if slot and slot.get_global_rect().has_point(position): return i return -1 func _refresh_mobile_quickbar_targets() -> void: if not _mobile_mode: return for i in _mobile_drop_slots.size(): var slot := _mobile_drop_slots[i] if slot == null or _mobile_quickbar == null: continue var state: Dictionary = _mobile_quickbar.mobile_slot_state(i) \ if _mobile_quickbar.has_method("mobile_slot_state") else {} var label := "—" if state.get("kind", "") == "skill" and table: label = table.name_of(int(state.get("id", 0))).substr(0, 4) elif state.get("kind", "") == "item": label = "物" elif state.get("kind", "") == "emote": label = "表" slot.set_caption(label) func _mobile_assign_skill(skill_id: int) -> void: if _mobile_quickbar == null or not _mobile_quickbar.has_method("assign_mobile"): if _mobile_hint: _mobile_hint.text = "快捷栏暂不可用" return if bool(_mobile_quickbar.assign_mobile("skill", skill_id)): drag_skill_id = 0 _refresh_mobile_quickbar_targets() if _mobile_hint: _mobile_hint.text = "已放入快捷栏" else: if _mobile_hint: _mobile_hint.text = "快捷栏已满,请先移除一个技能" func refresh() -> void: if not is_open(): return var lv := {} var ms := {} for s in client.get_skills(): lv[int(s["id"])] = int(s["level"]) ms[int(s["id"])] = int(s.get("master", 0)) # RefreshSkillPlusButtonList(uicharacter.py:984)→ __RefreshSkillPlusButton(:937): # 每页的技能点来源分开取,技能组未选时整列隐藏加号。 var page := _cat_key() var can_use_now := _can_use_skill_now() var stat_point := _stat_point_for(page) var plv := _player_level() for id in _rows: var lvl: int = lv.get(id, 0) var m: int = clampi(ms.get(id, 0), 0, 3) _rows[id]["lv"].text = "Lv %d%s" % [lvl, MASTER_SUFFIX[m]] var up: Button = _rows[id]["up"] if up != null: up.visible = _can_show_plus(id, lvl, m, page, can_use_now, stat_point, plv) # __CanUseSkillNow(uicharacter.py:1270):net.GetMainActorSkillGroup()==0 -> False。 func _can_use_skill_now() -> bool: if client.has_method("get_skill_group"): return int(client.get_skill_group()) != 0 return true # skillPageStatDict(uicharacter.py:225):SUPPORT=POINT_SUB_SKILL(27) / # ACTIVE=POINT_SKILL(28) / HORSE=POINT_HORSE_SKILL(113)。三个来源分开取,不合并成一个。 func _stat_point_for(page: String) -> int: if not client.has_method("get_points"): return 1 var p: Dictionary = client.get_points() match page: "SUPPORT": return int(p.get("skill_support", 0)) "HORSE": return int(p.get("skill_horse", 0)) _: return int(p.get("skill_active", 0)) func _player_level() -> int: if not client.has_method("get_points"): return 1 return int(client.get_points().get("level", 1)) # __RefreshSkillPlusButton 每槽体(uicharacter.py:937)+ CanShowPlusButton(:925): # 技能组未选 -> 全隐藏;辅助页 HIDE_SUPPORT_SKILL_POINT -> 整页隐藏; # statPoint <= 0 -> 不显示;skillGrade != 0(已过 0 级,走大师书)-> 跳过; # HORSE 页:GetStatus(LEVEL) >= GetSkillLevelLimit(idx) 时,skillLevel < 20 才显示; # 其余页:skill.CanLevelUpSkill(idx, level)。 func _can_show_plus(id: int, level: int, grade: int, page: String, can_use_now: bool, stat_point: int, plv: int) -> bool: if not can_use_now: return false if page == "SUPPORT" and HIDE_SUPPORT_SKILL_POINT: return false if stat_point <= 0: return false if grade != 0: return false if page == "HORSE": if plv >= table.skill_level_limit(id): return level < RIDING_SKILL_MAX_LEVEL return false return table.can_level_up_skill(id, level)