Files
mtgodot-poc/project/alignment_pk_system.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

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))