- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
170 lines
6.1 KiB
GDScript
170 lines
6.1 KiB
GDScript
# soulbound_lock_security_system.gd —— Metin2 40250 神装灵魂绑定与防丢安全锁系统 1:1
|
|
# 对照 40250 服务端 char_item.cpp, item.cpp, item_manager.cpp
|
|
class_name SoulboundLockSecuritySystem
|
|
extends RefCounted
|
|
|
|
signal item_soulbound(slot: int, item_name: String)
|
|
signal unbinding_started(slot: int, item_name: String, duration_seconds: float)
|
|
signal unbinding_cancelled(slot: int, item_name: String)
|
|
signal item_unbound(slot: int, item_name: String)
|
|
|
|
const VNUM_SOULBIND_SCROLL := 72051 # 灵魂绑定卷轴
|
|
const VNUM_SOULUNBIND_SCROLL := 72052 # 灵魂解绑卷轴
|
|
const DEFAULT_UNBIND_HOURS := 72.0 # 默认 72 小时解绑安全确认期
|
|
|
|
# 判定物品是否可在红名死亡时掉落 (灵魂绑定绝对免疫掉落)
|
|
static func can_drop_on_death(item: Dictionary) -> bool:
|
|
if item.is_empty():
|
|
return false
|
|
if item.get("is_soulbound", false):
|
|
return false
|
|
return true
|
|
|
|
# 判定物品是否允许丢弃至地面
|
|
static func can_drop_to_ground(item: Dictionary) -> bool:
|
|
if item.is_empty():
|
|
return false
|
|
if item.get("is_soulbound", false):
|
|
return false
|
|
return true
|
|
|
|
# 判定物品是否允许出售给商店
|
|
static func can_sell_to_shop(item: Dictionary) -> bool:
|
|
if item.is_empty():
|
|
return false
|
|
if item.get("is_soulbound", false):
|
|
return false
|
|
return true
|
|
|
|
# 判定物品是否允许放入摆摊货架或交易
|
|
static func can_trade_or_stall(item: Dictionary) -> bool:
|
|
if item.is_empty():
|
|
return false
|
|
if item.get("is_soulbound", false):
|
|
return false
|
|
return true
|
|
|
|
# 为装备施加灵魂绑定 (72051)
|
|
func bind_item(scroll_slot: int, target_slot: int, inventory: Array) -> Dictionary:
|
|
if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null:
|
|
return {"ok": false, "reason": "INVALID_SCROLL_SLOT"}
|
|
if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null:
|
|
return {"ok": false, "reason": "INVALID_TARGET_SLOT"}
|
|
|
|
var scroll = inventory[scroll_slot]
|
|
var target = inventory[target_slot]
|
|
|
|
if int(scroll.get("vnum", 0)) != VNUM_SOULBIND_SCROLL:
|
|
return {"ok": false, "reason": "NOT_A_BIND_SCROLL", "msg": "此物品不是【灵魂绑定卷轴】!"}
|
|
|
|
if target.get("is_soulbound", false):
|
|
return {"ok": false, "reason": "ALREADY_SOULBOUND", "msg": "该装备已处于灵魂绑定保护状态!"}
|
|
|
|
# 消耗 1 个绑定卷轴
|
|
var cnt = int(scroll.get("count", 1))
|
|
if cnt > 1:
|
|
scroll["count"] = cnt - 1
|
|
else:
|
|
inventory[scroll_slot] = null
|
|
|
|
target["is_soulbound"] = true
|
|
target["unbind_remaining_time"] = 0.0
|
|
|
|
var iname: String = target.get("name", "神装")
|
|
item_soulbound.emit(target_slot, iname)
|
|
|
|
return {
|
|
"ok": true,
|
|
"slot": target_slot,
|
|
"item_name": iname,
|
|
"msg": "【%s】已成功铭刻神龙灵魂烙印!永不掉落、永不遗失!" % iname
|
|
}
|
|
|
|
# 发起解绑倒计时 (72052)
|
|
func start_unbinding(scroll_slot: int, target_slot: int, inventory: Array, duration_hours: float = DEFAULT_UNBIND_HOURS) -> Dictionary:
|
|
if scroll_slot < 0 or scroll_slot >= inventory.size() or inventory[scroll_slot] == null:
|
|
return {"ok": false, "reason": "INVALID_SCROLL_SLOT"}
|
|
if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null:
|
|
return {"ok": false, "reason": "INVALID_TARGET_SLOT"}
|
|
|
|
var scroll = inventory[scroll_slot]
|
|
var target = inventory[target_slot]
|
|
|
|
if int(scroll.get("vnum", 0)) != VNUM_SOULUNBIND_SCROLL:
|
|
return {"ok": false, "reason": "NOT_AN_UNBIND_SCROLL", "msg": "此物品不是【灵魂解绑卷轴】!"}
|
|
|
|
if not target.get("is_soulbound", false):
|
|
return {"ok": false, "reason": "NOT_SOULBOUND", "msg": "该装备尚未绑定,无需解绑!"}
|
|
|
|
if float(target.get("unbind_remaining_time", 0.0)) > 0.0:
|
|
return {"ok": false, "reason": "UNBIND_ALREADY_IN_PROGRESS", "msg": "该装备已在解绑倒计时中!"}
|
|
|
|
# 消耗解绑卷轴
|
|
var cnt = int(scroll.get("count", 1))
|
|
if cnt > 1:
|
|
scroll["count"] = cnt - 1
|
|
else:
|
|
inventory[scroll_slot] = null
|
|
|
|
var duration_seconds = duration_hours * 3600.0
|
|
target["unbind_remaining_time"] = duration_seconds
|
|
|
|
var iname: String = target.get("name", "神装")
|
|
unbinding_started.emit(target_slot, iname, duration_seconds)
|
|
|
|
return {
|
|
"ok": true,
|
|
"slot": target_slot,
|
|
"item_name": iname,
|
|
"remaining_seconds": duration_seconds,
|
|
"msg": "【%s】进入解绑安全期,倒计时 %.1f 小时!在此期间随时可取消。" % [iname, duration_hours]
|
|
}
|
|
|
|
# 取消解绑 (防误操作与防盗号保护)
|
|
func cancel_unbinding(target_slot: int, inventory: Array) -> Dictionary:
|
|
if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null:
|
|
return {"ok": false, "reason": "INVALID_SLOT"}
|
|
|
|
var target = inventory[target_slot]
|
|
if float(target.get("unbind_remaining_time", 0.0)) <= 0.0:
|
|
return {"ok": false, "reason": "NO_UNBIND_IN_PROGRESS", "msg": "该装备并未处于解绑流程中。"}
|
|
|
|
target["unbind_remaining_time"] = 0.0
|
|
var iname: String = target.get("name", "神装")
|
|
unbinding_cancelled.emit(target_slot, iname)
|
|
|
|
return {
|
|
"ok": true,
|
|
"msg": "已终止【%s】的解绑流程,灵魂印记完好无损!" % iname
|
|
}
|
|
|
|
# 倒计时结束完成解绑
|
|
func finalize_unbinding(target_slot: int, inventory: Array) -> Dictionary:
|
|
if target_slot < 0 or target_slot >= inventory.size() or inventory[target_slot] == null:
|
|
return {"ok": false, "reason": "INVALID_SLOT"}
|
|
|
|
var target = inventory[target_slot]
|
|
if not target.get("is_soulbound", false):
|
|
return {"ok": false, "reason": "NOT_SOULBOUND"}
|
|
|
|
if float(target.get("unbind_remaining_time", 1.0)) > 0.0:
|
|
return {"ok": false, "reason": "COOLDOWN_NOT_FINISHED", "msg": "解绑安全冷却期尚未结束!"}
|
|
|
|
target["is_soulbound"] = false
|
|
target["unbind_remaining_time"] = 0.0
|
|
var iname: String = target.get("name", "神装")
|
|
item_unbound.emit(target_slot, iname)
|
|
|
|
return {
|
|
"ok": true,
|
|
"msg": "【%s】灵魂烙印消散,已成功解绑!" % iname
|
|
}
|
|
|
|
# 帧心跳更新解绑倒计时
|
|
func update(delta: float, inventory: Array) -> void:
|
|
for item in inventory:
|
|
if item != null and item.get("is_soulbound", false):
|
|
var rem = float(item.get("unbind_remaining_time", 0.0))
|
|
if rem > 0.0:
|
|
item["unbind_remaining_time"] = max(0.0, rem - delta)
|