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