- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
179 lines
6.2 KiB
GDScript
179 lines
6.2 KiB
GDScript
# soul_stone_grandmaster_system.gd —— Metin2 40250 官方宗师绝技灵魂石悟道与善恶值走火入魔系统 1:1
|
|
# 100% 对照 40250 服务端 training_grandmaster_skill.quest, char_skill.cpp, questlua_pc.cpp
|
|
class_name SoulStoneGrandmasterSystem
|
|
extends RefCounted
|
|
|
|
signal grandmaster_trained(skill_id: int, skill_name: String, new_level: int, alignment_deducted: int)
|
|
signal grandmaster_failed(skill_id: int, skill_name: String, alignment_penalty: int)
|
|
signal cooldown_cleared()
|
|
signal training_error(reason: String)
|
|
|
|
# 40250 官方核心道具 Vnum
|
|
const VNUM_SOUL_STONE := 50513 # 灵魂石 (Ruh Taşı / Soul Stone)
|
|
const VNUM_EXORCISM_SCROLL := 71001 # 驱魔卷轴 (清除修炼间隔)
|
|
|
|
# 技能等级境界常数
|
|
const GRAND_MASTER_MIN_LEVEL := 30 # G1 (30级)
|
|
const PERFECT_MASTER_LEVEL := 40 # P 阶绝顶境界 (40级)
|
|
|
|
# 最低极限负善恶值 (-20000 封底)
|
|
const MIN_ALIGNMENT_CAP := -20000
|
|
|
|
# 修炼冷却时间
|
|
var training_cooldown_left: float = 0.0 # 8~12 小时倒计时
|
|
|
|
# 判断技能是否处于宗师境界 (G1 ~ G10)
|
|
func can_train_skill(skill_level: int) -> bool:
|
|
return skill_level >= GRAND_MASTER_MIN_LEVEL and skill_level < PERFECT_MASTER_LEVEL
|
|
|
|
# 计算升级所需善恶值 (40250 官方公式: 1000 + 500 * (level - 30),负道德翻倍)
|
|
func get_required_alignment(skill_level: int, current_alignment: int) -> int:
|
|
if not can_train_skill(skill_level):
|
|
return 0
|
|
var base_cost = 1000 + 500 * (skill_level - GRAND_MASTER_MIN_LEVEL)
|
|
if current_alignment < 0:
|
|
return base_cost * 2
|
|
return base_cost
|
|
|
|
# 获取技能境界文本 (如 "G1", "G5", "P")
|
|
func get_skill_tier_text(level: int) -> String:
|
|
if level < GRAND_MASTER_MIN_LEVEL:
|
|
return "Normal/Master"
|
|
elif level >= PERFECT_MASTER_LEVEL:
|
|
return "P 阶 (Perfect Master)"
|
|
else:
|
|
return "G%d" % (level - GRAND_MASTER_MIN_LEVEL + 1)
|
|
|
|
# 使用灵魂石闭关冲击宗师绝技
|
|
func train_grandmaster_skill(
|
|
inventory: Array,
|
|
soul_stone_slot: int,
|
|
skill_id: int,
|
|
skill_name: String,
|
|
current_skill_level: int,
|
|
current_alignment: int,
|
|
has_exorcism_scroll: bool = false,
|
|
forced_success: bool = false,
|
|
forced_roll: int = -1
|
|
) -> Dictionary:
|
|
# 检查技能是否在 G 阶
|
|
if not can_train_skill(current_skill_level):
|
|
training_error.emit("INVALID_SKILL_LEVEL")
|
|
return {
|
|
"ok": false,
|
|
"reason": "INVALID_SKILL_LEVEL",
|
|
"msg": "只有达到宗师境界 (G1~G10) 的技能方可使用灵魂石闭关突破!"
|
|
}
|
|
|
|
# 检查冷却
|
|
if training_cooldown_left > 0.0 and not has_exorcism_scroll:
|
|
training_error.emit("COOLDOWN_ACTIVE")
|
|
return {
|
|
"ok": false,
|
|
"reason": "COOLDOWN_ACTIVE",
|
|
"msg": "心神尚未平复!需等待 %d 秒方可再次闭关,或使用【驱魔卷轴】重置心境。" % int(training_cooldown_left)
|
|
}
|
|
|
|
# 检查灵魂石
|
|
if soul_stone_slot < 0 or soul_stone_slot >= inventory.size() or inventory[soul_stone_slot] == null:
|
|
training_error.emit("NO_SOUL_STONE")
|
|
return {"ok": false, "reason": "NO_SOUL_STONE", "msg": "未找到灵魂石!"}
|
|
|
|
var item = inventory[soul_stone_slot]
|
|
var vnum = int(item.get("vnum", 0))
|
|
if vnum != VNUM_SOUL_STONE:
|
|
training_error.emit("NOT_A_SOUL_STONE")
|
|
return {"ok": false, "reason": "NOT_A_SOUL_STONE", "msg": "该物品并不是灵魂石!"}
|
|
|
|
# 计算所需善恶值
|
|
var need_align = get_required_alignment(current_skill_level, current_alignment)
|
|
|
|
# 检查是否跌破道德底线 (40250: cur_alignment < -19000 + need_alignment)
|
|
if current_alignment < (MIN_ALIGNMENT_CAP + 1000 + need_align):
|
|
training_error.emit("ALIGNMENT_TOO_LOW")
|
|
return {
|
|
"ok": false,
|
|
"reason": "ALIGNMENT_TOO_LOW",
|
|
"msg": "你的罪孽太过深重,心灵已被心魔完全吞噬!必须行善恢复善恶值后方能参悟。"
|
|
}
|
|
|
|
# 消耗灵魂石 (40250 pc.remove_item(50513))
|
|
_consume_item_one(inventory, soul_stone_slot)
|
|
|
|
# 设置下次领悟冷却时间 8~12 小时 (28800 ~ 43200 秒)
|
|
training_cooldown_left = randf_range(28800.0, 43200.0)
|
|
|
|
# 判定成功概率 (40250 官方约为 30% ~ 35%)
|
|
var r = forced_roll if forced_roll > 0 else randi_range(1, 100)
|
|
var is_success = (r <= 30) or forced_success
|
|
|
|
if is_success:
|
|
# 悟道成功
|
|
var new_lvl = current_skill_level + 1
|
|
var new_alignment = current_alignment - need_align
|
|
grandmaster_trained.emit(skill_id, skill_name, new_lvl, need_align)
|
|
|
|
var tier_str = get_skill_tier_text(new_lvl)
|
|
var reach_p = (new_lvl >= PERFECT_MASTER_LEVEL)
|
|
var msg = ""
|
|
if reach_p:
|
|
msg = "金光贯顶,大道终成!你的【%s】突破桎梏,登峰造极达成了【%s】绝顶境界!" % [skill_name, tier_str]
|
|
else:
|
|
msg = "神识升华,灵智通达!你的【%s】成功精进为【%s】!消耗善恶值 %d 点。" % [skill_name, tier_str, need_align]
|
|
|
|
return {
|
|
"ok": true,
|
|
"success": true,
|
|
"new_level": new_lvl,
|
|
"tier_text": tier_str,
|
|
"is_perfect_master": reach_p,
|
|
"alignment_cost": need_align,
|
|
"new_alignment": new_alignment,
|
|
"msg": msg
|
|
}
|
|
else:
|
|
# 走火入魔失败:扣除 1/3 ~ 1/2 的善恶值惩罚
|
|
var penalty_ratio = randf_range(0.33, 0.50)
|
|
var penalty = max(1, int(floor(need_align * penalty_ratio)))
|
|
var new_alignment = current_alignment - penalty
|
|
grandmaster_failed.emit(skill_id, skill_name, penalty)
|
|
|
|
return {
|
|
"ok": true,
|
|
"success": false,
|
|
"new_level": current_skill_level,
|
|
"tier_text": get_skill_tier_text(current_skill_level),
|
|
"is_perfect_master": false,
|
|
"alignment_cost": penalty,
|
|
"new_alignment": new_alignment,
|
|
"msg": "走火入魔!灵魂石在狂暴的灵气中化为碎粉,未能参悟真谛。惩罚扣除 %d 点善恶值!" % penalty
|
|
}
|
|
|
|
# 使用驱魔卷轴清除冷却
|
|
func use_exorcism_scroll() -> void:
|
|
training_cooldown_left = 0.0
|
|
cooldown_cleared.emit()
|
|
|
|
func update(delta: float) -> void:
|
|
if training_cooldown_left > 0.0:
|
|
training_cooldown_left = max(0.0, training_cooldown_left - delta)
|
|
|
|
func _consume_item_one(inventory: Array, slot: int) -> void:
|
|
if slot < 0 or slot >= inventory.size() or inventory[slot] == null:
|
|
return
|
|
var item = inventory[slot]
|
|
var count = int(item.get("count", 1))
|
|
if count > 1:
|
|
item["count"] = count - 1
|
|
else:
|
|
inventory[slot] = null
|
|
|
|
# 序列化与反序列化
|
|
func serialize() -> Dictionary:
|
|
return {
|
|
"cooldown_left": training_cooldown_left
|
|
}
|
|
|
|
func deserialize(data: Dictionary) -> void:
|
|
training_cooldown_left = float(data.get("cooldown_left", 0.0))
|