- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
145 lines
5.0 KiB
GDScript
145 lines
5.0 KiB
GDScript
# attach_metin_dialog_system.gd
|
|
# 40250 官方 1:1 装备幽魂石/灵魂石镶嵌与双重属性对比预览系统
|
|
# 对照: uiattachmetin.py, attachstonedialog.py, char_item.cpp
|
|
class_name AttachMetinDialogSystem
|
|
extends RefCounted
|
|
|
|
const METIN_SOCKET_MAX_NUM: int = 3
|
|
|
|
const METIN_NORMAL: int = 0
|
|
const METIN_GOLD: int = 1
|
|
|
|
const METIN_SOCKET_TYPE_NONE: int = 0
|
|
const METIN_SOCKET_TYPE_SILVER: int = 1
|
|
const METIN_SOCKET_TYPE_GOLD: int = 2
|
|
|
|
const ITEM_BROKEN_METIN_VNUM: int = 28960
|
|
|
|
# 经典灵魂石属性对照表 (40250 canonical metin stones 28000~28443)
|
|
const METIN_STONES: Dictionary = {
|
|
28030: {"name": "战神之石+0", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 5},
|
|
28130: {"name": "战神之石+1", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 8},
|
|
28230: {"name": "战神之石+2", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 12},
|
|
28330: {"name": "战神之石+3", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 17},
|
|
28430: {"name": "战神之石+4", "bonus_type": "ATT_BONUS_TO_WARRIOR", "bonus_value": 25},
|
|
28434: {"name": "穿透之石+4", "bonus_type": "POINT_PENETRATE_PCT", "bonus_value": 8},
|
|
28435: {"name": "致命之石+4", "bonus_type": "POINT_CRITICAL_PCT", "bonus_value": 8},
|
|
28438: {"name": "防御之石+4", "bonus_type": "POINT_DEF_GRADE_BONUS", "bonus_value": 15},
|
|
28442: {"name": "加速之石+4", "bonus_type": "POINT_MOV_SPEED", "bonus_value": 30},
|
|
28960: {"name": "破碎的石头碎块", "bonus_type": "NONE", "bonus_value": 0}
|
|
}
|
|
|
|
var is_open: bool = false
|
|
var metin_pos: int = -1
|
|
var target_pos: int = -1
|
|
|
|
var metin_vnum: int = 0
|
|
var metin_subtype: int = METIN_NORMAL
|
|
|
|
var target_item_vnum: int = 0
|
|
var current_sockets: Array = [0, 0, 0]
|
|
var preview_sockets: Array = [0, 0, 0]
|
|
var target_socket_idx: int = -1
|
|
|
|
var last_packet_sent: Dictionary = {}
|
|
var last_sound_played: String = ""
|
|
|
|
signal dialog_opened(metin_pos: int, target_pos: int, preview_sockets: Array)
|
|
signal dialog_closed()
|
|
signal metin_attached(metin_pos: int, target_pos: int, target_slot_idx: int)
|
|
signal attach_failed(reason: String)
|
|
|
|
func can_attach_metin(slot_type: int, metin_type: int) -> bool:
|
|
if metin_type == METIN_NORMAL:
|
|
if slot_type == METIN_SOCKET_TYPE_SILVER or slot_type == METIN_SOCKET_TYPE_GOLD:
|
|
return true
|
|
elif metin_type == METIN_GOLD:
|
|
if slot_type == METIN_SOCKET_TYPE_GOLD:
|
|
return true
|
|
return false
|
|
|
|
func open_dialog(p_metin_pos: int, p_target_pos: int, p_metin_vnum: int, p_metin_subtype: int, p_target_vnum: int, p_sockets: Array) -> bool:
|
|
metin_pos = p_metin_pos
|
|
target_pos = p_target_pos
|
|
metin_vnum = p_metin_vnum
|
|
metin_subtype = p_metin_subtype
|
|
target_item_vnum = p_target_vnum
|
|
|
|
current_sockets = [0, 0, 0]
|
|
preview_sockets = [0, 0, 0]
|
|
target_socket_idx = -1
|
|
|
|
for i in range(mini(p_sockets.size(), METIN_SOCKET_MAX_NUM)):
|
|
current_sockets[i] = p_sockets[i]
|
|
preview_sockets[i] = p_sockets[i]
|
|
|
|
# 寻找首个符合条件的开放槽位
|
|
var found_slot: bool = false
|
|
for i in range(METIN_SOCKET_MAX_NUM):
|
|
var slot_state: int = current_sockets[i]
|
|
if can_attach_metin(slot_state, metin_subtype):
|
|
preview_sockets[i] = metin_vnum
|
|
target_socket_idx = i
|
|
found_slot = true
|
|
break
|
|
|
|
if not found_slot:
|
|
attach_failed.emit("NO_AVAILABLE_SOCKET")
|
|
return false
|
|
|
|
is_open = true
|
|
dialog_opened.emit(metin_pos, target_pos, preview_sockets)
|
|
return true
|
|
|
|
func accept() -> bool:
|
|
if not is_open:
|
|
return false
|
|
if target_socket_idx < 0:
|
|
attach_failed.emit("INVALID_SOCKET")
|
|
return false
|
|
|
|
# 40250 发送 net.SendItemUseToItemPacket(self.metinItemPos, self.targetItemPos)
|
|
last_packet_sent = {
|
|
"header": "HEADER_CG_ITEM_USE_TO_ITEM",
|
|
"metin_pos": metin_pos,
|
|
"target_pos": target_pos,
|
|
"target_socket_idx": target_socket_idx,
|
|
"metin_vnum": metin_vnum
|
|
}
|
|
|
|
# 官方原版音效
|
|
last_sound_played = "sound/ui/metinstone_insert.wav"
|
|
|
|
metin_attached.emit(metin_pos, target_pos, target_socket_idx)
|
|
close_dialog()
|
|
return true
|
|
|
|
func close_dialog() -> void:
|
|
is_open = false
|
|
metin_pos = -1
|
|
target_pos = -1
|
|
dialog_closed.emit()
|
|
|
|
# 模拟服务端结算镶嵌 (支持成功率判定: 成功直接嵌入,失败产生破碎石块 28960)
|
|
func simulate_server_process(target_sockets: Array, socket_idx: int, attached_metin_vnum: int, success: bool) -> Array:
|
|
var result_sockets: Array = target_sockets.duplicate()
|
|
if socket_idx >= 0 and socket_idx < result_sockets.size():
|
|
if success:
|
|
result_sockets[socket_idx] = attached_metin_vnum
|
|
else:
|
|
result_sockets[socket_idx] = ITEM_BROKEN_METIN_VNUM
|
|
return result_sockets
|
|
|
|
# 模拟卷轴清理破碎石头 (vnum 71083 / Scroll of Cleansing)
|
|
func clean_broken_socket(target_sockets: Array, socket_idx: int) -> Array:
|
|
var result_sockets: Array = target_sockets.duplicate()
|
|
if socket_idx >= 0 and socket_idx < result_sockets.size():
|
|
if result_sockets[socket_idx] == ITEM_BROKEN_METIN_VNUM:
|
|
result_sockets[socket_idx] = METIN_SOCKET_TYPE_SILVER
|
|
return result_sockets
|
|
|
|
func get_metin_info(vnum: int) -> Dictionary:
|
|
if METIN_STONES.has(vnum):
|
|
return METIN_STONES[vnum]
|
|
return {"name": "未知幽魂石", "bonus_type": "UNKNOWN", "bonus_value": 0}
|