- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
201 lines
6.3 KiB
GDScript
201 lines
6.3 KiB
GDScript
# polymorph_marble_system.gd —— Metin2 40250 官方变身宝珠与怪物精魂附体系统 1:1
|
|
# 对照 40250 服务端 polymorph.cpp, polymorph.h, char.cpp, affect.h
|
|
class_name PolymorphMarbleSystem
|
|
extends RefCounted
|
|
|
|
signal polymorph_started(mob_vnum: int, mob_name: String, duration: float, bonus_type: int, bonus_val: int)
|
|
signal polymorph_ended(mob_vnum: int, mob_name: String)
|
|
|
|
const VNUM_POLYMORPH_MARBLE := 70104 # 变身宝珠 (Polymorph Marble)
|
|
|
|
# 40250 变身法术技能境界 (Skill Master Type)
|
|
const SKILL_NORMAL := 0 # 普通熟练度 (1~19 级): 10 分钟 (600s)
|
|
const SKILL_MASTER := 1 # Master (M1~M10): 15 分钟 (900s)
|
|
const SKILL_GRAND_MASTER := 2 # Grand Master (G1~G10): 20 分钟 (1200s)
|
|
const SKILL_PERFECT_MASTER := 3 # Perfect Master (P): 25 分钟 (1500s)
|
|
|
|
# 40250 polymorph.h 加成属性类型枚举
|
|
const POLYMORPH_NO_BONUS := 0
|
|
const POLYMORPH_ATK_BONUS := 1 # 攻击力增益 (POINT_ATT_BONUS)
|
|
const POLYMORPH_DEF_BONUS := 2 # 防御力增益 (POINT_DEF_BONUS)
|
|
const POLYMORPH_SPD_BONUS := 3 # 移动速度增益 (POINT_MOV_SPEED)
|
|
|
|
# 40250 经典怪物精魂图鉴与加成配置表
|
|
const MONSTER_POLY_TABLE: Dictionary = {
|
|
501: {
|
|
"mob_vnum": 501,
|
|
"name": "野蛮步兵",
|
|
"bonus_type": POLYMORPH_ATK_BONUS,
|
|
"bonus_val": 40,
|
|
"stat_key": "attack_power"
|
|
},
|
|
502: {
|
|
"mob_vnum": 502,
|
|
"name": "野蛮弓手",
|
|
"bonus_type": POLYMORPH_ATK_BONUS,
|
|
"bonus_val": 35,
|
|
"stat_key": "attack_power"
|
|
},
|
|
492: {
|
|
"mob_vnum": 492,
|
|
"name": "白发蜘蛛",
|
|
"bonus_type": POLYMORPH_DEF_BONUS,
|
|
"bonus_val": 50,
|
|
"stat_key": "defense"
|
|
},
|
|
101: {
|
|
"mob_vnum": 101,
|
|
"name": "野狗",
|
|
"bonus_type": POLYMORPH_SPD_BONUS,
|
|
"bonus_val": 25,
|
|
"stat_key": "move_speed"
|
|
},
|
|
1901: {
|
|
"mob_vnum": 1901,
|
|
"name": "九尾妖狐",
|
|
"bonus_type": POLYMORPH_SPD_BONUS,
|
|
"bonus_val": 30,
|
|
"stat_key": "move_speed"
|
|
}
|
|
}
|
|
|
|
var is_polymorphed: bool = false
|
|
var active_mob_vnum: int = 0
|
|
var active_mob_name: String = ""
|
|
var time_remaining: float = 0.0
|
|
var total_duration: float = 0.0
|
|
var active_bonus_type: int = POLYMORPH_NO_BONUS
|
|
var active_bonus_val: int = 0
|
|
var active_stat_key: String = ""
|
|
|
|
# 计算变身持续时长 (40250 polymorph.cpp:44-59)
|
|
static func get_duration_by_mastery(master_type: int) -> float:
|
|
match master_type:
|
|
SKILL_NORMAL:
|
|
return 600.0 # 10 分钟
|
|
SKILL_MASTER:
|
|
return 900.0 # 15 分钟
|
|
SKILL_GRAND_MASTER:
|
|
return 1200.0 # 20 分钟
|
|
SKILL_PERFECT_MASTER:
|
|
return 1500.0 # 25 分钟
|
|
_:
|
|
return 600.0
|
|
|
|
# 使用变身宝珠进行怪物附体变身
|
|
func polymorph_character(marble_item: Dictionary, skill_mastery: int, player_stats: Dictionary) -> Dictionary:
|
|
var vnum := int(marble_item.get("vnum", 0))
|
|
if vnum != VNUM_POLYMORPH_MARBLE:
|
|
return {"ok": false, "reason": "NOT_A_POLYMORPH_MARBLE", "msg": "该物品不是变身宝珠!"}
|
|
|
|
var mob_vnum := int(marble_item.get("mob_vnum", 0))
|
|
if mob_vnum == 0:
|
|
var sockets: Array = marble_item.get("sockets", [])
|
|
if not sockets.is_empty():
|
|
mob_vnum = int(sockets[0])
|
|
|
|
if not MONSTER_POLY_TABLE.has(mob_vnum):
|
|
return {"ok": false, "reason": "INVALID_MONSTER_VNUM", "msg": "未知的变身怪物精魂!"}
|
|
|
|
# 若当前已处于变身状态,先恢复原形
|
|
if is_polymorphed:
|
|
revert_polymorph(player_stats)
|
|
|
|
var mob_info: Dictionary = MONSTER_POLY_TABLE[mob_vnum]
|
|
var duration: float = get_duration_by_mastery(skill_mastery)
|
|
|
|
is_polymorphed = true
|
|
active_mob_vnum = mob_vnum
|
|
active_mob_name = mob_info["name"]
|
|
time_remaining = duration
|
|
total_duration = duration
|
|
active_bonus_type = int(mob_info["bonus_type"])
|
|
active_bonus_val = int(mob_info["bonus_val"])
|
|
active_stat_key = str(mob_info["stat_key"])
|
|
|
|
# 注入变身属性加成
|
|
if not active_stat_key.is_empty() and active_bonus_val > 0:
|
|
player_stats[active_stat_key] = int(player_stats.get(active_stat_key, 0)) + active_bonus_val
|
|
|
|
polymorph_started.emit(active_mob_vnum, active_mob_name, duration, active_bonus_type, active_bonus_val)
|
|
|
|
return {
|
|
"ok": true,
|
|
"mob_vnum": active_mob_vnum,
|
|
"mob_name": active_mob_name,
|
|
"duration": duration,
|
|
"bonus_type": active_bonus_type,
|
|
"bonus_val": active_bonus_val,
|
|
"msg": "怪物精魂附体!成功变身为【%s】,持续 %.0f 秒。" % [active_mob_name, duration]
|
|
}
|
|
|
|
# 解除变身恢复真身原型
|
|
func revert_polymorph(player_stats: Dictionary) -> Dictionary:
|
|
if not is_polymorphed:
|
|
return {"ok": false, "reason": "NOT_POLYMORPHED", "msg": "当前未处于变身状态!"}
|
|
|
|
# 清除施加的加成属性
|
|
if not active_stat_key.is_empty() and active_bonus_val > 0:
|
|
player_stats[active_stat_key] = int(player_stats.get(active_stat_key, 0)) - active_bonus_val
|
|
|
|
var old_mob := active_mob_vnum
|
|
var old_name := active_mob_name
|
|
|
|
is_polymorphed = false
|
|
active_mob_vnum = 0
|
|
active_mob_name = ""
|
|
time_remaining = 0.0
|
|
total_duration = 0.0
|
|
active_bonus_type = POLYMORPH_NO_BONUS
|
|
active_bonus_val = 0
|
|
active_stat_key = ""
|
|
|
|
polymorph_ended.emit(old_mob, old_name)
|
|
|
|
return {
|
|
"ok": true,
|
|
"mob_vnum": old_mob,
|
|
"mob_name": old_name,
|
|
"msg": "变身时效已过,恢复人物原形。"
|
|
}
|
|
|
|
# 逻辑时钟心跳
|
|
func update(delta: float, player_stats: Dictionary) -> void:
|
|
if not is_polymorphed:
|
|
return
|
|
time_remaining -= delta
|
|
if time_remaining <= 0.0:
|
|
revert_polymorph(player_stats)
|
|
|
|
# 40250 行为限制检查:变身期间禁止乘骑坐骑
|
|
func can_mount_horse() -> bool:
|
|
return not is_polymorphed
|
|
|
|
# 40250 行为限制检查:变身期间禁止释放原生职业技能
|
|
func can_cast_player_skills() -> bool:
|
|
return not is_polymorphed
|
|
|
|
# 序列化
|
|
func serialize() -> Dictionary:
|
|
return {
|
|
"is_polymorphed": is_polymorphed,
|
|
"active_mob_vnum": active_mob_vnum,
|
|
"active_mob_name": active_mob_name,
|
|
"time_remaining": time_remaining,
|
|
"total_duration": total_duration,
|
|
"active_bonus_type": active_bonus_type,
|
|
"active_bonus_val": active_bonus_val,
|
|
"active_stat_key": active_stat_key
|
|
}
|
|
|
|
# 反序列化
|
|
func deserialize(data: Dictionary) -> void:
|
|
is_polymorphed = bool(data.get("is_polymorphed", false))
|
|
active_mob_vnum = int(data.get("active_mob_vnum", 0))
|
|
active_mob_name = str(data.get("active_mob_name", ""))
|
|
time_remaining = float(data.get("time_remaining", 0.0))
|
|
total_duration = float(data.get("total_duration", 0.0))
|
|
active_bonus_type = int(data.get("active_bonus_type", POLYMORPH_NO_BONUS))
|
|
active_bonus_val = int(data.get("active_bonus_val", 0))
|
|
active_stat_key = str(data.get("active_stat_key", ""))
|