- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
148 lines
4.4 KiB
GDScript
148 lines
4.4 KiB
GDScript
# chat_hyperlink_dice_system.gd —— 40250 聊天物品超链接与 /dice 掷骰系统 1:1
|
|
# 严格对照:
|
|
# ClientVS22/source/EterLib/TextTag.cpp:32 (Hyperlink |Hitem:vnum:flags:socket:attr|h[%s]|h)
|
|
# Client/Eternexus/root/uichat.py & uitooltip.py (ItemHyperlink, SetHyperlinkItem)
|
|
# metin2/src/server/game/src/cmd_general.cpp:2392 (do_dice)
|
|
extends RefCounted
|
|
|
|
const LINK_COLOR := "ffffc700" # 官方物品链接专用亮金黄色
|
|
|
|
# 1. 将物品数据编码为 40250 官方超链接格式
|
|
# 格式: |cffffc700|Hitem:vnum:flags:s0:s1:s2:a0_t:a0_v:a1_t:a1_v:...|h[物品名]|h|r
|
|
static func encode_item_hyperlink(item_data: Dictionary) -> String:
|
|
var vnum: int = item_data.get("vnum", 0)
|
|
var name: String = item_data.get("name", "Unknown Item")
|
|
var flags: int = item_data.get("flags", 0)
|
|
|
|
var sockets: Array = item_data.get("sockets", [0, 0, 0])
|
|
while sockets.size() < 3:
|
|
sockets.append(0)
|
|
|
|
var attrs: Array = item_data.get("attributes", [])
|
|
var attr_parts: Array = []
|
|
for a in attrs:
|
|
attr_parts.append("%d:%d" % [a.get("type", 0), a.get("value", 0)])
|
|
|
|
var payload_parts := [
|
|
str(vnum),
|
|
str(flags),
|
|
str(sockets[0]),
|
|
str(sockets[1]),
|
|
str(sockets[2])
|
|
]
|
|
if not attr_parts.is_empty():
|
|
payload_parts.append((":").join(attr_parts))
|
|
|
|
var payload := (":").join(payload_parts)
|
|
return "|c%s|Hitem:%s|h[%s]|h|r" % [LINK_COLOR, payload, name]
|
|
|
|
# 2. 从超链接中解析提取物品完整数据结构 (1:1 TextTag::FindHyperlink)
|
|
static func decode_item_hyperlink(hyperlink_text: String) -> Dictionary:
|
|
var res := {
|
|
"valid": false,
|
|
"vnum": 0,
|
|
"name": "",
|
|
"flags": 0,
|
|
"sockets": [0, 0, 0],
|
|
"attributes": []
|
|
}
|
|
|
|
# 正则或字符串匹配提取 |Hitem:(.+?)\|h\[(.*?)\]\|h
|
|
var h_start := hyperlink_text.find("|Hitem:")
|
|
if h_start == -1:
|
|
return res
|
|
|
|
var payload_start := h_start + 7 # len("|Hitem:")
|
|
var payload_end := hyperlink_text.find("|h[", payload_start)
|
|
if payload_end == -1:
|
|
return res
|
|
|
|
var payload := hyperlink_text.substr(payload_start, payload_end - payload_start)
|
|
|
|
var name_start := payload_end + 3 # len("|h[")
|
|
var name_end := hyperlink_text.find("]|h", name_start)
|
|
if name_end == -1:
|
|
return res
|
|
|
|
var display_name := hyperlink_text.substr(name_start, name_end - name_start)
|
|
res["name"] = display_name
|
|
|
|
var tokens := payload.split(":")
|
|
if tokens.size() < 5:
|
|
return res
|
|
|
|
res["vnum"] = tokens[0].to_int()
|
|
res["flags"] = tokens[1].to_int()
|
|
res["sockets"] = [tokens[2].to_int(), tokens[3].to_int(), tokens[4].to_int()]
|
|
|
|
# 解析属性对 (type:value)
|
|
var attrs: Array = []
|
|
var idx := 5
|
|
while idx + 1 < tokens.size():
|
|
var at_type := tokens[idx].to_int()
|
|
var at_val := tokens[idx + 1].to_int()
|
|
attrs.append({ "type": at_type, "value": at_val })
|
|
idx += 2
|
|
|
|
res["attributes"] = attrs
|
|
res["valid"] = true
|
|
return res
|
|
|
|
# 3. 模拟 Tooltip 生成(悬停时渲染内容)
|
|
static func build_tooltip_text(item_dict: Dictionary) -> String:
|
|
if not item_dict.get("valid", false):
|
|
return "无效的物品链接"
|
|
|
|
var lines: Array = []
|
|
lines.append("=== %s (Vnum: %d) ===" % [item_dict["name"], item_dict["vnum"]])
|
|
|
|
# 宝石孔位信息
|
|
var sockets: Array = item_dict.get("sockets", [])
|
|
for i in range(sockets.size()):
|
|
var s: int = sockets[i]
|
|
if s > 0:
|
|
lines.append("孔位 %d: 镶嵌宝石 [Vnum %d]" % [i + 1, s])
|
|
elif s == 1: # 官方孔位开孔标记
|
|
lines.append("孔位 %d: 空孔" % [i + 1])
|
|
|
|
# 附加属性信息 (1~7 条)
|
|
var attrs: Array = item_dict.get("attributes", [])
|
|
for a in attrs:
|
|
lines.append("附加属性: 类型 %d +%d" % [a["type"], a["value"]])
|
|
|
|
return ("\n").join(lines)
|
|
|
|
# 4. 40250 官方 /dice 掷骰系统 (1:1 ACMD(do_dice))
|
|
static func execute_dice(player_name: String, args: Array = [], roll_override: int = -1) -> Dictionary:
|
|
var start_val := 1
|
|
var end_val := 100
|
|
|
|
if args.size() >= 2:
|
|
start_val = int(args[0])
|
|
end_val = int(args[1])
|
|
elif args.size() == 1:
|
|
start_val = 1
|
|
end_val = int(args[0])
|
|
|
|
# 40250 规则: end = MAX(start, end); start = MIN(start, end);
|
|
var real_start := mini(start_val, end_val)
|
|
var real_end := maxi(start_val, end_val)
|
|
|
|
var roll_result: int = 0
|
|
if roll_override >= 0:
|
|
roll_result = clamp(roll_override, real_start, real_end)
|
|
else:
|
|
roll_result = randi() % (real_end - real_start + 1) + real_start
|
|
|
|
var broadcast_msg := "%s 投掷了骰子,点数为 %d (%d-%d)" % [
|
|
player_name, roll_result, real_start, real_end
|
|
]
|
|
|
|
return {
|
|
"player": player_name,
|
|
"roll": roll_result,
|
|
"start": real_start,
|
|
"end": real_end,
|
|
"message": broadcast_msg
|
|
}
|