- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
188 lines
6.4 KiB
GDScript
188 lines
6.4 KiB
GDScript
# alignment_pk_system.gd —— Metin2 40250 善恶值与名望头衔奖惩系统 1:1
|
|
# 对照 40250 服务端 char.cpp:1190-1205, char_battle.cpp:UpdateAlignment, root/localeinfo.py:GetAlignmentTitleName
|
|
class_name AlignmentPkSystem
|
|
extends RefCounted
|
|
|
|
signal alignment_changed(old_val: int, new_val: int, title: String)
|
|
signal title_changed(old_title: String, new_title: String)
|
|
signal items_dropped_on_death(dropped_items: Array)
|
|
signal zen_bean_consumed(cleared_amount: int, current_alignment: int)
|
|
|
|
# 40250 官方善恶值极值 (内部数值 -200000 ~ +200000, 显示数值 -20000 ~ +20000)
|
|
const MIN_REAL_ALIGNMENT := -200000
|
|
const MAX_REAL_ALIGNMENT := 200000
|
|
|
|
const VNUM_ZEN_BEAN := 70102 # 禅豆 (消减罪孽值)
|
|
const ZEN_BEAN_CLEAR_POINTS := 500 # 每次消除 500 显示点数 (5000 内部点数)
|
|
|
|
# 9 大官方经典头衔段位表 (40250 localeinfo.py / locale_game.txt)
|
|
const TITLES: Array = [
|
|
{"threshold": 12000, "name": "崇高骑士", "en_name": "Chivalric", "is_evil": false},
|
|
{"threshold": 8000, "name": "尊贵贵族", "en_name": "Noble", "is_evil": false},
|
|
{"threshold": 4000, "name": "正义良民", "en_name": "Good", "is_evil": false},
|
|
{"threshold": 1000, "name": "友好伙伴", "en_name": "Friendly", "is_evil": false},
|
|
{"threshold": 0, "name": "中立行者", "en_name": "Neutral", "is_evil": false},
|
|
{"threshold": -3999, "name": "好斗狂徒", "en_name": "Aggressive","is_evil": true},
|
|
{"threshold": -7999, "name": "狡诈恶徒", "en_name": "Fraudulent","is_evil": true},
|
|
{"threshold": -11999,"name": "险恶凶手", "en_name": "Malicious", "is_evil": true},
|
|
{"threshold": -20000,"name": "残忍魔头", "en_name": "Cruel", "is_evil": true}
|
|
]
|
|
|
|
var real_alignment: int = 0 # 内部实际值 (-200000 ~ +200000)
|
|
|
|
# 获取玩家当前对外显示的善恶值 (40250: sAlignment = m_iAlignment / 10)
|
|
func get_alignment() -> int:
|
|
return int(real_alignment / 10)
|
|
|
|
# 根据善恶值获取官方 1:1 头衔名称 (40250 localeinfo.py:GetAlignmentTitleName)
|
|
static func get_title_name(alignment: int) -> String:
|
|
if alignment >= 12000:
|
|
return "崇高骑士"
|
|
elif alignment >= 8000:
|
|
return "尊贵贵族"
|
|
elif alignment >= 4000:
|
|
return "正义良民"
|
|
elif alignment >= 1000:
|
|
return "友好伙伴"
|
|
elif alignment >= 0:
|
|
return "中立行者"
|
|
elif alignment > -4000:
|
|
return "好斗狂徒"
|
|
elif alignment > -8000:
|
|
return "狡诈恶徒"
|
|
elif alignment > -12000:
|
|
return "险恶凶手"
|
|
else:
|
|
return "残忍魔头"
|
|
|
|
# 检查当前是否为红名/恶名
|
|
func is_evil() -> bool:
|
|
return get_alignment() < 0
|
|
|
|
# 更新善恶值 (40250 char_battle.cpp:UpdateAlignment)
|
|
# amount 为内部值加减 (如传入 100 表示内部 +100)
|
|
func update_real_alignment(amount: int) -> void:
|
|
var old_display = get_alignment()
|
|
var old_title = get_title_name(old_display)
|
|
|
|
real_alignment = clampi(real_alignment + amount, MIN_REAL_ALIGNMENT, MAX_REAL_ALIGNMENT)
|
|
|
|
var new_display = get_alignment()
|
|
var new_title = get_title_name(new_display)
|
|
|
|
if old_display != new_display:
|
|
alignment_changed.emit(old_display, new_display, new_title)
|
|
|
|
if old_title != new_title:
|
|
title_changed.emit(old_title, new_title)
|
|
|
|
# 按显示点数直接调整善恶值 (如传入 +500 相当于内部 +5000)
|
|
func update_alignment_points(points: int) -> void:
|
|
update_real_alignment(points * 10)
|
|
|
|
# 在线时间自然恢复善恶值 (40250 char.cpp:1194-1203)
|
|
# minutes: 挂机分钟数
|
|
func recover_alignment_by_time(minutes: int, has_fast_recovery_item: bool = false) -> void:
|
|
if minutes <= 0:
|
|
return
|
|
|
|
if real_alignment < 0:
|
|
var rate_per_min = 120 if has_fast_recovery_item else 60
|
|
update_alignment_points(rate_per_min * minutes)
|
|
else:
|
|
var rate_per_min = 5
|
|
update_alignment_points(rate_per_min * minutes)
|
|
|
|
# 使用禅豆消减罪孽 (40250 Zen Bean 70102)
|
|
func use_zen_bean(inventory: Array, slot_index: int) -> Dictionary:
|
|
if slot_index < 0 or slot_index >= inventory.size() or inventory[slot_index] == null:
|
|
return {"ok": false, "reason": "INVALID_SLOT"}
|
|
|
|
var item: Dictionary = inventory[slot_index]
|
|
if int(item.get("vnum", 0)) != VNUM_ZEN_BEAN:
|
|
return {"ok": false, "reason": "NOT_A_ZEN_BEAN", "msg": "此物品不是禅豆!"}
|
|
|
|
var cur_align := get_alignment()
|
|
if cur_align >= 0:
|
|
return {"ok": false, "reason": "ALIGNMENT_ALREADY_GOOD", "msg": "当前心境平和,无需使用禅豆消业!"}
|
|
|
|
# 消耗 1 颗禅豆
|
|
var cnt := int(item.get("count", 1))
|
|
if cnt > 1:
|
|
item["count"] = cnt - 1
|
|
else:
|
|
inventory[slot_index] = null
|
|
|
|
# 恢复 500 点善恶值 (若剩余负值不足 500 则刚好回满到 0)
|
|
var clear_amount: int = mini(ZEN_BEAN_CLEAR_POINTS, -cur_align)
|
|
update_alignment_points(clear_amount)
|
|
|
|
zen_bean_consumed.emit(clear_amount, get_alignment())
|
|
|
|
return {
|
|
"ok": true,
|
|
"cleared": clear_amount,
|
|
"current_alignment": get_alignment(),
|
|
"title": get_title_name(get_alignment()),
|
|
"msg": "服用禅豆,消除 %d 点罪业!当前善恶值:%d (%s)" % [clear_amount, get_alignment(), get_title_name(get_alignment())]
|
|
}
|
|
|
|
# 击杀和平角色 (恶名惩罚)
|
|
func record_pk_kill() -> void:
|
|
update_alignment_points(-2000)
|
|
|
|
# 击破魔石或野外怪物 (正向善名积累)
|
|
func record_monster_kill(is_metin: bool = false) -> void:
|
|
var bonus = 50 if is_metin else 10
|
|
update_alignment_points(bonus)
|
|
|
|
# 死亡惩罚计算 (40250 红名野外死亡爆装机制)
|
|
func handle_death_drop_penalty(inventory: Array) -> Array:
|
|
var cur_align := get_alignment()
|
|
if cur_align >= 0:
|
|
return [] # 白名/良民免死掉落
|
|
|
|
var drop_chance := 0.10
|
|
var max_drop_count := 1
|
|
|
|
if cur_align <= -12000: # 残忍魔头
|
|
drop_chance = 0.80
|
|
max_drop_count = 3
|
|
elif cur_align <= -8000: # 险恶凶手
|
|
drop_chance = 0.50
|
|
max_drop_count = 2
|
|
elif cur_align <= -4000: # 狡诈恶徒
|
|
drop_chance = 0.25
|
|
max_drop_count = 1
|
|
|
|
var dropped_items: Array = []
|
|
for i in range(inventory.size()):
|
|
if inventory[i] == null:
|
|
continue
|
|
var item: Dictionary = inventory[i]
|
|
# 灵魂绑定锁定的装备免于死亡掉落
|
|
if item.get("is_soulbound", false):
|
|
continue
|
|
|
|
# 判定概率爆出
|
|
if randf() <= drop_chance:
|
|
dropped_items.append(item.duplicate(true))
|
|
inventory[i] = null
|
|
if dropped_items.size() >= max_drop_count:
|
|
break
|
|
|
|
if not dropped_items.is_empty():
|
|
items_dropped_on_death.emit(dropped_items)
|
|
|
|
return dropped_items
|
|
|
|
# 序列化
|
|
func serialize() -> Dictionary:
|
|
return {
|
|
"real_alignment": real_alignment
|
|
}
|
|
|
|
# 反序列化
|
|
func deserialize(data: Dictionary) -> void:
|
|
real_alignment = int(data.get("real_alignment", 0))
|