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

184 lines
7.2 KiB
GDScript

# status_skill_reset_system.gd —— Metin2 40250 官方属性重修卷轴与忘却之书洗点系统 1:1
# 对照 40250 服务端 char_item.cpp, char.cpp, root/uipointreset.py
class_name StatusSkillResetSystem
extends RefCounted
signal status_reset(stat_name: String, refunded_points: int)
signal all_status_reset(refunded_points: int)
signal skill_point_reset(skill_id: int, new_level: int)
signal all_skills_reset(refunded_points: int)
# 40250 官方重修道具 Vnum
const VNUM_SKILL_RESET_SCROLL := 71001 # 技能重置卷轴 (全部技能洗点)
const VNUM_STATUS_REDIST_SCROLL := 71002 # 属性重修卷轴 (单项四维属性洗点)
const VNUM_ALL_STATUS_RESET_SCROLL := 71003 # 全属性重置卷轴 (血统重塑卷轴)
# 40250 四维职业初始基础属性标准表 (Lv 1 Base)
const JOB_BASE_STATS: Dictionary = {
"warrior": {"vit": 16, "int": 3, "str": 15, "dex": 5},
"assassin": {"vit": 14, "int": 3, "str": 8, "dex": 16},
"sura": {"vit": 15, "int": 16, "str": 8, "dex": 3},
"shaman": {"vit": 15, "int": 16, "str": 5, "dex": 6}
}
# 单项属性重修 (71002 属性重修卷轴)
func reset_single_status(stat_name: String, player_data: Dictionary, inventory: Array, item_slot: int) -> Dictionary:
var valid_stats = ["vit", "int", "str", "dex"]
if not valid_stats.has(stat_name):
return {"ok": false, "reason": "INVALID_STAT_NAME", "msg": "无效的属性名称!"}
if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null:
return {"ok": false, "reason": "INVALID_SLOT"}
var item: Dictionary = inventory[item_slot]
if int(item.get("vnum", 0)) != VNUM_STATUS_REDIST_SCROLL:
return {"ok": false, "reason": "NOT_A_STATUS_REDIST_SCROLL", "msg": "需要使用【属性重修卷轴】(71002)!"}
var job: String = str(player_data.get("job", "warrior"))
var base_stat_table: Dictionary = JOB_BASE_STATS.get(job, JOB_BASE_STATS["warrior"])
var base_val: int = int(base_stat_table.get(stat_name, 10))
var cur_val: int = int(player_data.get(stat_name, base_val))
if cur_val <= base_val:
return {"ok": false, "reason": "STAT_ALREADY_AT_BASE", "msg": "该属性尚未分配任何点数,无需重置!"}
var refunded: int = cur_val - base_val
# 消耗 1 卷
var cnt := int(item.get("count", 1))
if cnt > 1:
item["count"] = cnt - 1
else:
inventory[item_slot] = null
player_data[stat_name] = base_val
player_data["unused_status_points"] = int(player_data.get("unused_status_points", 0)) + refunded
status_reset.emit(stat_name, refunded)
return {
"ok": true,
"stat_name": stat_name,
"refunded_points": refunded,
"new_val": base_val,
"total_unused": player_data["unused_status_points"],
"msg": "成功重置【%s】,全额退还 %d 点属性点!" % [stat_name.to_upper(), refunded]
}
# 全属性重置 (71003 全属性重置卷轴)
func reset_all_status(player_data: Dictionary, inventory: Array, item_slot: int) -> Dictionary:
if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null:
return {"ok": false, "reason": "INVALID_SLOT"}
var item: Dictionary = inventory[item_slot]
if int(item.get("vnum", 0)) != VNUM_ALL_STATUS_RESET_SCROLL:
return {"ok": false, "reason": "NOT_AN_ALL_STATUS_RESET_SCROLL", "msg": "需要使用【全属性重置卷轴】(71003)!"}
var job: String = str(player_data.get("job", "warrior"))
var base_stat_table: Dictionary = JOB_BASE_STATS.get(job, JOB_BASE_STATS["warrior"])
var total_refunded := 0
for stat_name in ["vit", "int", "str", "dex"]:
var base_val: int = int(base_stat_table.get(stat_name, 10))
var cur_val: int = int(player_data.get(stat_name, base_val))
if cur_val > base_val:
total_refunded += (cur_val - base_val)
player_data[stat_name] = base_val
# 消耗 1 卷
var cnt := int(item.get("count", 1))
if cnt > 1:
item["count"] = cnt - 1
else:
inventory[item_slot] = null
player_data["unused_status_points"] = int(player_data.get("unused_status_points", 0)) + total_refunded
all_status_reset.emit(total_refunded)
return {
"ok": true,
"refunded_points": total_refunded,
"total_unused": player_data["unused_status_points"],
"msg": "血统重塑完成!四维属性恢复初始基础值,共退还 %d 点属性点。" % total_refunded
}
# 忘却之书重置单项技能 1 点 (50311~50320 Book of Oblivion)
func reset_single_skill_point(skill_id: int, player_skills: Dictionary, player_data: Dictionary, inventory: Array, item_slot: int) -> Dictionary:
if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null:
return {"ok": false, "reason": "INVALID_SLOT"}
var book: Dictionary = inventory[item_slot]
var book_vnum := int(book.get("vnum", 0))
var book_target_skill := int(book.get("target_skill", 0))
# 检查是否为忘却之书 (50311~50330)
if book_vnum < 50311 or book_vnum > 50330:
return {"ok": false, "reason": "NOT_A_BOOK_OF_OBLIVION", "msg": "需要使用对应的【忘却之书】!"}
if book_target_skill != 0 and book_target_skill != skill_id:
return {"ok": false, "reason": "SKILL_ID_MISMATCH", "msg": "此忘却之书与目标技能不符!"}
var cur_skill_lv := int(player_skills.get(skill_id, 0))
if cur_skill_lv <= 0:
return {"ok": false, "reason": "SKILL_LEVEL_ZERO", "msg": "该技能尚未习得,无法忘却!"}
# 40250 官方规则:技能达到 Master (>= 20) 后无法使用普通忘却之书降级
if cur_skill_lv >= 20:
return {"ok": false, "reason": "SKILL_IS_MASTER", "msg": "已达到大师境界(Master)的技能无法通过普通忘却之书降级!"}
# 消耗 1 本忘却之书
var cnt := int(book.get("count", 1))
if cnt > 1:
book["count"] = cnt - 1
else:
inventory[item_slot] = null
player_skills[skill_id] = cur_skill_lv - 1
player_data["unused_skill_points"] = int(player_data.get("unused_skill_points", 0)) + 1
skill_point_reset.emit(skill_id, player_skills[skill_id])
return {
"ok": true,
"skill_id": skill_id,
"new_level": player_skills[skill_id],
"refunded_points": 1,
"total_unused": player_data["unused_skill_points"],
"msg": "领悟忘却之书!技能等级降低 1 级,返还 1 点可用技能点。"
}
# 技能重置卷轴全技能洗点 (71001)
func reset_all_skills(player_skills: Dictionary, player_data: Dictionary, inventory: Array, item_slot: int) -> Dictionary:
if item_slot < 0 or item_slot >= inventory.size() or inventory[item_slot] == null:
return {"ok": false, "reason": "INVALID_SLOT"}
var item: Dictionary = inventory[item_slot]
if int(item.get("vnum", 0)) != VNUM_SKILL_RESET_SCROLL:
return {"ok": false, "reason": "NOT_A_SKILL_RESET_SCROLL", "msg": "需要使用【技能重置卷轴】(71001)!"}
var total_refunded := 0
for sk_id in player_skills.keys():
var lv := int(player_skills[sk_id])
if lv > 0:
total_refunded += lv
player_skills[sk_id] = 0
# 消耗 1 卷
var cnt := int(item.get("count", 1))
if cnt > 1:
item["count"] = cnt - 1
else:
inventory[item_slot] = null
player_data["unused_skill_points"] = int(player_data.get("unused_skill_points", 0)) + total_refunded
all_skills_reset.emit(total_refunded)
return {
"ok": true,
"refunded_points": total_refunded,
"total_unused": player_data["unused_skill_points"],
"msg": "技能重置完成!所有技能点已洗净,全额返还 %d 点可用技能点。" % total_refunded
}