- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
204 lines
6.6 KiB
GDScript
204 lines
6.6 KiB
GDScript
# skill_book_system.gd —— 40250 技能书研读与技能突破进阶系统 1:1 规则引擎
|
||
# 严格对照:
|
||
# metin2/src/server/game/src/char_item.cpp:2128-2160 (ITEM_SKILLBOOK / 50300)
|
||
# metin2/src/server/game/src/char_skill.cpp:416-570 (CHARACTER::LearnSkillByBook)
|
||
# metin2/src/server/game/src/char_item.cpp:1657 (Soul Stone 50513 / G->P)
|
||
# ClientVS22/UserInterface/Packet.h
|
||
extends RefCounted
|
||
|
||
const SKILL_BOOK_VNUM := 50300
|
||
const EXORCISM_SCROLL_VNUM := 71001 # 驱魔卷轴 (AFFECT_SKILL_NO_BOOK_DELAY)
|
||
const HERMIT_ADVICE_VNUM := 71094 # 隐士教训 (AFFECT_SKILL_BOOK_BONUS)
|
||
const SOUL_STONE_VNUM := 71001 # 魂石 (50513)
|
||
|
||
const SKILL_BOOK_EXP_COST := 20000 # 40250 char_skill.cpp:430 need_exp = 20000
|
||
const SKILL_BOOK_DELAY := 86400 # 24 小时冷却 (LC_IsKorea 86400s)
|
||
|
||
# 40250 MasterType 枚举
|
||
const MASTER_NORMAL := 0 # Lv 1 - 20
|
||
const MASTER_M := 1 # M1 - M10 (Lv 21 - 30)
|
||
const MASTER_G := 2 # G1 - G10 (Lv 31 - 40)
|
||
const MASTER_P := 3 # Perfect Master (Lv 40)
|
||
|
||
# 错误码
|
||
enum Result {
|
||
SUCCESS_LEVEL_UP, # 成功并升级(或突破到 G)
|
||
SUCCESS_READ_ONCE, # 成功研读一本(累积进度)
|
||
FAIL_READ, # 研读失败
|
||
ERR_NOT_LEARNABLE, # 非本职业/未入流派技能
|
||
ERR_NOT_MASTER_TIER, # 尚未达到 M 阶 (Lv < 20)
|
||
ERR_ALREADY_G_OR_P, # 已达到 G 阶或更高,无法再读技能书
|
||
ERR_INSUFFICIENT_EXP, # 经验不足 20,000
|
||
ERR_COOLDOWN, # 24小时冷却中
|
||
}
|
||
|
||
# 魂石善恶值消耗表 (char_skill.cpp / training_grandmaster_skill.quest)
|
||
const SOUL_STONE_KARMA_COST := {
|
||
1: 1000, 2: 1500, 3: 2000, 4: 2500, 5: 3000,
|
||
6: 3500, 7: 4000, 8: 4500, 9: 5000, 10: 5500
|
||
}
|
||
|
||
# 判定技能书升级所需本数:40250 char_skill.cpp:509 need_bookcount = GetSkillLevel(dwSkillVnum) - 20
|
||
static func get_need_books(skill_level: int) -> int:
|
||
# M1 (21) 需 1 本,M2 (22) 需 2 本 ... M10 (30) 需 10 本
|
||
return maxi(1, skill_level - 20)
|
||
|
||
# 研读条件前置校验
|
||
static func check_can_read(
|
||
skill_id: int,
|
||
skill_level: int,
|
||
master_type: int,
|
||
player_exp: int,
|
||
next_read_time: int,
|
||
current_time: int,
|
||
has_exorcism: bool
|
||
) -> int:
|
||
if skill_id <= 0:
|
||
return Result.ERR_NOT_LEARNABLE
|
||
|
||
if master_type < MASTER_M or skill_level < 20:
|
||
return Result.ERR_NOT_MASTER_TIER
|
||
|
||
if master_type > MASTER_M or skill_level >= 30:
|
||
return Result.ERR_ALREADY_G_OR_P
|
||
|
||
if player_exp < SKILL_BOOK_EXP_COST:
|
||
return Result.ERR_INSUFFICIENT_EXP
|
||
|
||
if current_time < next_read_time and not has_exorcism:
|
||
return Result.ERR_COOLDOWN
|
||
|
||
return 0 # OK
|
||
|
||
# 研读技能书核心判定逻辑(1:1 char_skill.cpp:488-570)
|
||
static func read_skill_book(
|
||
skill_id: int,
|
||
skill_level: int,
|
||
master_type: int,
|
||
read_count: int,
|
||
player_exp: int,
|
||
next_read_time: int,
|
||
current_time: int,
|
||
has_exorcism: bool,
|
||
has_hermit: bool,
|
||
roll_val := -1 # 测试可注入 1..100
|
||
) -> Dictionary:
|
||
var err := check_can_read(skill_id, skill_level, master_type, player_exp, next_read_time, current_time, has_exorcism)
|
||
if err != 0:
|
||
return {
|
||
"result": err,
|
||
"skill_id": skill_id,
|
||
"level": skill_level,
|
||
"master_type": master_type,
|
||
"read_count": read_count,
|
||
"exp_consumed": 0,
|
||
"next_read_time": next_read_time,
|
||
"consumed_exorcism": false,
|
||
"consumed_hermit": false,
|
||
}
|
||
|
||
var consumed_exorcism := false
|
||
if current_time < next_read_time and has_exorcism:
|
||
consumed_exorcism = true
|
||
|
||
var consumed_hermit := false
|
||
var fail_percent := 65 # 40250 char_skill.cpp:525 int percent = 65;
|
||
if has_hermit:
|
||
fail_percent = 0 # 100% 成功
|
||
consumed_hermit = true
|
||
|
||
var dice := roll_val if roll_val > 0 else (randi() % 100 + 1)
|
||
var success := (dice > fail_percent)
|
||
|
||
var new_level := skill_level
|
||
var new_master := master_type
|
||
var new_read_count := read_count
|
||
var new_next_time := current_time + SKILL_BOOK_DELAY
|
||
var res := Result.FAIL_READ
|
||
|
||
if success:
|
||
var need_books := get_need_books(skill_level)
|
||
if read_count + 1 >= need_books:
|
||
# 升级!
|
||
new_level += 1
|
||
new_read_count = 0
|
||
if new_level >= 30:
|
||
new_master = MASTER_G # 突破到 G1
|
||
res = Result.SUCCESS_LEVEL_UP
|
||
else:
|
||
new_read_count += 1
|
||
res = Result.SUCCESS_READ_ONCE
|
||
|
||
return {
|
||
"result": res,
|
||
"skill_id": skill_id,
|
||
"level": new_level,
|
||
"master_type": new_master,
|
||
"read_count": new_read_count,
|
||
"exp_consumed": SKILL_BOOK_EXP_COST,
|
||
"next_read_time": new_next_time,
|
||
"consumed_exorcism": consumed_exorcism,
|
||
"consumed_hermit": consumed_hermit,
|
||
"need_books": get_need_books(new_level),
|
||
}
|
||
|
||
# 魂石修炼逻辑(1:1 G1..G10 -> P)
|
||
static func soul_stone_train(
|
||
skill_id: int,
|
||
skill_level: int,
|
||
master_type: int,
|
||
current_karma: int,
|
||
roll_val := -1
|
||
) -> Dictionary:
|
||
if master_type != MASTER_G or skill_level < 30 or skill_level >= 40:
|
||
return {"success": false, "err": "not_g_tier"}
|
||
|
||
var g_step := skill_level - 30 # 1..10
|
||
var base_cost: int = SOUL_STONE_KARMA_COST.get(g_step, 1000)
|
||
var actual_cost: int = base_cost if current_karma >= 0 else (base_cost * 2)
|
||
|
||
var dice := roll_val if roll_val > 0 else (randi() % 100 + 1)
|
||
# 40250 魂石成功率约 30%
|
||
var success := (dice <= 30)
|
||
|
||
var new_level := skill_level
|
||
var new_master := master_type
|
||
if success:
|
||
new_level += 1
|
||
if new_level >= 40:
|
||
new_master = MASTER_P
|
||
|
||
return {
|
||
"success": success,
|
||
"skill_id": skill_id,
|
||
"level": new_level,
|
||
"master_type": new_master,
|
||
"karma_consumed": actual_cost,
|
||
}
|
||
|
||
# 格式化官方提示文本
|
||
static func get_result_message(res_dict: Dictionary) -> String:
|
||
match res_dict.get("result", -1):
|
||
Result.SUCCESS_LEVEL_UP:
|
||
if res_dict.get("master_type") == MASTER_G and res_dict.get("level") == 30:
|
||
return "领悟了大宗师的奥秘!技能已成功突破至大宗师(G1)!"
|
||
return "完全理解了书中内容,技能成功提升到了新的境界!"
|
||
Result.SUCCESS_READ_ONCE:
|
||
var remaining := int(res_dict.get("need_books", 1)) - int(res_dict.get("read_count", 0))
|
||
return "成功领悟了部分技巧!还需要成功研读 %d 本书以完成突破。" % remaining
|
||
Result.FAIL_READ:
|
||
return "研读失败,未能领悟书中的奥秘。"
|
||
Result.ERR_NOT_LEARNABLE:
|
||
return "无法修炼该技能书。"
|
||
Result.ERR_NOT_MASTER_TIER:
|
||
return "这个技能还未达到大师级别(M),不能通过技能书修炼。"
|
||
Result.ERR_ALREADY_G_OR_P:
|
||
return "这个技能已达到大宗师或以上级别,无法再通过普通技能书提升。"
|
||
Result.ERR_INSUFFICIENT_EXP:
|
||
return "经验值不足(需要 20,000 EXP)。"
|
||
Result.ERR_COOLDOWN:
|
||
var remaining_s := maxi(0, int(res_dict.get("next_read_time", 0)))
|
||
return "当前处于研读冷却中,请使用驱魔卷轴消除冷却或稍后再试。"
|
||
_:
|
||
return ""
|