# 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)