- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
214 lines
7.4 KiB
GDScript
214 lines
7.4 KiB
GDScript
# 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
|