- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
154 lines
4.0 KiB
GDScript
154 lines
4.0 KiB
GDScript
# pet_companion_system.gd —— Metin2 40250 伴随宠物封印召唤与战力注入系统 1:1
|
|
# 对照 40250 服务端 PetSystem.cpp, char_pet.cpp
|
|
class_name PetCompanionSystem
|
|
extends RefCounted
|
|
|
|
signal pet_summoned(pet_name: String, vnum: int)
|
|
signal pet_dismissed(pet_name: String)
|
|
signal pet_state_changed(state: String)
|
|
|
|
const PET_TABLE: Dictionary = {
|
|
53001: {
|
|
"name": "火凤凰",
|
|
"mob_vnum": 34001,
|
|
"bonuses": {"max_hp": 1500, "monster_dmg_pct": 15},
|
|
"desc": "最大生命 +1500,对怪物伤害提升 15%"
|
|
},
|
|
53002: {
|
|
"name": "幼鹿",
|
|
"mob_vnum": 34002,
|
|
"bonuses": {"max_hp": 1000, "exp_pct": 15},
|
|
"desc": "最大生命 +1000,获得经验值增加 15%"
|
|
},
|
|
53003: {
|
|
"name": "冰凤凰",
|
|
"mob_vnum": 34003,
|
|
"bonuses": {"max_hp": 1500, "def_pct": 15},
|
|
"desc": "最大生命 +1500,物理防御提升 15%"
|
|
},
|
|
53005: {
|
|
"name": "小胖熊猫",
|
|
"mob_vnum": 34005,
|
|
"bonuses": {"max_hp": 1000, "critical_pct": 15},
|
|
"desc": "最大生命 +1000,致命一击几率提升 15%"
|
|
},
|
|
53006: {
|
|
"name": "幼狮",
|
|
"mob_vnum": 34006,
|
|
"bonuses": {"max_hp": 1500, "att_pct": 10},
|
|
"desc": "最大生命 +1500,物理攻击力提升 10%"
|
|
}
|
|
}
|
|
|
|
const FOLLOW_STOP_DIST := 2.0 # 停止距离
|
|
const FOLLOW_RUN_DIST := 3.5 # 开始跟随奔跑距离
|
|
const TELEPORT_DIST := 25.0 # 瞬间拉回距离
|
|
const PET_MOVE_SPEED := 4.5 # 移动速度 (m/s)
|
|
|
|
var is_summoned: bool = false
|
|
var current_seal_vnum: int = 0
|
|
var active_pet: Dictionary = {}
|
|
var pet_position: Vector3 = Vector3.ZERO
|
|
var pet_state: String = "wait" # "wait" 或 "run"
|
|
var remaining_time: float = 0.0
|
|
|
|
# 召唤 / 收回宠物 (右键点击宠物封印石)
|
|
func toggle_pet_seal(seal_item: Dictionary, player_pos: Vector3) -> Dictionary:
|
|
var vnum: int = int(seal_item.get("vnum", 0))
|
|
if not PET_TABLE.has(vnum):
|
|
return {"ok": false, "reason": "NOT_A_PET_SEAL", "msg": "这不是有效的宠物封印!"}
|
|
|
|
# 若当前已召唤同一只宠物,则收回
|
|
if is_summoned and current_seal_vnum == vnum:
|
|
var pet_name = active_pet.get("name", "宠物")
|
|
dismiss_pet()
|
|
return {
|
|
"ok": true,
|
|
"action": "dismiss",
|
|
"pet_name": pet_name,
|
|
"msg": "已将【%s】收回封印石。" % pet_name
|
|
}
|
|
|
|
# 若当前召唤了不同宠物,先收回
|
|
if is_summoned:
|
|
dismiss_pet()
|
|
|
|
# 召唤新宠物
|
|
var cfg: Dictionary = PET_TABLE[vnum]
|
|
current_seal_vnum = vnum
|
|
is_summoned = true
|
|
remaining_time = float(seal_item.get("duration", 86400.0)) # 默认 24 小时
|
|
pet_position = player_pos + Vector3(-1.5, 0.0, -1.5)
|
|
pet_state = "wait"
|
|
|
|
active_pet = {
|
|
"vnum": vnum,
|
|
"name": cfg["name"],
|
|
"mob_vnum": cfg["mob_vnum"],
|
|
"bonuses": cfg["bonuses"].duplicate()
|
|
}
|
|
|
|
pet_summoned.emit(cfg["name"], vnum)
|
|
|
|
return {
|
|
"ok": true,
|
|
"action": "summon",
|
|
"pet_name": cfg["name"],
|
|
"bonuses": cfg["bonuses"],
|
|
"msg": "伴随宠物【%s】已召唤出战!" % cfg["name"]
|
|
}
|
|
|
|
# 收回当前宠物
|
|
func dismiss_pet() -> void:
|
|
if not is_summoned:
|
|
return
|
|
|
|
var pname = active_pet.get("name", "")
|
|
is_summoned = false
|
|
current_seal_vnum = 0
|
|
active_pet.clear()
|
|
pet_state = "wait"
|
|
|
|
pet_dismissed.emit(pname)
|
|
|
|
# 获取当前宠物注入的战力属性
|
|
func get_active_bonuses() -> Dictionary:
|
|
if not is_summoned:
|
|
return {}
|
|
return active_pet.get("bonuses", {}).duplicate()
|
|
|
|
# 3D 伴随移动逻辑
|
|
func update_movement(player_pos: Vector3, dt: float) -> void:
|
|
if not is_summoned:
|
|
return
|
|
|
|
var dist: float = pet_position.distance_to(player_pos)
|
|
|
|
# 超远距离瞬间传回身边
|
|
if dist >= TELEPORT_DIST:
|
|
pet_position = player_pos + Vector3(-1.5, 0.0, -1.5)
|
|
pet_state = "wait"
|
|
pet_state_changed.emit("wait")
|
|
return
|
|
|
|
# 跟随靠近
|
|
if dist > FOLLOW_STOP_DIST:
|
|
var dir := (player_pos - pet_position).normalized()
|
|
pet_position += dir * PET_MOVE_SPEED * dt
|
|
if pet_state != "run":
|
|
pet_state = "run"
|
|
pet_state_changed.emit("run")
|
|
else:
|
|
if pet_state != "wait":
|
|
pet_state = "wait"
|
|
pet_state_changed.emit("wait")
|
|
|
|
# 逐帧倒计时衰减
|
|
func update(dt: float) -> void:
|
|
if not is_summoned:
|
|
return
|
|
|
|
remaining_time -= dt
|
|
if remaining_time <= 0.0:
|
|
dismiss_pet()
|