feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复

- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+664 -3
View File
@@ -19,6 +19,7 @@ const UiScript = preload("res://ui/uiscript.gd")
const UiBuild = preload("res://ui/ui_build.gd")
const UiAssets = preload("res://ui/ui_assets.gd")
const MobileTouchButton = preload("res://ui/mobile/mobile_touch_button.gd")
const SkillSlotMap = preload("res://skill_slot_map.gd")
# --- EPointTypesm2dev Packet.h),只列本窗要用的 ---
const P_LEVEL := 1
@@ -41,6 +42,7 @@ const P_CASTING_SPEED := 21
const P_MAGIC_ATT_GRADE := 22
const P_MAGIC_DEF_GRADE := 23
const P_STAT := 26 # 남은 능력치 포인트
const P_SKILL := 27 # 남은 스킬 포인트
const P_MIN_ATK := 29
const P_MAX_ATK := 30
const P_PARTY_ATT_GRADE := 91 # player.ATTACKER_BONUS
@@ -49,6 +51,39 @@ const P_DEF_GRADE_BONUS := 96 # player.DEF_BONUS
const P_MIN_MAGIC_WEP := 202
const P_MAX_MAGIC_WEP := 203
const EMOTE_NAMES := {
1: "喝彩", 2: "拍手", 3: "肯定", 4: "原谅",
5: "愤怒", 6: "诱惑", 7: "悲伤", 8: "害羞",
9: "欢呼", 10: "嘲讽", 11: "思考", 12: "跳舞 1",
13: "跳舞 2", 14: "跳舞 3", 15: "跳舞 4", 16: "跳舞 5",
17: "跳舞 6", 18: "大笑",
51: "亲吻", 52: "法式深吻", 53: "耳光",
}
const EMOTE_ICONS := {
1: "ETC/ymir work/ui/game/windows/emotion_cheers_1.sub",
2: "ETC/ymir work/ui/game/windows/emotion_clap.sub",
3: "icon/icon/action/congratulation.tga",
4: "icon/icon/action/forgive.tga",
5: "icon/icon/action/angry.tga",
6: "icon/icon/action/attractive.tga",
7: "icon/icon/action/sad.tga",
8: "icon/icon/action/shy.tga",
9: "icon/icon/action/cheerup.tga",
10: "icon/icon/action/banter.tga",
11: "icon/icon/action/joy.tga",
12: "icon/icon/action/dance1.tga",
13: "icon/icon/action/dance2.tga",
14: "icon/icon/action/dance3.tga",
15: "icon/icon/action/dance4.tga",
16: "icon/icon/action/dance5.tga",
17: "icon/icon/action/dance6.tga",
18: "ETC/ymir work/ui/game/windows/emotion_cheers_2.sub",
51: "ETC/ymir work/ui/game/windows/emotion_kiss.sub",
52: "ETC/ymir work/ui/game/windows/emotion_french_kiss.sub",
53: "ETC/ymir work/ui/game/windows/emotion_slap.sub",
}
# uicharacter.py statusPlusCommandDict / statusMinusCommandDict
const PLUS_CMD := {"HTH": "/stat ht", "INT": "/stat iq", "STR": "/stat st", "DEX": "/stat dx"}
const MINUS_CMD := {"HTH": "/stat- ht", "INT": "/stat- iq", "STR": "/stat- st", "DEX": "/stat- dx"}
@@ -57,10 +92,15 @@ const STATES := ["STATUS", "SKILL", "EMOTICON", "QUEST"]
const PAGE := {"STATUS": "Character_Page", "SKILL": "Skill_Page", "EMOTICON": "Emoticon_Page", "QUEST": "Quest_Page"}
const TITLEBAR := {"STATUS": "Character_TitleBar", "SKILL": "Skill_TitleBar", "EMOTICON": "Emoticon_TitleBar", "QUEST": "Quest_TitleBar"}
const TAB_BUTTON := {"STATUS": "Tab_Button_01", "SKILL": "Tab_Button_02", "EMOTICON": "Tab_Button_03", "QUEST": "Tab_Button_04"}
const TAB_IMAGE := {"STATUS": "Tab_01", "SKILL": "Tab_02", "EMOTICON": "Tab_03", "QUEST": "Tab_04"}
const FACE_BY_JOB := ["face_warrior", "face_assassin", "face_sura", "face_shaman"]
var ui: CanvasLayer # UiManager
var client: Node # M2Client
var skill_table: RefCounted # SkillTable
var item_mouse: Node # MouseController
var drag_skill_id := 0 # Quickbar 可读取的待拖拽技能
var _preview_group := 1 # 40250 uicharacter.py curSelectedSkillGroup (group==0 时预览分支)
var assets_root := ""
var uiscript_dir := ""
@@ -74,23 +114,66 @@ var _mobile_labels := {}
var _mobile_stats := {}
var _mobile_equipment_labels: Array[Label] = []
var _mobile_hint: Label
var _quest_showing_start_index := 0
func setup(ui_manager: CanvasLayer, m2client: Node, assets := "") -> void:
func setup(ui_manager: CanvasLayer, m2client: Node, assets := "", st_table: RefCounted = null, imouse: Node = null) -> void:
ui = ui_manager
client = m2client
assets_root = assets
if imouse != null:
item_mouse = imouse
if assets_root == "" and ui and "assets_root" in ui:
assets_root = ui.assets_root
uiscript_dir = assets_root.path_join("uiscript/uiscript")
if not DirAccess.dir_exists_absolute(uiscript_dir):
uiscript_dir = assets_root.path_join("uiscript")
skill_table = st_table
if skill_table == null:
var st_script := load("res://ui/skill_table.gd")
if st_script:
skill_table = st_script.new()
for lang in ["en", "common"]:
var desc_path := assets_root.path_join("locale/locale/%s/skilldesc.txt" % lang)
if not FileAccess.file_exists(desc_path):
desc_path = assets_root.path_join("locale/%s/skilldesc.txt" % lang)
if FileAccess.file_exists(desc_path) and skill_table.has_method("load_file"):
skill_table.load_file(desc_path)
break
for lang in ["en", "common"]:
var tbl_path := assets_root.path_join("locale/locale/%s/skilltable.txt" % lang)
if not FileAccess.file_exists(tbl_path):
tbl_path = assets_root.path_join("locale/%s/skilltable.txt" % lang)
if FileAccess.file_exists(tbl_path) and skill_table.has_method("load_table"):
skill_table.load_table(tbl_path)
break
elif skill_table != null and skill_table.has_method("load_table") and skill_table.has_method("is_passive"):
if not skill_table.is_passive(121):
for lang in ["en", "common"]:
var tbl_path := assets_root.path_join("locale/locale/%s/skilltable.txt" % lang)
if not FileAccess.file_exists(tbl_path):
tbl_path = assets_root.path_join("locale/%s/skilltable.txt" % lang)
if FileAccess.file_exists(tbl_path):
skill_table.load_table(tbl_path)
break
if client and client.has_signal("points_changed"):
client.points_changed.connect(_on_points_changed)
if client and client.has_signal("entity_main_set"):
client.entity_main_set.connect(func(_v): if is_open(): _refresh())
if client and client.has_signal("skills_changed"):
client.skills_changed.connect(func(): if is_open(): _refresh())
if client and client.has_signal("skill_group_changed"):
client.skill_group_changed.connect(func(_g): if is_open(): _refresh())
if ui and ui.has_signal("window_closed"):
ui.window_closed.connect(func(w):
if not _win.is_empty() and _win.get("root") == w:
_win = {}
)
func set_job(_j: String) -> void:
pass
func set_mobile_mode(enabled: bool) -> void:
if not enabled and _mobile_root != null:
if enabled != _mobile_mode and is_open():
var old_mode := _mobile_mode
_mobile_mode = true
close()
@@ -106,7 +189,7 @@ func is_open() -> bool:
func toggle() -> void:
if is_open(): close()
else: open()
else: open("STATUS")
func close() -> void:
if _mobile_mode:
@@ -140,8 +223,21 @@ func open(state := "") -> void:
_win = ui.open_script(path, assets_root)
if not is_open():
return
var root_w: Control = _win.get("root") as Control
if root_w:
var vp_size := Vector2(800.0, 600.0)
if root_w.is_inside_tree():
vp_size = root_w.get_viewport_rect().size
elif ui and ui.is_inside_tree():
vp_size = ui.get_viewport().get_visible_rect().size
var target_y: float = (vp_size.y - 37.0 - 361.0) / 2.0
root_w.position = Vector2(24.0, maxf(10.0, target_y))
_wire_titlebars()
_wire_tabs()
_wire_stat_buttons()
_wire_skills()
_wire_emoticons()
_wire_quests()
_set_state(_state)
_refresh()
@@ -444,9 +540,17 @@ func _set_state(state: String) -> void:
var bar := _node(TITLEBAR[st])
if bar:
bar.visible = on
var tab_img := _node(TAB_IMAGE[st])
if tab_img:
tab_img.visible = on
var btn := _node(TAB_BUTTON[st])
if btn is BaseButton and btn.toggle_mode:
btn.set_pressed_no_signal(on)
match state:
"SKILL": _refresh_skills()
"EMOTICON": _refresh_emotes()
"QUEST": _refresh_quests()
"STATUS": _refresh()
# --- stat +/- buttons ----------------------------------------------------
@@ -477,6 +581,15 @@ func _u32(v: int) -> int:
func _refresh() -> void:
if not is_open() or client == null:
return
_refresh_status()
if _state == "SKILL":
_refresh_skills()
elif _state == "EMOTICON":
_refresh_emotes()
elif _state == "QUEST":
_refresh_quests()
func _refresh_status() -> void:
var pd: Dictionary = client.get_points() if client.has_method("get_points") else {}
if pd.is_empty():
return
@@ -583,3 +696,551 @@ func _set_text(nm: String, value: String) -> void:
n.text = value
elif n.has_method("set_text"):
n.set_text(value)
func _wire_titlebars() -> void:
for tb_name: String in TITLEBAR.values():
var tb := _node(tb_name)
if tb:
for btn in tb.find_children("*", "BaseButton", true, false):
if not btn.pressed.is_connected(close):
btn.pressed.connect(close)
func _wire_skills() -> void:
var g1 := _node("Skill_Group_Button_1")
if g1 is BaseButton and not g1.pressed.is_connected(_on_group1_pressed):
g1.pressed.connect(_on_group1_pressed)
var g2 := _node("Skill_Group_Button_2")
if g2 is BaseButton and not g2.pressed.is_connected(_on_group2_pressed):
g2.pressed.connect(_on_group2_pressed)
func _on_group1_pressed() -> void:
if _skill_group() == 0:
_preview_group = 1
_refresh_skills()
if client and client.has_method("say"):
client.say(0, "/skillgroup 1")
func _on_group2_pressed() -> void:
if _skill_group() == 0:
_preview_group = 2
_refresh_skills()
if client and client.has_method("say"):
client.say(0, "/skillgroup 2")
func _wire_emoticons() -> void:
pass
func _wire_quests() -> void:
var sb := _node("Quest_ScrollBar")
if sb is Range:
if not sb.value_changed.is_connected(_on_quest_scroll_changed):
sb.value_changed.connect(_on_quest_scroll_changed)
func _on_quest_scroll_changed(val: float) -> void:
_quest_showing_start_index = int(val)
_refresh_quests()
func _refresh_skills() -> void:
if not is_open() or client == null:
return
var race := _main_race()
var group := _skill_group()
var job := race & 3
var pd: Dictionary = client.get_points() if client.has_method("get_points") else {}
var pts: Array = pd.get("points", [])
var stat_skill: int = int(pts[P_SKILL]) if pts.size() > P_SKILL else 0
_set_text("Active_Skill_Point_Value", str(max(0, stat_skill)))
_set_text("Support_Skill_Point_Value", "0")
var sk_pt_lbl := _node("Active_Skill_Point_Label")
if sk_pt_lbl:
sk_pt_lbl.visible = stat_skill > 0
var grp_name := _node("Active_Skill_Group_Name")
var g1_btn := _node("Skill_Group_Button_1")
var g2_btn := _node("Skill_Group_Button_2")
var g3_btn := _node("Skill_Group_Button_3")
if g3_btn:
g3_btn.visible = false
var display_group := group
if group == 0:
# 40250 uicharacter.py: 当未选流派时显示两组按钮,供玩家预览切换
if grp_name: grp_name.visible = false
if g1_btn:
g1_btn.visible = true
if "text" in g1_btn: g1_btn.text = _get_skill_group_name(job, 1)
if g1_btn is BaseButton: g1_btn.set_pressed_no_signal(_preview_group == 1)
if g2_btn:
g2_btn.visible = true
if "text" in g2_btn: g2_btn.text = _get_skill_group_name(job, 2)
if g2_btn is BaseButton: g2_btn.set_pressed_no_signal(_preview_group == 2)
display_group = _preview_group
else:
# 40250 uicharacter.py: 已选流派时隐藏切换按钮,只展示当前流派名
if g1_btn: g1_btn.visible = false
if g2_btn: g2_btn.visible = false
if grp_name:
grp_name.visible = true
grp_name.text = _get_skill_group_name(job, group)
var active_slot := _node("Skill_Active_Slot")
if active_slot:
for slot_idx in range(1, 9):
var sid := SkillSlotMap.skill_id_for_slot(race, display_group, slot_idx)
var cell := active_slot.get_node_or_null("slot_%d" % slot_idx)
if cell:
_populate_skill_cell(cell, sid, slot_idx)
var plus_cell := active_slot.get_node_or_null("slot_%d" % (20 + slot_idx))
if plus_cell:
var pts_alloc := stat_skill if group > 0 else 0
_populate_skill_plus(plus_cell, sid, pts_alloc)
var etc_slot := _node("Skill_ETC_Slot")
if etc_slot:
for slot_idx in range(101, 113):
var sid := SkillSlotMap.skill_id_for_slot(race, display_group, slot_idx)
var cell := etc_slot.get_node_or_null("slot_%d" % slot_idx)
if cell:
_populate_skill_cell(cell, sid, slot_idx)
func _populate_skill_cell(cell: Control, skill_id: int, slot_idx: int) -> void:
var btn: TextureButton = cell.get_node_or_null("Btn")
if btn == null:
btn = TextureButton.new()
btn.name = "Btn"
btn.set_anchors_preset(Control.PRESET_FULL_RECT)
btn.ignore_texture_size = true
btn.stretch_mode = TextureButton.STRETCH_SCALE
btn.mouse_filter = Control.MOUSE_FILTER_PASS
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
cell.add_child(btn)
var lvl_lbl: Label = cell.get_node_or_null("Lvl")
if lvl_lbl == null:
lvl_lbl = Label.new()
lvl_lbl.name = "Lvl"
lvl_lbl.position = Vector2(2, 16)
lvl_lbl.size = Vector2(28, 14)
lvl_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
lvl_lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
lvl_lbl.add_theme_font_size_override("font_size", 10)
lvl_lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE
cell.add_child(lvl_lbl)
for conn in btn.gui_input.get_connections():
btn.gui_input.disconnect(conn["callable"])
if skill_id <= 0:
btn.texture_normal = null
lvl_lbl.text = ""
cell.tooltip_text = ""
btn.set_drag_forwarding(Callable(), Callable(), Callable())
return
btn.texture_normal = _skill_icon(skill_id)
var sk_data := _get_player_skill(skill_id)
var lvl: int = int(sk_data.get("level", 0))
var master: int = int(sk_data.get("master", 0))
if master == 1:
lvl_lbl.text = "M%d" % lvl
lvl_lbl.modulate = Color(1.0, 0.4, 0.4)
elif master == 2:
lvl_lbl.text = "G%d" % lvl
lvl_lbl.modulate = Color(0.4, 0.7, 1.0)
elif master == 3:
lvl_lbl.text = "P"
lvl_lbl.modulate = Color(0.9, 0.3, 1.0)
elif lvl > 0:
lvl_lbl.text = str(lvl)
lvl_lbl.modulate = Color(1.0, 1.0, 1.0)
else:
lvl_lbl.text = ""
var sk_name := _skill_name(skill_id)
cell.tooltip_text = "%s (Lv %d)" % [sk_name, lvl] if lvl > 0 else sk_name
btn.gui_input.connect(_on_skill_btn_input.bind(skill_id, slot_idx))
btn.set_drag_forwarding(
func(_at_pos: Vector2):
if not _can_drag_skill(skill_id):
return null
drag_skill_id = skill_id
var preview := TextureRect.new()
preview.texture = btn.texture_normal
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
preview.custom_minimum_size = Vector2(32, 32)
preview.size = Vector2(32, 32)
preview.position = -Vector2(16, 16)
btn.set_drag_preview(preview)
return {
"type": "skill",
"kind": "skill",
"id": skill_id,
"skill_id": skill_id,
"slot": slot_idx,
},
Callable(),
Callable()
)
func _can_drag_skill(skill_id: int) -> bool:
if skill_id <= 0:
return false
if skill_table and skill_table.has_method("is_passive") and skill_table.is_passive(skill_id):
return false
return true
func _find_quickbar() -> Node:
var root: Node = null
if is_inside_tree():
var tree := get_tree()
if tree: root = tree.root
elif ui != null and is_instance_valid(ui) and ui.is_inside_tree():
var tree := ui.get_tree()
if tree: root = tree.root
elif get_parent():
root = get_parent()
if root:
for n in root.find_children("*", "Quickbar", true, false):
return n
for n in root.find_children("*", "Node", true, false):
if n.has_method("assign_mobile") and n.has_method("assign"):
return n
return null
func _find_item_mouse() -> Node:
if item_mouse != null and is_instance_valid(item_mouse):
return item_mouse
var root: Node = null
if is_inside_tree():
var tree := get_tree()
if tree: root = tree.root
elif ui != null and is_instance_valid(ui) and ui.is_inside_tree():
var tree := ui.get_tree()
if tree: root = tree.root
elif get_parent():
root = get_parent()
if root:
for n in root.find_children("*", "MouseController", true, false):
return n
for n in root.find_children("*", "Node", true, false):
if n.has_method("attach_skill") and n.has_method("attach_item"):
return n
return null
func _on_skill_btn_input(ev: InputEvent, skill_id: int, slot_idx: int) -> void:
if not (ev is InputEventMouseButton and ev.pressed):
return
if ev.button_index == MOUSE_BUTTON_RIGHT:
_cast_skill(skill_id)
elif ev.button_index == MOUSE_BUTTON_LEFT:
if not _can_drag_skill(skill_id):
return
drag_skill_id = skill_id
# 40250 uicharacter.py: Ctrl + Left Click adds to first empty quickslot
if ev.ctrl_pressed or Input.is_key_pressed(KEY_CTRL):
var qb := _find_quickbar()
if qb and qb.has_method("assign_mobile"):
if qb.assign_mobile("skill", skill_id):
return
# 40250 mouseController.AttachObject(self, player.SLOT_TYPE_SKILL, srcSlotIndex, selectedSkillIndex)
var im := item_mouse if item_mouse else _find_item_mouse()
if im and im.has_method("attach_skill"):
im.attach_skill(skill_id, slot_idx, _skill_icon(skill_id), _skill_name(skill_id))
func _populate_skill_plus(cell: Control, skill_id: int, stat_skill: int) -> void:
var plus_btn: Button = cell.get_node_or_null("PlusBtn")
if plus_btn == null:
plus_btn = Button.new()
plus_btn.name = "PlusBtn"
plus_btn.set_anchors_preset(Control.PRESET_FULL_RECT)
plus_btn.text = "+"
plus_btn.add_theme_font_size_override("font_size", 10)
plus_btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
cell.add_child(plus_btn)
var sk_data := _get_player_skill(skill_id)
var lvl: int = int(sk_data.get("level", 0))
var master: int = int(sk_data.get("master", 0))
var can_up := (skill_id > 0 and stat_skill > 0 and master == 0 and lvl < 20)
plus_btn.visible = can_up
for conn in plus_btn.pressed.get_connections():
plus_btn.pressed.disconnect(conn["callable"])
if can_up:
plus_btn.pressed.connect(func():
if client and client.has_method("say"):
client.say(0, "/skillup %d" % skill_id))
func _refresh_emotes() -> void:
var solo := _node("SoloEmotionSlot")
if solo:
for id in range(1, 19):
var cell := solo.get_node_or_null("slot_%d" % id)
if cell:
_populate_emote_cell(cell, id)
var dual := _node("DualEmotionSlot")
if dual:
for id in [51, 52, 53]:
var cell := dual.get_node_or_null("slot_%d" % id)
if cell:
_populate_emote_cell(cell, id)
func _populate_emote_cell(cell: Control, emote_id: int) -> void:
var btn: TextureButton = cell.get_node_or_null("Btn")
if btn == null:
btn = TextureButton.new()
btn.name = "Btn"
btn.set_anchors_preset(Control.PRESET_FULL_RECT)
btn.ignore_texture_size = true
btn.stretch_mode = TextureButton.STRETCH_SCALE
btn.mouse_filter = Control.MOUSE_FILTER_PASS
btn.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
cell.add_child(btn)
for conn in btn.gui_input.get_connections():
btn.gui_input.disconnect(conn["callable"])
for conn in btn.pressed.get_connections():
btn.pressed.disconnect(conn["callable"])
var rel: String = EMOTE_ICONS.get(emote_id, "")
var tex: Texture2D = UiAssets.load_tex(assets_root, rel) if rel != "" else null
if tex:
btn.texture_normal = tex
var nm: String = EMOTE_NAMES.get(emote_id, "动作 %d" % emote_id)
cell.tooltip_text = nm
btn.gui_input.connect(func(ev: InputEvent):
if not (ev is InputEventMouseButton and ev.pressed):
return
if ev.button_index == MOUSE_BUTTON_RIGHT:
if client and client.has_method("send_emoticon"):
client.send_emoticon(emote_id)
elif ev.button_index == MOUSE_BUTTON_LEFT:
if ev.ctrl_pressed or Input.is_key_pressed(KEY_CTRL):
var qb := _find_quickbar()
if qb and qb.has_method("assign_mobile"):
if qb.assign_mobile("emote", emote_id):
return
var im := item_mouse if item_mouse else _find_item_mouse()
if im and im.has_method("attach_emotion"):
im.attach_emotion(emote_id, tex, nm)
)
btn.set_drag_forwarding(
func(_at_pos: Vector2):
var preview := TextureRect.new()
preview.texture = tex
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
preview.custom_minimum_size = Vector2(32, 32)
preview.size = Vector2(32, 32)
preview.position = -Vector2(16, 16)
btn.set_drag_preview(preview)
return {
"type": "emotion",
"kind": "emote",
"id": emote_id,
"emote_id": emote_id,
},
Callable(),
Callable()
)
func _refresh_quests() -> void:
var quest_slot := _node("Quest_Slot")
if quest_slot == null or client == null:
return
var quests: Array = []
if client.has_method("get_quests"):
var raw_quests: Array = client.get_quests()
for q in raw_quests:
if String(q.get("title", q.get("name", ""))).strip_edges() != "":
quests.append(q)
var quest_count := quests.size()
var sb := _node("Quest_ScrollBar")
if sb is Range:
if quest_count > 5:
sb.visible = true
sb.max_value = maxi(0, quest_count - 5)
sb.value = _quest_showing_start_index
else:
sb.visible = false
_quest_showing_start_index = 0
for i in range(5):
var data_idx := _quest_showing_start_index + i
var nm_node := _node("Quest_Name_0%d" % i)
var time_node := _node("Quest_LastTime_0%d" % i)
var cnt_node := _node("Quest_LastCount_0%d" % i)
var cell := quest_slot.get_node_or_null("slot_%d" % i)
if cell == null and i < quest_slot.get_child_count():
cell = quest_slot.get_child(i)
if data_idx < quest_count:
var q: Dictionary = quests[data_idx]
var q_title := String(q.get("title", q.get("name", "任务 %d" % data_idx)))
var q_idx := int(q.get("index", data_idx))
var counter_name := String(q.get("counter_name", ""))
var counter_val := int(q.get("counter_value", 0))
var clock_name := String(q.get("clock_name", ""))
var clock_val := int(q.get("clock_value", 0))
if nm_node:
nm_node.visible = true
nm_node.text = q_title
nm_node.mouse_filter = Control.MOUSE_FILTER_PASS
nm_node.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
for conn in nm_node.gui_input.get_connections():
nm_node.gui_input.disconnect(conn["callable"])
nm_node.gui_input.connect(func(ev: InputEvent):
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
_select_quest(q_idx)
)
if cnt_node:
cnt_node.visible = true
if counter_name != "":
cnt_node.text = "%s : %d" % [counter_name, counter_val]
else:
cnt_node.text = ""
if time_node:
time_node.visible = true
if clock_name != "" and clock_val > 0:
var m := clock_val / 60
var s := clock_val % 60
time_node.text = "%s %02d:%02d" % [clock_name, m, s]
else:
time_node.text = ""
if cell:
cell.mouse_filter = Control.MOUSE_FILTER_PASS
cell.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
for conn in cell.gui_input.get_connections():
cell.gui_input.disconnect(conn["callable"])
cell.gui_input.connect(func(ev: InputEvent):
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
_select_quest(q_idx)
)
else:
if nm_node:
nm_node.visible = false
nm_node.text = ""
if cnt_node:
cnt_node.visible = false
cnt_node.text = ""
if time_node:
time_node.visible = false
time_node.text = ""
if cell:
for conn in cell.gui_input.get_connections():
cell.gui_input.disconnect(conn["callable"])
func _select_quest(quest_idx: int) -> void:
if client and client.has_method("script_button"):
# 40250 uicharacter.py:655 event.QuestButtonClick(-2147483648 + questIndex)
client.script_button(-2147483648 + quest_idx)
func _main_race() -> int:
if client and client.has_method("get_main_vid") and client.has_method("get_entity"):
var vid := int(client.get_main_vid())
if vid != 0:
return int(client.get_entity(vid).get("race", 0))
return 0
const SKILL_GROUP_NAMES := {
0: {1: "剑宗", 2: "气宗"},
1: {1: "锋刀", 2: "百羿"},
2: {1: "幻舞", 2: "黑魔"},
3: {1: "潜龙", 2: "狂雷"},
}
func _get_skill_group_name(job: int, grp: int) -> String:
var names: Dictionary = SKILL_GROUP_NAMES.get(job, {})
return String(names.get(grp, "技能组 %d" % grp))
func _skill_group() -> int:
var grp := int(client.get_skill_group()) if client and client.has_method("get_skill_group") else 0
if grp > 0:
return grp
return _infer_skill_group()
func _infer_skill_group() -> int:
if client == null or not client.has_method("get_skills"):
return 0
var race := _main_race()
for sk in client.get_skills():
var sid := int(sk.get("id", 0))
var lvl := int(sk.get("level", 0))
if lvl <= 0 and int(sk.get("master", 0)) == 0:
continue
for g in [1, 2]:
var slot := SkillSlotMap.skill_slot_for_id(race, g, sid)
if slot >= 1 and slot <= 8:
return g
return 0
func _skill_icon(skill_id: int) -> Texture2D:
if assets_root == "":
return null
var data: Dictionary = skill_table.entry(skill_id) if skill_table and skill_table.has_method("entry") else {}
var job := String(data.get("job", "")).to_upper()
var motion := String(data.get("motion", "")).strip_edges()
if motion == "":
return null
var candidates: Array[String] = []
const JOB_DIR := { "WARRIOR": "warrior", "ASSASSIN": "assassin", "SURA": "sura", "SHAMAN": "shaman" }
var grade := _skill_grade(skill_id)
var suffixes: Array[String] = ["_01", "_02", "_03"]
if grade == 1:
suffixes = ["_02", "_01", "_03"]
elif grade >= 2:
suffixes = ["_03", "_02", "_01"]
if JOB_DIR.has(job):
var dir := String(JOB_DIR[job])
for sfx in suffixes:
candidates.append("ETC/ymir work/ui/skill/%s/%s%s.sub" % [dir, motion, sfx])
elif job == "SUPPORT":
for sfx in suffixes:
candidates.append("ETC/ymir work/ui/skill/common/support/%s%s.sub" % [motion, sfx])
candidates.append("ETC/ymir work/ui/skill/common/support/%s.sub" % motion)
elif job == "HORSE":
candidates.append("ETC/ymir work/ui/skill/common/horse/%s.sub" % motion)
elif job == "GUILD":
candidates.append("ETC/ymir work/ui/skill/common/guild/%s.sub" % motion)
for rel in candidates:
var tex: Texture2D = UiAssets.load_tex(assets_root, rel)
if tex != null:
return tex
return null
func _skill_grade(skill_id: int) -> int:
if client and client.has_method("get_skills"):
for skill in client.get_skills():
if int(skill.get("id", 0)) == skill_id:
return clampi(int(skill.get("master", 0)), 0, 3)
return 0
func _get_player_skill(skill_id: int) -> Dictionary:
if client and client.has_method("get_skills"):
for skill in client.get_skills():
if int(skill.get("id", 0)) == skill_id:
return skill
return {}
func _skill_name(skill_id: int) -> String:
if skill_table and skill_table.has_method("name_of"):
var nm := String(skill_table.name_of(skill_id))
if nm != "":
return nm
return "技能 %d" % skill_id
func _cast_skill(skill_id: int) -> void:
var tree := get_tree()
if tree and tree.root:
for n in tree.root.find_children("*", "Node", true, false):
if n.has_method("activate_skill_direct"):
n.activate_skill_direct(skill_id)
return
if client and client.has_method("use_skill"):
client.use_skill(skill_id, 0)