完善客户端功能并同步差距文档
This commit is contained in:
+110
-13
@@ -1,29 +1,36 @@
|
||||
# Quickbar (P6) —— 快捷栏:36 槽(4 页 × 9 格),数字键 1-9 施放技能 / 用物品。
|
||||
# Quickbar (P6) —— 服务端 36 槽;参考客户端本地显示 4 页 × 8 格(32 格)。
|
||||
#
|
||||
# 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 切页
|
||||
# # 输入:数字 1..4 -> 本页 0..3,F1..F4 -> 本页 4..7。
|
||||
#
|
||||
# 技能施放:先发送 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
|
||||
|
||||
const PlayerSkill := preload("res://player_skill.gd")
|
||||
|
||||
signal skill_activated(skill_id: int) # game_scene 接它播技能特效
|
||||
# §3.8 修改 1:三层校验挡下(code = OnCannotUseSkill 字符串码),game_scene 弹文案
|
||||
signal skill_rejected(skill_id: int, code: String)
|
||||
|
||||
const CM := 100.0
|
||||
const DEFAULT_CD := 2.0
|
||||
const SLOTS_PER_PAGE := 9
|
||||
const SLOTS_PER_PAGE := 8
|
||||
const PAGE_COUNT := 4
|
||||
const SLOT_COUNT := SLOTS_PER_PAGE * PAGE_COUNT
|
||||
const SLOT_COUNT := 36 # QUICKSLOT_MAX_NUM;最后 4 格没有本地显示页
|
||||
|
||||
var client: Node
|
||||
var table: RefCounted # SkillTable
|
||||
var net_play: Node # NetPlay(可空)——技能三层校验的运行期上下文来源
|
||||
var _skill_gate: RefCounted # PlayerSkill(§3.8 修改 1)
|
||||
var _player_getter: Callable
|
||||
var _root: Control
|
||||
var item_mouse: Node
|
||||
var _slots := [] # 当前页 UI:[{btn, cd, lbl}]
|
||||
var _state := [] # 36 个服务端槽:[{kind, id, cd_end:float}]
|
||||
var _page := 0
|
||||
@@ -33,6 +40,8 @@ func setup(m2client: Node, skill_table: RefCounted, parent: Node, player_getter:
|
||||
client = m2client
|
||||
table = skill_table
|
||||
_player_getter = player_getter
|
||||
_skill_gate = PlayerSkill.new()
|
||||
_skill_gate.setup(client, table)
|
||||
for _i in SLOT_COUNT:
|
||||
_state.append({"kind": "", "id": 0, "cd_end": 0.0})
|
||||
_build(parent)
|
||||
@@ -43,7 +52,7 @@ func setup(m2client: Node, skill_table: RefCounted, parent: Node, player_getter:
|
||||
restore_from_server()
|
||||
|
||||
# 从服务器 GC_QUICKSLOT_* 恢复全部 36 个快捷栏槽位。
|
||||
# 服务器 type:1 道具 / 2 技能 / 3 命令 / 4 表情;ref = 道具格 / 技能 id。
|
||||
# 参考 ESlotType:1 道具 / 2 技能 / 3 表情 / 4 商店(不可执行)。
|
||||
func restore_from_server() -> void:
|
||||
if client == null or not client.has_method("get_quickslots"):
|
||||
return
|
||||
@@ -57,19 +66,23 @@ func restore_from_server() -> void:
|
||||
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}
|
||||
3: restored[pos] = {"kind": "emote", "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:
|
||||
func assign(slot: int, kind: String, id: int, persist := true) -> bool:
|
||||
if slot < 0 or slot >= SLOTS_PER_PAGE:
|
||||
return
|
||||
return false
|
||||
if kind not in ["skill", "item", "emote"] or id <= 0:
|
||||
return false
|
||||
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
|
||||
var type := 2 if kind == "skill" else 1 if kind == "item" else 3 if kind == "emote" else 0
|
||||
if type == 0 or not client.quickslot_add(global, type, id):
|
||||
return
|
||||
return false
|
||||
_state[global] = {"kind": kind, "id": id, "cd_end": 0.0}
|
||||
_refresh_slot(slot)
|
||||
return true
|
||||
|
||||
func activate(slot: int) -> void:
|
||||
if slot < 0 or slot >= SLOTS_PER_PAGE:
|
||||
@@ -91,6 +104,38 @@ func activate(slot: int) -> void:
|
||||
var target_vid := 0
|
||||
if client.has_method("get_target"):
|
||||
target_vid = int(client.get_target().get("vid", 0))
|
||||
# §3.8 修改 1:三层合法性校验(ClickSkillSlot -> __CanUseSkill /
|
||||
# __CheckSkillUsable / __UseSkill)。挡下就发 skill_rejected(静默码除外),
|
||||
# TOGGLE_OFF 只发 use_skill 关掉开关技、不施法。
|
||||
if _skill_gate:
|
||||
if net_play and net_play.has_method("skill_context"):
|
||||
var ctx: Dictionary = net_play.skill_context()
|
||||
for k in ctx:
|
||||
_skill_gate.set(k, ctx[k])
|
||||
_skill_gate.slot_cd_end = s.cd_end
|
||||
var gate: Dictionary = _skill_gate.click_skill_slot(int(s.id))
|
||||
if not bool(gate.get("ok", false)):
|
||||
var code := String(gate.get("code", ""))
|
||||
# PythonPlayerSkill::__CheckSpecialSkill(123) 不是失败:参考端
|
||||
# 直接进入 NEW_Fishing,并把 EQUIP_FISHING_ROD 作为唯一错误反馈。
|
||||
if code == "SPECIAL_SKILL" and int(s.id) == PlayerSkill.SKILL_INDEX_FISHING \
|
||||
and net_play and net_play.has_method("activate_fishing"):
|
||||
var fishing_result := String(net_play.activate_fishing())
|
||||
if fishing_result == "OK":
|
||||
skill_activated.emit(int(s.id))
|
||||
elif not PlayerSkill.is_silent_code(fishing_result):
|
||||
skill_rejected.emit(int(s.id), fishing_result)
|
||||
return
|
||||
if code == "TOGGLE_OFF":
|
||||
if client.has_method("use_skill"):
|
||||
client.use_skill(int(s.id), 0)
|
||||
skill_activated.emit(int(s.id))
|
||||
elif not PlayerSkill.is_silent_code(code):
|
||||
skill_rejected.emit(int(s.id), code)
|
||||
return
|
||||
var resolved := int(gate.get("target_vid", 0))
|
||||
if resolved != 0:
|
||||
target_vid = resolved
|
||||
# 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.
|
||||
@@ -100,9 +145,42 @@ func activate(slot: int) -> void:
|
||||
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
|
||||
# §3.4:扇形 / 圆形技能的附加 fly-targeting(主目标已在 use_skill 里发过;
|
||||
# PythonPlayerSkill.cpp:684 的 `if (dwTargetMaxCount>0 …)` 补目标块)。
|
||||
if target_vid != 0 and net_play and table \
|
||||
and net_play.has_method("send_fly_targeting") \
|
||||
and (table.is_fan_range(int(s.id)) or table.is_circle_range(int(s.id))):
|
||||
var lvl := _skill_level(int(s.id))
|
||||
net_play.send_fly_targeting(target_vid, float(table.target_range(int(s.id))),
|
||||
table.target_count(int(s.id), lvl), table.fly_shape(int(s.id)))
|
||||
skill_activated.emit(int(s.id))
|
||||
elif s.kind == "item":
|
||||
client.use_item(1, s.id) # WINDOW_INVENTORY
|
||||
elif s.kind == "emote" and client.has_method("send_emoticon"):
|
||||
client.send_emoticon(s.id)
|
||||
|
||||
# net_play 的 MODE_USE_SKILL 预约进射程后回调(use_skill_hook)——slot 为全局快捷栏
|
||||
# 索引。返回是否确实发起了施法(对齐参考端 `if (__UseSkill(slot)) __ClearReservedAction()`)。
|
||||
func activate_reserved(global_slot: int) -> bool:
|
||||
if global_slot < 0 or global_slot >= SLOT_COUNT:
|
||||
return false
|
||||
var local := global_slot - _page * SLOTS_PER_PAGE
|
||||
if local < 0 or local >= SLOTS_PER_PAGE:
|
||||
return false
|
||||
var s: Dictionary = _state[global_slot]
|
||||
if s.kind != "skill" or s.id == 0 or _now() < s.cd_end:
|
||||
return false
|
||||
activate(local)
|
||||
return true
|
||||
|
||||
# client.get_skills() 里该技能的当前等级(quickbar 施法时算 target_count 用)。
|
||||
func _skill_level(sid: int) -> int:
|
||||
if client == null or not client.has_method("get_skills"):
|
||||
return 0
|
||||
for sk in client.get_skills():
|
||||
if int(sk.get("id", 0)) == sid:
|
||||
return int(sk.get("level", 0))
|
||||
return 0
|
||||
|
||||
func set_page(page: int) -> void:
|
||||
if page < 0 or page >= PAGE_COUNT or page == _page:
|
||||
@@ -111,6 +189,13 @@ func set_page(page: int) -> void:
|
||||
_move_from = -1
|
||||
_refresh_page()
|
||||
|
||||
func toggle_visible() -> void:
|
||||
if _root:
|
||||
_root.visible = not _root.visible
|
||||
|
||||
func is_visible() -> bool:
|
||||
return _root != null and _root.visible
|
||||
|
||||
func _global_slot(local_slot: int) -> int:
|
||||
return _page * SLOTS_PER_PAGE + local_slot
|
||||
|
||||
@@ -141,7 +226,7 @@ func _swap(local_a: int, local_b: int) -> void:
|
||||
func _build(parent: Node) -> void:
|
||||
_root = Control.new()
|
||||
_root.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
||||
_root.position = Vector2(-9 * 23, -54)
|
||||
_root.position = Vector2(-SLOTS_PER_PAGE * 23, -54)
|
||||
parent.add_child(_root)
|
||||
var prev := Button.new()
|
||||
prev.text = "‹"
|
||||
@@ -150,12 +235,12 @@ func _build(parent: Node) -> void:
|
||||
_root.add_child(prev)
|
||||
var next := Button.new()
|
||||
next.text = "›"
|
||||
next.position = Vector2(9 * 44 + 2, 7)
|
||||
next.position = Vector2(SLOTS_PER_PAGE * 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.position = Vector2(SLOTS_PER_PAGE * 44 + 7, 38)
|
||||
page_label.add_theme_font_size_override("font_size", 9)
|
||||
_root.add_child(page_label)
|
||||
var row := HBoxContainer.new()
|
||||
@@ -194,8 +279,20 @@ func _build(parent: Node) -> void:
|
||||
_clear(idx))
|
||||
row.add_child(slot)
|
||||
_slots.append({"btn": slot, "cd": cd, "lbl": lbl})
|
||||
if item_mouse and item_mouse.has_method("register_target"):
|
||||
var local_idx: int = i
|
||||
item_mouse.register_target(slot,
|
||||
func(payload: Dictionary): return _drop_mouse_item(payload, local_idx), self)
|
||||
_refresh_page()
|
||||
|
||||
func _drop_mouse_item(payload: Dictionary, slot: int) -> bool:
|
||||
if payload.is_empty() or String(payload.get("source", "")) != "inventory":
|
||||
return false
|
||||
if int(payload.get("window", -1)) != 1 or int(payload.get("cell", -1)) < 0:
|
||||
return false
|
||||
# QuickSlot inventory entries store the source inventory cell, not the vnum.
|
||||
return assign(slot, "item", int(payload.get("cell", -1)))
|
||||
|
||||
func _try_drop(slot: int) -> void:
|
||||
# skill_ui 把待拖技能放在 drag_skill_id;这里落点接
|
||||
var sk_ui := _find_skill_ui()
|
||||
|
||||
Reference in New Issue
Block a user