# Quickbar (P6) —— 快捷栏:36 槽(4 页 × 9 格),数字键 1-9 施放技能 / 用物品。 # # var qb := preload("res://ui/quickbar.gd").new() # add_child(qb) # qb.setup(m2client, skill_table, canvas_parent, player_getter) # qb.assign(0, "skill", 1) # 当前页 0 号格放技能 id 1 # qb.assign(1, "item", 5) # 当前页 1 号格放背包 5 格的药水 # # 输入: game_scene 里 KEY_1..KEY_9 -> qb.activate(n),F1..F4 切页 # # 技能施放:先发送 M2Client.use_skill(skill_id, target_vid),再同步 # M2Client.cast_skill(motion_idx, heading, x_cm, y_cm)(朝向 = 玩家 yaw)。 # 冷却:本地预测时长由 SkillTable 提供;GC_SKILL_COOLTIME_END 可提前解锁。 extends Node signal skill_activated(skill_id: int) # game_scene 接它播技能特效 const CM := 100.0 const DEFAULT_CD := 2.0 const SLOTS_PER_PAGE := 9 const PAGE_COUNT := 4 const SLOT_COUNT := SLOTS_PER_PAGE * PAGE_COUNT var client: Node var table: RefCounted # SkillTable var _player_getter: Callable var _root: Control var _slots := [] # 当前页 UI:[{btn, cd, lbl}] var _state := [] # 36 个服务端槽:[{kind, id, cd_end:float}] var _page := 0 var _move_from := -1 func setup(m2client: Node, skill_table: RefCounted, parent: Node, player_getter: Callable) -> void: client = m2client table = skill_table _player_getter = player_getter for _i in SLOT_COUNT: _state.append({"kind": "", "id": 0, "cd_end": 0.0}) _build(parent) if client.has_signal("skill_cooldown_end"): client.skill_cooldown_end.connect(_on_cd_end) if client.has_signal("quickslots_changed"): client.quickslots_changed.connect(restore_from_server) restore_from_server() # 从服务器 GC_QUICKSLOT_* 恢复全部 36 个快捷栏槽位。 # 服务器 type:1 道具 / 2 技能 / 3 命令 / 4 表情;ref = 道具格 / 技能 id。 func restore_from_server() -> void: if client == null or not client.has_method("get_quickslots"): return var restored := [] for _i in SLOT_COUNT: restored.append({"kind": "", "id": 0, "cd_end": 0.0}) for qs in client.get_quickslots(): var pos := int(qs.get("pos", -1)) if pos < 0 or pos >= SLOT_COUNT: continue match int(qs.get("type", 0)): 1: restored[pos] = {"kind": "item", "id": int(qs.get("ref", 0)), "cd_end": 0.0} 2: restored[pos] = {"kind": "skill", "id": int(qs.get("ref", 0)), "cd_end": 0.0} _state = restored _refresh_page() func assign(slot: int, kind: String, id: int, persist := true) -> void: if slot < 0 or slot >= SLOTS_PER_PAGE: return var global := _global_slot(slot) _state[global] = {"kind": kind, "id": id, "cd_end": 0.0} if persist and client and client.has_method("quickslot_add"): var type := 2 if kind == "skill" else 1 if kind == "item" else 0 if type == 0 or not client.quickslot_add(global, type, id): return _refresh_slot(slot) func activate(slot: int) -> void: if slot < 0 or slot >= SLOTS_PER_PAGE: return var global := _global_slot(slot) var s: Dictionary = _state[global] if s.kind == "" or s.id == 0: return if _now() < s.cd_end: return if s.kind == "skill": var mi: int = table.motion_idx_of(s.id) if table else 0 var p: Node3D = _player_getter.call() if _player_getter.is_valid() else null var yaw := 0.0 var xy := Vector2.ZERO if p: yaw = fposmod(90.0 - rad_to_deg(p.rotation.y), 360.0) xy = Vector2(p.position.x * CM, -p.position.z * CM) var target_vid := 0 if client.has_method("get_target"): target_vid = int(client.get_target().get("vid", 0)) # use_skill is the authoritative server intent. Keep the CG_MOVE skill # state packet as a separate visual/action-state sync, just like the # original client did. var intent_sent := true if client.has_method("use_skill"): intent_sent = client.use_skill(int(s.id), target_vid) if intent_sent and client.cast_skill(mi, yaw, int(xy.x), int(xy.y)): s.cd_end = _now() + _skill_cooldown(int(s.id)) _state[global] = s skill_activated.emit(int(s.id)) elif s.kind == "item": client.use_item(1, s.id) # WINDOW_INVENTORY func set_page(page: int) -> void: if page < 0 or page >= PAGE_COUNT or page == _page: return _page = page _move_from = -1 _refresh_page() func _global_slot(local_slot: int) -> int: return _page * SLOTS_PER_PAGE + local_slot func _clear(slot: int) -> void: if slot < 0 or slot >= SLOTS_PER_PAGE: return var global := _global_slot(slot) if _state[global].kind == "": return if client and client.has_method("quickslot_del") and not client.quickslot_del(global): return _state[global] = {"kind": "", "id": 0, "cd_end": 0.0} _refresh_slot(slot) func _swap(local_a: int, local_b: int) -> void: var a := _global_slot(local_a) var b := _global_slot(local_b) if client and client.has_method("quickslot_swap") and not client.quickslot_swap(a, b): return var tmp: Dictionary = _state[a] _state[a] = _state[b] _state[b] = tmp _refresh_slot(local_a) _refresh_slot(local_b) # --- UI --------------------------------------------------------------- func _build(parent: Node) -> void: _root = Control.new() _root.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) _root.position = Vector2(-9 * 23, -54) parent.add_child(_root) var prev := Button.new() prev.text = "‹" prev.position = Vector2(-24, 7) prev.pressed.connect(func(): set_page(posmod(_page - 1, PAGE_COUNT))) _root.add_child(prev) var next := Button.new() next.text = "›" next.position = Vector2(9 * 44 + 2, 7) next.pressed.connect(func(): set_page(posmod(_page + 1, PAGE_COUNT))) _root.add_child(next) var page_label := Label.new() page_label.name = "page" page_label.position = Vector2(9 * 44 + 7, 38) page_label.add_theme_font_size_override("font_size", 9) _root.add_child(page_label) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 4) _root.add_child(row) for i in SLOTS_PER_PAGE: var slot := Panel.new() slot.custom_minimum_size = Vector2(40, 40) var sb := StyleBoxFlat.new() sb.bg_color = Color(0.1, 0.11, 0.14, 0.9) sb.border_color = Color(0.35, 0.32, 0.26) sb.set_border_width_all(1) slot.add_theme_stylebox_override("panel", sb) var num := Label.new() num.text = str(i + 1) num.position = Vector2(3, 1) num.add_theme_font_size_override("font_size", 10) slot.add_child(num) var lbl := Label.new() lbl.name = "lbl" lbl.position = Vector2(3, 15) lbl.add_theme_font_size_override("font_size", 9) slot.add_child(lbl) var cd := ColorRect.new() cd.name = "cd" cd.color = Color(0, 0, 0, 0.55) cd.set_anchors_preset(Control.PRESET_FULL_RECT) cd.visible = false slot.add_child(cd) var idx := i slot.gui_input.connect(func(e: InputEvent): if e is InputEventMouseButton and e.pressed: if e.button_index == MOUSE_BUTTON_LEFT: _try_drop(idx) elif e.button_index == MOUSE_BUTTON_RIGHT: _clear(idx)) row.add_child(slot) _slots.append({"btn": slot, "cd": cd, "lbl": lbl}) _refresh_page() func _try_drop(slot: int) -> void: # skill_ui 把待拖技能放在 drag_skill_id;这里落点接 var sk_ui := _find_skill_ui() if sk_ui and sk_ui.drag_skill_id != 0: assign(slot, "skill", sk_ui.drag_skill_id) sk_ui.drag_skill_id = 0 _move_from = -1 return if _move_from >= 0 and _move_from != slot: _swap(_move_from, slot) _move_from = -1 return if _state[_global_slot(slot)].kind != "": _move_from = slot func _find_skill_ui() -> Node: var tree := get_tree() if tree == null or tree.root == null: return null for n in tree.root.find_children("*", "Node", true, false): if n.get_script() and n.has_method("set_job") and "drag_skill_id" in n: return n return null func _refresh_slot(slot: int) -> void: if slot < 0 or slot >= _slots.size(): return var s: Dictionary = _state[_global_slot(slot)] var txt := "" if s.kind == "skill" and table: txt = table.name_of(s.id).substr(0, 5) elif s.kind == "item": txt = "#%d" % s.id _slots[slot].lbl.text = txt func _refresh_page() -> void: if _root and _root.has_node("page"): _root.get_node("page").text = "F%d" % (_page + 1) for slot in _slots.size(): _refresh_slot(slot) func _on_cd_end(skill_id: int) -> void: for i in _state.size(): var s: Dictionary = _state[i] if s.kind == "skill" and s.id == skill_id: s.cd_end = 0.0 _state[i] = s func _process(_dt: float) -> void: var t := _now() for slot in _slots.size(): var s: Dictionary = _state[_global_slot(slot)] var on_cd: bool = t < s.cd_end _slots[slot].cd.visible = on_cd if on_cd: var duration := _skill_cooldown(int(s.id)) var frac: float = clampf((s.cd_end - t) / duration, 0.0, 1.0) _slots[slot].cd.anchor_top = 1.0 - frac func _skill_cooldown(skill_id: int) -> float: if table and table.has_method("cooldown_of"): var level := 0 if client and client.has_method("get_skills"): for skill in client.get_skills(): if int(skill.get("id", 0)) == skill_id: level = int(skill.get("level", 0)) break var configured := float(table.cooldown_of(skill_id, level)) if configured > 0.0: return configured return DEFAULT_CD func _now() -> float: return Time.get_ticks_msec() / 1000.0