Files
mtgodot-poc/project/class_specialization_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

234 lines
7.0 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
}