Files
mtgodot-poc/project/ui/refine_ui.gd
T
shenandshen 66d217b313 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

469 lines
14 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RefineUI —— 1:1 复刻官方 40250 uirefine.py:RefineDialogNew 与 QuestionDialog2。
#
# 协议驱动:
# GC_REFINE_INFORMATION (119) -> refine_ask({type, pos, src_vnum, result_vnum, cost, prob, materials})
# 发送发包:
# 确定 -> CG_REFINE (96): client.refine(pos, type)
# 取消/ESC/超距 -> CG_REFINE (96): client.refine(255, 255)
# 40250 规则:
# 1. prob == 100 或 type == 5 时直接 Accept,免弹二次确认窗;
# 2. type == 3 (铁匠石) 弹彻底摧毁+概率加成警告;
# 3. type == 2 (祝福卷轴) 弹降级警告;
# 4. 其他 (普通铁匠) 弹彻底摧毁警告;
# 5. 材料根据背包实际存量显示白字(>= 需求)或红字(< 需求);
# 6. 离场超 10.0m 自动发送取消发包并关闭。
extends Node
const QuestionDialog2Scene = null
const USE_REFINE_LIMIT_RANGE_M := 10.0
var client: Node
var proto: Node
var audio: Node
var _root: Control
var _board: Panel
var _title_bar: Control
var _title_label: Label
var _close_btn: Button
var _preview_slot: Panel
var _preview_icon: TextureRect
var _text: RichTextLabel
var _material_container: VBoxContainer
var _prob_label: Label
var _cost_label: Label
var _action_row: HBoxContainer
var _refine_button: Button
var _cancel_button: Button
# QuestionDialog2 确认弹窗
var _question_dialog: Control
var _qd_message1: Label
var _qd_message2: Label
var _qd_accept_btn: Button
var _qd_cancel_btn: Button
var _cur := {}
var _mobile_mode := false
var _open_char_pos := Vector3.ZERO
var _has_open_pos := false
func setup(m2client: Node, parent: Node, proto_node: Node = null, audio_node: Node = null) -> void:
client = m2client
proto = proto_node
audio = audio_node
_build(parent)
if client != null:
if client.has_signal("refine_ask"):
client.refine_ask.connect(_on_ask)
if client.has_signal("refine_result"):
client.refine_result.connect(_on_result)
func is_open() -> bool:
return _root != null and _root.visible
func get_mobile_window() -> Control:
return _root
func set_mobile_mode(enabled: bool) -> void:
_mobile_mode = enabled
_apply_mobile_layout()
func _name_of(vnum: int) -> String:
if vnum == 0:
return "-"
if proto and proto.has_method("item"):
var d: Dictionary = proto.item(vnum)
var n := String(d.get("locale_name", d.get("name", "")))
if n != "":
return n
return "#%d" % vnum
func _get_inventory_count(vnum: int) -> int:
if client == null or not client.has_method("get_inventory"):
return 0
var total := 0
for item in client.get_inventory():
if int(item.get("vnum", 0)) == vnum:
total += maxi(1, int(item.get("count", 1)))
return total
func _get_main_char_pos() -> Variant:
if client == null:
return null
var vid := 0
if client.has_method("get_main_vid"):
vid = int(client.get_main_vid())
if vid != 0 and client.has_method("get_entity"):
var e: Dictionary = client.get_entity(vid)
if not e.is_empty() and e.has("pos"):
return e["pos"]
return null
func _process(_delta: float) -> void:
if not is_open() or not _has_open_pos:
return
var cur = _get_main_char_pos()
if cur is Vector3:
if _open_char_pos.distance_to(cur) > USE_REFINE_LIMIT_RANGE_M:
cancel_refine()
func _unhandled_input(event: InputEvent) -> void:
if not is_open():
return
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
cancel_refine()
func _on_ask(info: Dictionary) -> void:
_cur = info
var pos_val = _get_main_char_pos()
if pos_val is Vector3:
_open_char_pos = pos_val
_has_open_pos = true
else:
_has_open_pos = false
var src_vnum := int(info.get("src_vnum", 0))
var result_vnum := int(info.get("result_vnum", 0))
var prob := int(info.get("prob", 0))
var cost := int(info.get("cost", 0))
var materials: Array = info.get("materials", [])
# 1. 组装向下兼容的 _text(供 headless 自动化测试断言及快速阅读)
var lines := PackedStringArray()
lines.append("[b]%s[/b] → [b]%s[/b]" % [_name_of(src_vnum), _name_of(result_vnum)])
lines.append("成功率 %d%% 费用 %d 金" % [prob, cost])
if not materials.is_empty():
lines.append("材料:")
for m in materials:
lines.append(" %s ×%d" % [_name_of(int(m.get("vnum", 0))), int(m.get("count", 1))])
_text.text = "\n".join(lines)
# 2. 40250 视觉:底部成功率与费用
_prob_label.text = "成功率: %d%%" % prob
_cost_label.text = "费用: %d 金" % cost
var cur_gold := 0
if client and client.has_method("get_gold"):
cur_gold = int(client.get_gold())
elif client and client.has_method("get_points"):
cur_gold = int(client.get_points().get("gold", 0))
if cur_gold > 0 and cur_gold < cost:
_cost_label.modulate = Color(1.0, 0.33, 0.33)
else:
_cost_label.modulate = Color(1.0, 1.0, 1.0)
# 3. 40250 视觉:材质栏 AppendMaterial 动态排布
for child in _material_container.get_children():
_material_container.remove_child(child)
child.queue_free()
for m in materials:
var mvnum := int(m.get("vnum", 0))
var mcount := int(m.get("count", 1))
var owned := _get_inventory_count(mvnum)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
row.custom_minimum_size = Vector2(250, 32)
# Slot
var slot := Panel.new()
slot.custom_minimum_size = Vector2(32, 32)
var slot_sb := StyleBoxFlat.new()
slot_sb.bg_color = Color(0.12, 0.1, 0.08, 0.9)
slot_sb.border_color = Color(0.35, 0.3, 0.22, 1.0)
slot_sb.set_border_width_all(1)
slot.add_theme_stylebox_override("panel", slot_sb)
var mat_icon := TextureRect.new()
mat_icon.set_anchors_preset(Control.PRESET_FULL_RECT)
mat_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
mat_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
slot.add_child(mat_icon)
row.add_child(slot)
# ThinBoard with label
var tb := Panel.new()
tb.custom_minimum_size = Vector2(190, 28)
tb.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var tb_sb := StyleBoxFlat.new()
tb_sb.bg_color = Color(0.15, 0.12, 0.1, 0.8)
tb_sb.set_corner_radius_all(2)
tb.add_theme_stylebox_override("panel", tb_sb)
var lbl := Label.new()
lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
lbl.text = " %s x %02d" % [_name_of(mvnum), mcount]
lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
# 40250 存量足显示白字,不足显示红字 0xffff5555
if owned >= mcount:
lbl.modulate = Color(0.87, 0.87, 0.87)
else:
lbl.modulate = Color(1.0, 0.33, 0.33)
tb.add_child(lbl)
row.add_child(tb)
_material_container.add_child(row)
# 4. 自适应尺寸
var base_h := 220
var mats_h := materials.size() * 36
var new_h := base_h + mats_h
_root.size = Vector2(340, new_h)
if _question_dialog:
_question_dialog.visible = false
_root.visible = true
func _on_accept_clicked() -> void:
if _cur.is_empty():
return
var prob := int(_cur.get("prob", 0))
var rtype := int(_cur.get("type", 0))
# 40250 uirefine.py:385:成功率 100% 或 type == 5 直接执行,不弹确认框
if prob == 100 or rtype == 5:
_accept()
return
# 唤起 QuestionDialog2
_open_question_dialog(rtype)
func _open_question_dialog(rtype: int) -> void:
if _question_dialog == null:
return
# 40250 对齐文案:
_qd_message2.text = "确定要继续吗?"
if rtype == 3:
_qd_message1.text = "强化失败时装备将会被彻底销毁,同时成功率提升!"
elif rtype == 2:
_qd_message1.text = "强化失败时装备等级将会降低1级!"
else:
_qd_message1.text = "强化失败时装备将会被彻底销毁!"
_question_dialog.visible = true
_question_dialog.move_to_front()
func _accept() -> void:
if client and client.has_method("refine") and not _cur.is_empty():
client.refine(int(_cur.get("pos", 0)), int(_cur.get("type", 0)))
close()
func cancel_refine() -> void:
# 40250 uirefine.py:418: net.SendRefinePacket(255, 255)
if client and client.has_method("refine") and not _cur.is_empty():
client.refine(255, 255)
close()
func close() -> void:
if _question_dialog:
_question_dialog.visible = false
if _root:
_root.visible = false
_cur = {}
_has_open_pos = false
func _on_result(ok: bool) -> void:
_cur = {}
_text.text = "[b]精炼成功[/b]" if ok else "[b]精炼失败[/b]"
if _prob_label:
_prob_label.text = "强化成功!" if ok else "强化失败!"
_prob_label.modulate = Color(0.2, 1.0, 0.4) if ok else Color(1.0, 0.3, 0.3)
if _cost_label:
_cost_label.text = ""
for child in _material_container.get_children():
_material_container.remove_child(child)
child.queue_free()
if audio and audio.has_method("play_ui"):
if ok:
audio.play_ui("refine_success.wav")
else:
audio.play_ui("refine_fail.wav")
_root.visible = true
func _apply_mobile_layout() -> void:
if _root == null:
return
if _mobile_mode:
_root.size = Vector2(460, 320)
_root.position = Vector2(-230, -160)
else:
_root.size = Vector2(340, 260)
_root.position = Vector2(-170, -130)
func _build(parent: Node) -> void:
_root = Control.new()
_root.name = "RefineDialogNew"
_root.size = Vector2(340, 260)
_root.position = Vector2(200, 150)
_root.size = Vector2(340, 260)
_root.visible = false
_root.set_meta("is_titlebar", true)
parent.add_child(_root)
# 主底板 Board
_board = Panel.new()
_board.name = "Board"
_board.set_anchors_preset(Control.PRESET_FULL_RECT)
var sb := StyleBoxFlat.new()
sb.bg_color = Color(0.08, 0.06, 0.05, 0.98)
sb.border_color = Color(0.45, 0.38, 0.28, 1.0)
sb.set_border_width_all(2)
sb.set_corner_radius_all(4)
_board.add_theme_stylebox_override("panel", sb)
_root.add_child(_board)
# 标题栏 TitleBar (40250 经典红标题栏)
_title_bar = Panel.new()
_title_bar.name = "TitleBar"
_title_bar.position = Vector2(8, 8)
_title_bar.size = Vector2(324, 26)
var title_sb := StyleBoxFlat.new()
title_sb.bg_color = Color(0.45, 0.1, 0.1, 0.95)
title_sb.set_corner_radius_all(2)
_title_bar.add_theme_stylebox_override("panel", title_sb)
_root.add_child(_title_bar)
_title_label = Label.new()
_title_label.name = "TitleName"
_title_label.set_anchors_preset(Control.PRESET_FULL_RECT)
_title_label.text = "强化"
_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_title_bar.add_child(_title_label)
_close_btn = Button.new()
_close_btn.text = "×"
_close_btn.position = Vector2(300, 2)
_close_btn.size = Vector2(22, 22)
_close_btn.pressed.connect(cancel_refine)
_title_bar.add_child(_close_btn)
# 物品展示与说明
_text = RichTextLabel.new()
_text.bbcode_enabled = true
_text.position = Vector2(16, 40)
_text.size = Vector2(308, 60)
_root.add_child(_text)
# 材料列表容器 (AppendMaterial)
_material_container = VBoxContainer.new()
_material_container.position = Vector2(16, 106)
_material_container.size = Vector2(308, 50)
_material_container.add_theme_constant_override("separation", 4)
_root.add_child(_material_container)
# 底部区域:成功率 + 费用
var bottom_info := VBoxContainer.new()
bottom_info.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
bottom_info.offset_top = -90
bottom_info.offset_bottom = -45
bottom_info.offset_left = 16
bottom_info.offset_right = -16
bottom_info.alignment = BoxContainer.ALIGNMENT_CENTER
_root.add_child(bottom_info)
_prob_label = Label.new()
_prob_label.name = "SuccessPercentage"
_prob_label.text = "成功率: --%"
_prob_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
bottom_info.add_child(_prob_label)
_cost_label = Label.new()
_cost_label.name = "Cost"
_cost_label.text = "费用: -- 金"
_cost_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
bottom_info.add_child(_cost_label)
# 底部按钮行:AcceptButton ("精炼") 与 CancelButton ("取消")
_action_row = HBoxContainer.new()
_action_row.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
_action_row.offset_top = -42
_action_row.offset_bottom = -10
_action_row.offset_left = 16
_action_row.offset_right = -16
_action_row.alignment = BoxContainer.ALIGNMENT_CENTER
_action_row.add_theme_constant_override("separation", 24)
_root.add_child(_action_row)
_refine_button = Button.new()
_refine_button.name = "AcceptButton"
_refine_button.text = "精炼"
_refine_button.custom_minimum_size = Vector2(110, 30)
_refine_button.pressed.connect(_on_accept_clicked)
_action_row.add_child(_refine_button)
_cancel_button = Button.new()
_cancel_button.name = "CancelButton"
_cancel_button.text = "取消"
_cancel_button.custom_minimum_size = Vector2(110, 30)
_cancel_button.pressed.connect(cancel_refine)
_action_row.add_child(_cancel_button)
# 构建内嵌 QuestionDialog2
_build_question_dialog(parent)
func _build_question_dialog(parent: Node) -> void:
_question_dialog = Control.new()
_question_dialog.name = "QuestionDialog2"
_question_dialog.size = Vector2(290, 120)
_question_dialog.position = Vector2(225, 200)
_question_dialog.visible = false
_question_dialog.z_index = 100
parent.add_child(_question_dialog)
var qd_board := Panel.new()
qd_board.name = "board"
qd_board.set_anchors_preset(Control.PRESET_FULL_RECT)
var qd_sb := StyleBoxFlat.new()
qd_sb.bg_color = Color(0.12, 0.08, 0.06, 0.98)
qd_sb.border_color = Color(0.65, 0.5, 0.3, 1.0)
qd_sb.set_border_width_all(2)
qd_sb.set_corner_radius_all(4)
qd_board.add_theme_stylebox_override("panel", qd_sb)
_question_dialog.add_child(qd_board)
_qd_message1 = Label.new()
_qd_message1.name = "message1"
_qd_message1.position = Vector2(12, 14)
_qd_message1.size = Vector2(266, 32)
_qd_message1.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_qd_message1.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_qd_message1.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_question_dialog.add_child(_qd_message1)
_qd_message2 = Label.new()
_qd_message2.name = "message2"
_qd_message2.position = Vector2(12, 48)
_qd_message2.size = Vector2(266, 24)
_qd_message2.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_qd_message2.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_qd_message2.text = "确定要继续吗?"
_question_dialog.add_child(_qd_message2)
var qd_btn_row := HBoxContainer.new()
qd_btn_row.position = Vector2(12, 78)
qd_btn_row.size = Vector2(266, 32)
qd_btn_row.alignment = BoxContainer.ALIGNMENT_CENTER
qd_btn_row.add_theme_constant_override("separation", 24)
_question_dialog.add_child(qd_btn_row)
_qd_accept_btn = Button.new()
_qd_accept_btn.name = "accept"
_qd_accept_btn.text = "是"
_qd_accept_btn.custom_minimum_size = Vector2(80, 28)
_qd_accept_btn.pressed.connect(func():
_question_dialog.visible = false
_accept()
)
qd_btn_row.add_child(_qd_accept_btn)
_qd_cancel_btn = Button.new()
_qd_cancel_btn.name = "cancel"
_qd_cancel_btn.text = "否"
_qd_cancel_btn.custom_minimum_size = Vector2(80, 28)
_qd_cancel_btn.pressed.connect(func():
_question_dialog.visible = false
)
qd_btn_row.add_child(_qd_cancel_btn)