- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
159 lines
4.6 KiB
GDScript
159 lines
4.6 KiB
GDScript
# pk_penalty_system.gd —— Metin2 40250 恶名红名掉落惩罚与露西之戒保护 1:1
|
|
# 对照 40250 服务端 char_battle.cpp, char.cpp, Item 70043, 70102, 70048
|
|
class_name PkPenaltySystem
|
|
extends RefCounted
|
|
|
|
signal lucy_ring_protected()
|
|
signal item_dropped_on_death(item: Dictionary)
|
|
signal alignment_restored(new_alignment: int)
|
|
|
|
const MIN_PENALTY_LEVEL := 50
|
|
const ITEM_LUCY_RING := 70043 # 露西之戒
|
|
const ITEM_ZEN_BEAN := 70102 # 禅豆 (+500 善恶值洗恶名)
|
|
const ITEM_CLOAK_OF_PEACE := 70048 # 和平披风 (隐匿红名)
|
|
|
|
# 负善恶值掉落几率表 (40250 char_battle.cpp 1:1)
|
|
static func get_death_drop_chance(alignment: int) -> float:
|
|
if alignment >= 0:
|
|
return 0.0
|
|
elif alignment >= -3999: # 凶恶
|
|
return 0.05
|
|
elif alignment >= -7999: # 歹毒
|
|
return 0.10
|
|
elif alignment >= -11999: # 恶贯满盈
|
|
return 0.15
|
|
else: # <= -12000 嗜血魔王
|
|
return 0.25
|
|
|
|
# 死亡掉落综合判定
|
|
func process_death_penalty(
|
|
player_level: int,
|
|
alignment: int,
|
|
equipment: Dictionary,
|
|
inventory: Array,
|
|
forced_roll: float = -1.0
|
|
) -> Dictionary:
|
|
# 1. 50 级以下或非红名(>=0) 免除一切死亡掉落
|
|
if player_level < MIN_PENALTY_LEVEL or alignment >= 0:
|
|
return {
|
|
"dropped": false,
|
|
"reason": "SAFE_LEVEL_OR_POSITIVE_KARMA",
|
|
"msg": "受新手保护或和平声望庇佑,免于死亡惩罚。"
|
|
}
|
|
|
|
# 2. 检查是否佩戴露西之戒 (70043)
|
|
var has_lucy_ring := false
|
|
var ring_slot_key = ""
|
|
for eq_key in equipment.keys():
|
|
var eq = equipment[eq_key]
|
|
if eq != null and int(eq.get("vnum", 0)) == ITEM_LUCY_RING:
|
|
has_lucy_ring = true
|
|
ring_slot_key = eq_key
|
|
break
|
|
|
|
if has_lucy_ring:
|
|
# 露西之戒替死碎裂,阻断掉落
|
|
equipment[ring_slot_key] = null
|
|
lucy_ring_protected.emit()
|
|
return {
|
|
"dropped": false,
|
|
"ring_shattered": true,
|
|
"msg": "【露西之戒】替你承受了死亡的厄运,戒指粉碎瓦解,身上物品完好无损!"
|
|
}
|
|
|
|
# 3. 计算掉落几率
|
|
var chance := get_death_drop_chance(alignment)
|
|
var roll := forced_roll if forced_roll >= 0.0 else randf()
|
|
|
|
if roll > chance:
|
|
return {
|
|
"dropped": false,
|
|
"reason": "LUCKY_ROLL",
|
|
"msg": "幸运未触发掉落惩罚。"
|
|
}
|
|
|
|
# 4. 寻找可掉落的物品 (已锁定的物品 is_locked 严禁掉落)
|
|
var droppable_candidates: Array = []
|
|
|
|
# 从未锁定装备中搜寻
|
|
for eq_key in equipment.keys():
|
|
var it = equipment[eq_key]
|
|
if it != null and not it.get("is_locked", false):
|
|
droppable_candidates.append({"type": "equip", "key": eq_key, "item": it})
|
|
|
|
# 从未锁定背包中搜寻
|
|
for i in range(inventory.size()):
|
|
var it = inventory[i]
|
|
if it != null and not it.get("is_locked", false):
|
|
droppable_candidates.append({"type": "inv", "slot": i, "item": it})
|
|
|
|
if droppable_candidates.is_empty():
|
|
return {
|
|
"dropped": false,
|
|
"reason": "ALL_ITEMS_PROTECTED",
|
|
"msg": "身上的所有贵重物品均处于安全加锁保护中,未发生掉落!"
|
|
}
|
|
|
|
# 掉落一件物品
|
|
var chosen = droppable_candidates[randi() % droppable_candidates.size()]
|
|
var dropped_item: Dictionary = chosen["item"].duplicate(true)
|
|
|
|
if chosen["type"] == "equip":
|
|
equipment[chosen["key"]] = null
|
|
else:
|
|
inventory[chosen["slot"]] = null
|
|
|
|
item_dropped_on_death.emit(dropped_item)
|
|
|
|
return {
|
|
"dropped": true,
|
|
"dropped_item": dropped_item,
|
|
"msg": "恶名昭彰!你被击杀,珍贵物品【%s】散落到了地面!" % dropped_item.get("name", "物品")
|
|
}
|
|
|
|
# 使用禅豆 (70102) 消除 500 点恶名
|
|
func use_zen_bean(
|
|
slot_idx: int,
|
|
inventory: Array,
|
|
current_alignment: int
|
|
) -> Dictionary:
|
|
if slot_idx < 0 or slot_idx >= inventory.size():
|
|
return {"ok": false, "reason": "INVALID_SLOT"}
|
|
|
|
var it = inventory[slot_idx]
|
|
if it == null or int(it.get("vnum", 0)) != ITEM_ZEN_BEAN:
|
|
return {"ok": false, "reason": "NOT_ZEN_BEAN", "msg": "这不是禅豆!"}
|
|
|
|
if current_alignment >= 0:
|
|
return {
|
|
"ok": false,
|
|
"reason": "NOT_EVIL",
|
|
"msg": "你的名声本就光明清白,无需服用禅豆洗去恶名。"
|
|
}
|
|
|
|
# 消耗禅豆
|
|
var cnt: int = int(it.get("count", 1))
|
|
if cnt <= 1:
|
|
inventory[slot_idx] = null
|
|
else:
|
|
it["count"] = cnt - 1
|
|
|
|
# 回复 500 点善恶值 (上限为 0)
|
|
var new_alignment: int = mini(0, current_alignment + 500)
|
|
alignment_restored.emit(new_alignment)
|
|
|
|
return {
|
|
"ok": true,
|
|
"old_alignment": current_alignment,
|
|
"new_alignment": new_alignment,
|
|
"restored": 500,
|
|
"msg": "服用了禅豆,心灵得到了净化,恶名值减少了 500 点!"
|
|
}
|
|
|
|
# 判定和平披风 (70048) 是否隐匿红名
|
|
func is_red_name_hidden(equipment: Dictionary) -> bool:
|
|
for eq in equipment.values():
|
|
if eq != null and int(eq.get("vnum", 0)) == ITEM_CLOAK_OF_PEACE:
|
|
return true
|
|
return false
|