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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+213
View File
@@ -0,0 +1,213 @@
# skill_book_reading_system.gd —— 40250 官方大师技能书悟道研读与突破系统 1:1
# 严格对照:
# metin2/src/server/game/src/char_skill.cpp:416-560 (CHARACTER::LearnSkillByBook)
# metin2/src/server/game/src/char_item.cpp:2128-2160 (ITEM_SKILLBOOK / 50300)
# metin2/src/server/game/src/affect.h (AFFECT_SKILL_BOOK_BONUS 512, AFFECT_SKILL_NO_BOOK_DELAY 513)
# metin2/src/server/share/conf/item_proto.txt (71001 驱魔卷轴, 71094 隐士之药)
extends RefCounted
const ITEM_SKILLBOOK_VNUM := 50300
const EXORCISM_SCROLL_VNUM := 71001 # 驱魔卷轴: AFFECT_SKILL_NO_BOOK_DELAY
const HERMIT_ADVICE_VNUM := 71094 # 隐士之药: AFFECT_SKILL_BOOK_BONUS
const EXP_COST := 20000 # 40250 char_skill.cpp:430 need_exp = 20000
const COOLDOWN_DURATION := 86400 # 24小时冷却 (LC_IsKorea 86400 秒)
const BASE_SUCCESS_RATE := 35 # 基础成功率 35% (char_skill.cpp:524: percent = 65 -> 100 - 65 = 35%)
# 技能进阶档次 (MasterType)
const SKILL_NORMAL := 0 # Lv 1 - 19
const SKILL_MASTER := 1 # M1 (Lv 20) ~ M10 (Lv 29)
const SKILL_GRAND_MASTER := 2 # G1 (Lv 30) ~ G10 (Lv 39)
const SKILL_PERFECT_MASTER := 3 # P (Lv 40)
# 角色技能书研读状态
var skills: Dictionary = {} # skill_id -> {"level": int, "read_count": int, "next_read_time": int}
var affects: Dictionary = {} # affect_type -> bool
var player_exp: int = 100000
var current_time: int = 1000000
# 累计统计
var total_books_read: int = 0
var total_books_succeeded: int = 0
var total_books_failed: int = 0
func _init(initial_exp: int = 100000, initial_time: int = 1000000) -> void:
player_exp = initial_exp
current_time = initial_time
skills = {}
affects = {}
total_books_read = 0
total_books_succeeded = 0
total_books_failed = 0
# 注册/初始化技能
func init_skill(skill_id: int, level: int = 20, read_count: int = 0, next_read_time: int = 0) -> void:
skills[skill_id] = {
"level": level,
"read_count": read_count,
"next_read_time": next_read_time
}
# 获取技能档次
func get_skill_master_type(skill_id: int) -> int:
if not skills.has(skill_id):
return SKILL_NORMAL
var lvl: int = skills[skill_id]["level"]
if lvl < 20:
return SKILL_NORMAL
elif lvl < 30:
return SKILL_MASTER
elif lvl < 40:
return SKILL_GRAND_MASTER
else:
return SKILL_PERFECT_MASTER
# 计算升下一级所需的成功书本数 (char_skill.cpp:509 need_bookcount = level - 20)
# M1(Lv20) 升 M2: 需 1 本 (20 - 20 + 1 = 1)
# M2(Lv21) 升 M3: 需 2 本
# ...
# M10(Lv29) 升 G1: 需 10 本
# 累计突破至 G 需 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55 本成功!
func get_need_books_for_next_level(skill_id: int) -> int:
if not skills.has(skill_id):
return 0
var lvl: int = skills[skill_id]["level"]
if lvl < 20 or lvl >= 30:
return 0
return lvl - 20 + 1
# 使用驱魔卷轴 (71001)
func use_exorcism_scroll() -> bool:
affects[EXORCISM_SCROLL_VNUM] = true
return true
# 使用隐士之药 (71094)
func use_hermit_advice() -> bool:
affects[HERMIT_ADVICE_VNUM] = true
return true
# 检查是否存在状态
func has_affect(affect_vnum: int) -> bool:
return affects.get(affect_vnum, false)
# 研读技能书核心方法 (1:1 CHARACTER::LearnSkillByBook)
# roll_override: 传入 1~100 用于单元测试确定性判定,-1 表示根据随机数判定
func learn_skill_by_book(skill_id: int, book_socket_skill_vnum: int = -1, roll_override: int = -1) -> Dictionary:
var result := {
"success": false,
"level_up": false,
"breakthrough_g": false,
"error_code": "OK",
"read_count": 0,
"needed_books": 0,
"remaining_books": 0,
"current_level": 0,
"exp_deducted": 0,
"consumed_exorcism": false,
"consumed_hermit": false,
"message": ""
}
# 1. 校验技能是否存在
if not skills.has(skill_id):
result["error_code"] = "NOT_LEARNABLE"
result["message"] = "无法研读未掌握或非本流派的技能。"
return result
# 2. 校验技能书 Socket 绑定的技能编号 (40250 char_item.cpp:2132)
if book_socket_skill_vnum != -1 and book_socket_skill_vnum != skill_id:
result["error_code"] = "SKILL_MISMATCH"
result["message"] = "该技能书并非此技能专研典籍。"
return result
var skill_data: Dictionary = skills[skill_id]
var current_lvl: int = skill_data["level"]
result["current_level"] = current_lvl
# 3. 校验阶档
var master_type: int = get_skill_master_type(skill_id)
if master_type < SKILL_MASTER:
result["error_code"] = "NOT_MASTER_TIER"
result["message"] = "此技能尚未达到大师 (M) 阶档,无法研读书籍。"
return result
elif master_type >= SKILL_GRAND_MASTER:
result["error_code"] = "ALREADY_G_OR_P"
result["message"] = "此技能已达到宗师 (G) 阶或更高,书籍无法再提升造诣。"
return result
# 4. 经验值检查与消耗 (char_skill.cpp:430: need_exp = 20000)
if player_exp < EXP_COST:
result["error_code"] = "INSUFFICIENT_EXP"
result["message"] = "角色经验值不足 20,000,精神无法专注研读。"
return result
# 5. 冷却时间与驱魔卷轴检查 (char_skill.cpp:455)
var next_time: int = skill_data.get("next_read_time", 0)
var bypass_cooldown := false
if current_time < next_time:
if has_affect(EXORCISM_SCROLL_VNUM):
affects.erase(EXORCISM_SCROLL_VNUM)
bypass_cooldown = true
result["consumed_exorcism"] = true
else:
result["error_code"] = "COOLDOWN_ACTIVE"
result["message"] = "心智尚需沉淀,请在冷却结束后再读,或使用驱魔卷轴。"
return result
# 扣除 20,000 EXP 并产生 24 小时冷却 (86,400 秒)
player_exp -= EXP_COST
result["exp_deducted"] = EXP_COST
skill_data["next_read_time"] = current_time + COOLDOWN_DURATION
total_books_read += 1
# 6. 成功率计算与隐士之药加成
var success_chance: int = BASE_SUCCESS_RATE
if has_affect(HERMIT_ADVICE_VNUM):
affects.erase(HERMIT_ADVICE_VNUM)
success_chance = 100 # 隐士之药 100% 保底成功 (char_skill.cpp:527)
result["consumed_hermit"] = true
var roll := roll_override if roll_override > 0 else (randi() % 100 + 1)
var is_read_success: bool = (roll <= success_chance)
var need_books: int = get_need_books_for_next_level(skill_id)
result["needed_books"] = need_books
if not is_read_success:
# 研读失败 (char_skill.cpp:613 READ_FAIL)
total_books_failed += 1
result["success"] = false
result["read_count"] = skill_data["read_count"]
result["remaining_books"] = need_books - skill_data["read_count"]
result["error_code"] = "READ_FAIL"
result["message"] = "研读失败:经脉运转不顺,书中奥义未能参透。"
return result
# 研读成功
total_books_succeeded += 1
result["success"] = true
var cur_count: int = skill_data["read_count"] + 1
if cur_count >= need_books:
# 晋级提升一阶等级!(SkillLevelUp)
skill_data["level"] += 1
skill_data["read_count"] = 0
result["level_up"] = true
result["current_level"] = skill_data["level"]
result["read_count"] = 0
result["remaining_books"] = 0
if skill_data["level"] >= 30:
result["breakthrough_g"] = true
result["message"] = "顿悟成功!技能历经千锤百炼,成功突破至宗师境界 (G1)!"
else:
result["message"] = "顿悟成功!技能境界提升至 M%d" % (skill_data["level"] - 19)
else:
# 累积研读进度
skill_data["read_count"] = cur_count
result["read_count"] = cur_count
result["remaining_books"] = need_books - cur_count
result["message"] = "心领神会!还需研读 %d 本成功即可突破进阶。" % (need_books - cur_count)
return result