- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
234 lines
7.0 KiB
GDScript
234 lines
7.0 KiB
GDScript
# class_specialization_system.gd —— Metin2 40250 角色 30 级双流派转职与无损洗髓系统 1:1
|
||
# 对照 40250 服务端 quest/skill_group.quest, char_skill.cpp, cmd_general.cpp
|
||
class_name ClassSpecializationSystem
|
||
extends RefCounted
|
||
|
||
signal specialization_chosen(job: int, spec_id: int, spec_name: String)
|
||
signal skill_point_allocated(skill_vnum: int, new_level: int, remaining_points: int)
|
||
signal specialization_reset(refunded_points: int)
|
||
|
||
const VNUM_RESET_SCROLL := 71100 # 洗髓卷轴 / 重新分配技能书 (Scroll of Redistribution)
|
||
const REQUIRED_LEVEL := 30 # 转职等级门槛
|
||
|
||
const SPEC_TREE: Dictionary = {
|
||
0: { # 战士
|
||
1: {
|
||
"name": "战狂流派 (Arahan)",
|
||
"desc": "极度偏向狂暴输出与高速连斩,兼具霸体与破坏力",
|
||
"skills": {
|
||
1: "三连斩",
|
||
2: "旋风斩",
|
||
3: "狂战士",
|
||
4: "剑气",
|
||
5: "冲锋"
|
||
}
|
||
},
|
||
2: {
|
||
"name": "圣战流派 (Partisan)",
|
||
"desc": "坚若磐石的钢铁之躯,以强身术与精神打击粉碎强敌",
|
||
"skills": {
|
||
16: "强身术",
|
||
17: "精神打击",
|
||
18: "宝剑斩",
|
||
19: "震地波",
|
||
20: "剑气斩"
|
||
}
|
||
}
|
||
},
|
||
1: { # 刺客
|
||
1: {
|
||
"name": "近战刺客流派 (Dagger)",
|
||
"desc": "隐身于阴影之中,擅长淬毒与致命近身背刺",
|
||
"skills": {
|
||
31: "伏击",
|
||
32: "绝杀",
|
||
33: "匕首旋风",
|
||
34: "隐身术",
|
||
35: "毒刃"
|
||
}
|
||
},
|
||
2: {
|
||
"name": "远程弓箭流派 (Archer)",
|
||
"desc": "千里之外取敌首级,漫天箭雨与火炎穿透射击",
|
||
"skills": {
|
||
46: "连射",
|
||
47: "箭雨",
|
||
48: "火炎箭",
|
||
49: "疾风步",
|
||
50: "毒箭"
|
||
}
|
||
}
|
||
},
|
||
2: { # 修罗
|
||
1: {
|
||
"name": "魔剑修罗流派 (Weapon)",
|
||
"desc": "附魔之刃附体,吸血噬魂与破魔剑气并存",
|
||
"skills": {
|
||
61: "附魔之刃",
|
||
62: "附魔之甲",
|
||
63: "惧意",
|
||
64: "龙卷风",
|
||
65: "手指戳击"
|
||
}
|
||
},
|
||
2: {
|
||
"name": "黑魔法修罗流派 (Black Magic)",
|
||
"desc": "掌控幽冥暗焰,幽暗之盾抵消伤害,黑暗之球远程轰杀",
|
||
"skills": {
|
||
76: "幽冥一击",
|
||
77: "烈焰一击",
|
||
78: "黑暗之球",
|
||
79: "幽暗之盾",
|
||
80: "鬼影步"
|
||
}
|
||
}
|
||
},
|
||
3: { # 萨满
|
||
1: {
|
||
"name": "巨龙神圣辅助流派 (Dragon)",
|
||
"desc": "掌控巨龙图腾之力,为团队提供巨龙之赐与龙之协助光环",
|
||
"skills": {
|
||
91: "飞龙射击",
|
||
92: "巨龙射击",
|
||
93: "巨龙咆哮",
|
||
94: "巨龙之赐",
|
||
95: "龙之协助"
|
||
}
|
||
},
|
||
2: {
|
||
"name": "雷电神圣治疗流派 (Lightning)",
|
||
"desc": "引动九天雷电轰杀群敌,施展神圣治愈术与疾风迅捷",
|
||
"skills": {
|
||
106: "雷电箭",
|
||
107: "召唤雷电",
|
||
108: "雷电之爪",
|
||
109: "治愈术",
|
||
110: "迅捷步"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# 获取职业可选流派
|
||
func get_available_specializations(job: int) -> Dictionary:
|
||
return SPEC_TREE.get(job, {})
|
||
|
||
# 进行 30 级转职选定流派
|
||
func choose_specialization(spec_id: int, player_data: Dictionary) -> Dictionary:
|
||
var job: int = int(player_data.get("job", 0))
|
||
var lvl: int = int(player_data.get("level", 1))
|
||
|
||
if lvl < REQUIRED_LEVEL:
|
||
return {"ok": false, "reason": "LEVEL_TOO_LOW", "msg": "转职需要角色等级达到 30 级!当前等级: %d" % lvl}
|
||
|
||
var cur_group: int = int(player_data.get("skill_group", 0))
|
||
if cur_group != 0:
|
||
return {"ok": false, "reason": "ALREADY_SPECIALIZED", "msg": "你已选定了流派!若需变更,请先使用【洗髓卷轴】。"}
|
||
|
||
if not SPEC_TREE.has(job) or not SPEC_TREE[job].has(spec_id):
|
||
return {"ok": false, "reason": "INVALID_SPEC_ID", "msg": "无效的流派代码!"}
|
||
|
||
var spec = SPEC_TREE[job][spec_id]
|
||
player_data["skill_group"] = spec_id
|
||
|
||
# 初始化该流派专属 5 大技能
|
||
if not player_data.has("skills"):
|
||
player_data["skills"] = {}
|
||
|
||
for svnum in spec["skills"].keys():
|
||
player_data["skills"][svnum] = {
|
||
"name": spec["skills"][svnum],
|
||
"level": 0,
|
||
"master_type": 0
|
||
}
|
||
|
||
specialization_chosen.emit(job, spec_id, spec["name"])
|
||
|
||
return {
|
||
"ok": true,
|
||
"job": job,
|
||
"spec_id": spec_id,
|
||
"spec_name": spec["name"],
|
||
"skills_count": spec["skills"].size(),
|
||
"msg": "恭喜!成功完成 30 级转职,踏入【%s】流派之道!" % spec["name"]
|
||
}
|
||
|
||
# 学习与加点技能
|
||
func allocate_skill_point(skill_vnum: int, player_data: Dictionary) -> Dictionary:
|
||
var cur_group: int = int(player_data.get("skill_group", 0))
|
||
if cur_group == 0:
|
||
return {"ok": false, "reason": "NOT_SPECIALIZED", "msg": "尚未进行 30 级转职,无法修习流派绝学!"}
|
||
|
||
var pts: int = int(player_data.get("skill_points", 0))
|
||
if pts <= 0:
|
||
return {"ok": false, "reason": "NO_SKILL_POINTS", "msg": "技能点数不足!"}
|
||
|
||
var skills: Dictionary = player_data.get("skills", {})
|
||
if not skills.has(skill_vnum):
|
||
return {"ok": false, "reason": "SKILL_NOT_AVAILABLE", "msg": "此技能不属于当前专精流派!"}
|
||
|
||
var sk = skills[skill_vnum]
|
||
var cur_lvl: int = int(sk.get("level", 0))
|
||
if cur_lvl >= 20:
|
||
return {"ok": false, "reason": "MAX_NORMAL_LEVEL", "msg": "该技能基础等级已满 (20),进阶需使用技能秘籍!"}
|
||
|
||
sk["level"] = cur_lvl + 1
|
||
player_data["skill_points"] = pts - 1
|
||
|
||
skill_point_allocated.emit(skill_vnum, sk["level"], player_data["skill_points"])
|
||
|
||
return {
|
||
"ok": true,
|
||
"skill_vnum": skill_vnum,
|
||
"new_level": sk["level"],
|
||
"remaining_points": player_data["skill_points"],
|
||
"msg": "【%s】提升至等级 %d!" % [sk.get("name", "技能"), sk["level"]]
|
||
}
|
||
|
||
# 使用洗髓卷轴 (71100) 无损重置流派与返还技能点
|
||
func reset_specialization(inventory: Array, player_data: Dictionary, free_reset: bool = false) -> Dictionary:
|
||
var cur_group: int = int(player_data.get("skill_group", 0))
|
||
if cur_group == 0:
|
||
return {"ok": false, "reason": "NOT_SPECIALIZED", "msg": "当前尚未选定任何专精流派,无需洗髓!"}
|
||
|
||
# 卷轴消耗检查
|
||
var scroll_slot = -1
|
||
if not free_reset:
|
||
for i in range(inventory.size()):
|
||
if inventory[i] != null and int(inventory[i].get("vnum", 0)) == VNUM_RESET_SCROLL:
|
||
scroll_slot = i
|
||
break
|
||
if scroll_slot == -1:
|
||
return {"ok": false, "reason": "NEED_RESET_SCROLL", "msg": "需要【洗髓卷轴】(71100) 才能重置专精流派!"}
|
||
|
||
# 统计已投入的所有技能点数
|
||
var skills: Dictionary = player_data.get("skills", {})
|
||
var total_invested: int = 0
|
||
for svnum in skills.keys():
|
||
var sk = skills[svnum]
|
||
total_invested += int(sk.get("level", 0))
|
||
|
||
# 消耗卷轴
|
||
if scroll_slot != -1:
|
||
var sc_item = inventory[scroll_slot]
|
||
var sc_cnt = int(sc_item.get("count", 1))
|
||
if sc_cnt > 1:
|
||
sc_item["count"] = sc_cnt - 1
|
||
else:
|
||
inventory[scroll_slot] = null
|
||
|
||
# 全额返还技能点
|
||
var cur_pts: int = int(player_data.get("skill_points", 0))
|
||
player_data["skill_points"] = cur_pts + total_invested
|
||
player_data["skill_group"] = 0
|
||
player_data["skills"] = {}
|
||
|
||
specialization_reset.emit(total_invested)
|
||
|
||
return {
|
||
"ok": true,
|
||
"refunded_points": total_invested,
|
||
"total_skill_points": player_data["skill_points"],
|
||
"msg": "洗髓成功!经脉重铸,%d 点技能点数全额返还,可重新选定双分支流派!" % total_invested
|
||
}
|