- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
204 lines
6.8 KiB
GDScript
204 lines
6.8 KiB
GDScript
# classic_pet_system.gd —— Metin2 40250 经典封印宠物跟随与守护系统 1:1
|
|
# 对照 40250 服务端 PetSystem.cpp, PetSystem.h, questlua_pet.cpp
|
|
class_name ClassicPetSystem
|
|
extends RefCounted
|
|
|
|
signal pet_summoned(mob_vnum: int, pet_name: String, buffs: Dictionary)
|
|
signal pet_unsummoned(mob_vnum: int, pet_name: String)
|
|
signal pet_moved(current_pos: Vector2, is_teleport: bool)
|
|
|
|
# 40250 官方经典宠物召唤 Mob Vnum
|
|
const MOB_BABY_BOAR := 34001 # 幼野猪
|
|
const MOB_BABY_WOLF := 34002 # 幼狼
|
|
const MOB_BABY_TIGER := 34003 # 幼虎
|
|
const MOB_BABY_LION := 34004 # 幼狮
|
|
const MOB_BABY_FIREDRAKE := 34005 # 幼火龙
|
|
const MOB_ICE_PHOENIX := 34006 # 冰凤凰
|
|
const MOB_BABY_AZRAEL := 34009 # 幼小阿兹瑞尔
|
|
|
|
# 40250 官方经典封印物品 Vnum
|
|
const SEAL_BABY_BOAR := 53001 # 幼野猪封印
|
|
const SEAL_BABY_FIREDRAKE := 53002 # 幼火龙封印
|
|
const SEAL_ICE_PHOENIX := 53003 # 冰凤凰封印
|
|
const SEAL_BABY_LION := 53004 # 幼狮封印
|
|
const SEAL_BABY_AZRAEL := 53005 # 幼小阿兹瑞尔封印
|
|
|
|
# 40250 AI 跟随与瞬移距离参数
|
|
const MIN_FOLLOW_DIST := 2.0 # 最小跟随停步距离 (米)
|
|
const MAX_TELEPORT_DIST := 15.0 # 最大跟随超距,超过直接瞬移到身边 (米)
|
|
const FOLLOW_SPEED := 6.0 # 跟随移动速度
|
|
|
|
# 40250 宠物封印配置表
|
|
const PET_SEAL_CONFIGS: Dictionary = {
|
|
SEAL_BABY_BOAR: {
|
|
"mob_vnum": MOB_BABY_BOAR,
|
|
"name": "幼野猪",
|
|
"buffs": {"monster_damage_pct": 15},
|
|
"dungeon_only": false
|
|
},
|
|
SEAL_BABY_FIREDRAKE: {
|
|
"mob_vnum": MOB_BABY_FIREDRAKE,
|
|
"name": "幼火龙",
|
|
"buffs": {"max_hp": 1500, "attack_pct": 15},
|
|
"dungeon_only": false
|
|
},
|
|
SEAL_ICE_PHOENIX: {
|
|
"mob_vnum": MOB_ICE_PHOENIX,
|
|
"name": "冰凤凰",
|
|
"buffs": {"max_hp": 1500, "defense_pct": 15},
|
|
"dungeon_only": false
|
|
},
|
|
SEAL_BABY_LION: {
|
|
"mob_vnum": MOB_BABY_LION,
|
|
"name": "幼狮",
|
|
"buffs": {"crit_pct": 10},
|
|
"dungeon_only": false
|
|
},
|
|
SEAL_BABY_AZRAEL: {
|
|
"mob_vnum": MOB_BABY_AZRAEL,
|
|
"name": "小阿兹瑞尔",
|
|
"buffs": {"dungeon_attack_pct": 20, "max_hp": 1000},
|
|
"dungeon_only": true # 40250 源码判定:若不在地牢中,专属地牢增益不生效
|
|
}
|
|
}
|
|
|
|
# 当前宠物出战状态
|
|
var is_summoned: bool = false
|
|
var active_mob_vnum: int = 0
|
|
var active_seal_vnum: int = 0
|
|
var active_pet_name: String = ""
|
|
var active_buffs: Dictionary = {}
|
|
var pet_pos: Vector2 = Vector2.ZERO
|
|
|
|
# 检查物品是否为 40250 宠物封印
|
|
static func is_pet_seal(vnum: int) -> bool:
|
|
return PET_SEAL_CONFIGS.has(vnum)
|
|
|
|
# 获取宠物封印配置
|
|
static func get_pet_config_by_seal(seal_vnum: int) -> Dictionary:
|
|
return PET_SEAL_CONFIGS.get(seal_vnum, {}).duplicate(true)
|
|
|
|
# 召唤经典封印宠物 (40250 CPetSystem::Summon)
|
|
func summon_pet(seal_item: Dictionary, player_stats: Dictionary, is_in_dungeon: bool = false, owner_pos: Vector2 = Vector2.ZERO) -> Dictionary:
|
|
var seal_vnum := int(seal_item.get("vnum", 0))
|
|
if not PET_SEAL_CONFIGS.has(seal_vnum):
|
|
return {"ok": false, "reason": "NOT_A_PET_SEAL", "msg": "该物品不是有效的宠物封印!"}
|
|
|
|
# 若已有宠物出战,先解散旧宠物 (40250 规则:同时只出战一只)
|
|
if is_summoned:
|
|
unsummon_pet(player_stats)
|
|
|
|
var conf: Dictionary = PET_SEAL_CONFIGS[seal_vnum]
|
|
active_mob_vnum = conf["mob_vnum"]
|
|
active_seal_vnum = seal_vnum
|
|
active_pet_name = conf["name"]
|
|
pet_pos = owner_pos + Vector2(1.5, 0.0) # 出生在身侧
|
|
|
|
# 计算注入的 Buff (40250 GiveBuff 规则)
|
|
var buffs_to_apply: Dictionary = {}
|
|
var base_buffs: Dictionary = conf.get("buffs", {})
|
|
var is_dungeon_only: bool = bool(conf.get("dungeon_only", false))
|
|
|
|
for stat_key in base_buffs.keys():
|
|
var val = base_buffs[stat_key]
|
|
# 40250 特殊判定:小阿兹瑞尔等特定宠物仅在地牢中激活特定地牢 Buff
|
|
if stat_key == "dungeon_attack_pct":
|
|
if is_in_dungeon:
|
|
buffs_to_apply[stat_key] = val
|
|
else:
|
|
buffs_to_apply[stat_key] = val
|
|
|
|
# 应用属性加成至玩家
|
|
_apply_buffs_to_player(buffs_to_apply, player_stats, 1)
|
|
active_buffs = buffs_to_apply
|
|
is_summoned = true
|
|
|
|
pet_summoned.emit(active_mob_vnum, active_pet_name, active_buffs)
|
|
|
|
return {
|
|
"ok": true,
|
|
"mob_vnum": active_mob_vnum,
|
|
"pet_name": active_pet_name,
|
|
"buffs": active_buffs,
|
|
"pos": pet_pos,
|
|
"msg": "成功召唤宠物【%s】!守护属性已激活。" % active_pet_name
|
|
}
|
|
|
|
# 召回/解散宠物 (40250 CPetSystem::Unsummon / ClearBuff)
|
|
func unsummon_pet(player_stats: Dictionary) -> Dictionary:
|
|
if not is_summoned:
|
|
return {"ok": false, "reason": "NO_PET_SUMMONED", "msg": "当前没有出战的宠物!"}
|
|
|
|
# 清除施加在玩家身上的属性
|
|
_apply_buffs_to_player(active_buffs, player_stats, -1)
|
|
|
|
var old_mob := active_mob_vnum
|
|
var old_name := active_pet_name
|
|
|
|
is_summoned = false
|
|
active_mob_vnum = 0
|
|
active_seal_vnum = 0
|
|
active_pet_name = ""
|
|
active_buffs.clear()
|
|
pet_pos = Vector2.ZERO
|
|
|
|
pet_unsummoned.emit(old_mob, old_name)
|
|
|
|
return {
|
|
"ok": true,
|
|
"mob_vnum": old_mob,
|
|
"pet_name": old_name,
|
|
"msg": "宠物【%s】已召回,守护属性已解除。" % old_name
|
|
}
|
|
|
|
# 跟随 AI 逻辑步进 (40250 CPetActor::_UpdateFollowAI)
|
|
func update_follow_ai(owner_pos: Vector2, delta: float) -> Dictionary:
|
|
if not is_summoned:
|
|
return {"action": "none"}
|
|
|
|
var dist := pet_pos.distance_to(owner_pos)
|
|
|
|
# 超过最大瞬移距离,直接瞬移回主人身边
|
|
if dist > MAX_TELEPORT_DIST:
|
|
pet_pos = owner_pos + Vector2(1.5, 0.0)
|
|
pet_moved.emit(pet_pos, true)
|
|
return {"action": "teleport", "pos": pet_pos, "dist": dist}
|
|
|
|
# 超过最小跟随距离,平滑朝主人位移
|
|
if dist > MIN_FOLLOW_DIST:
|
|
var move_dist: float = minf(FOLLOW_SPEED * delta, dist - MIN_FOLLOW_DIST)
|
|
pet_pos = pet_pos.move_toward(owner_pos, move_dist)
|
|
pet_moved.emit(pet_pos, false)
|
|
return {"action": "follow", "pos": pet_pos, "dist": pet_pos.distance_to(owner_pos)}
|
|
|
|
# 处于舒适距离,保持待机
|
|
return {"action": "idle", "pos": pet_pos, "dist": dist}
|
|
|
|
# 属性加成注入或反向扣减
|
|
func _apply_buffs_to_player(buffs: Dictionary, player_stats: Dictionary, factor: int) -> void:
|
|
for stat_key in buffs.keys():
|
|
var val = int(buffs[stat_key]) * factor
|
|
var cur = int(player_stats.get(stat_key, 0))
|
|
player_stats[stat_key] = cur + val
|
|
|
|
# 序列化
|
|
func serialize() -> Dictionary:
|
|
return {
|
|
"is_summoned": is_summoned,
|
|
"active_mob_vnum": active_mob_vnum,
|
|
"active_seal_vnum": active_seal_vnum,
|
|
"active_pet_name": active_pet_name,
|
|
"active_buffs": active_buffs.duplicate(true),
|
|
"pos_x": pet_pos.x,
|
|
"pos_y": pet_pos.y
|
|
}
|
|
|
|
# 反序列化
|
|
func deserialize(data: Dictionary) -> void:
|
|
is_summoned = bool(data.get("is_summoned", false))
|
|
active_mob_vnum = int(data.get("active_mob_vnum", 0))
|
|
active_seal_vnum = int(data.get("active_seal_vnum", 0))
|
|
active_pet_name = str(data.get("active_pet_name", ""))
|
|
active_buffs = data.get("active_buffs", {}).duplicate(true)
|
|
pet_pos = Vector2(float(data.get("pos_x", 0.0)), float(data.get("pos_y", 0.0)))
|