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

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)))