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

280 lines
7.7 KiB
GDScript

# mercenary_companion_system.gd —— Metin2 40250 离线单机假人/陪练佣兵助战系统 1:1
# 对照 40250 服务端 char_companion.cpp, party.cpp, affect.h
class_name MercenaryCompanionSystem
extends RefCounted
signal mercenary_summoned(merc_id: int, name: String)
signal mercenary_dismissed()
signal buff_applied_to_player(buff_name: String, duration: float, value: float)
signal heal_applied_to_player(heal_amount: int)
signal mercenary_attacked(target_id: String, damage: int)
signal mercenary_downed()
signal mercenary_revived()
enum MercenaryType {
SHAMAN_HEALER = 1, # 龙神萨满·苏菲 (辅助/治疗)
WARRIOR_TANK = 2 # 近卫战狂·阿雷斯 (肉盾/前锋)
}
enum Stance {
FOLLOW = 0, # 被动跟随
ASSIST = 1, # 协同攻击
STAY = 2 # 原地待命
}
const REVIVE_COOLDOWN: float = 60.0 # 死亡自动复活冷却 (秒)
const TELEPORT_DISTANCE: float = 25.0 # 超出此距离瞬间传送归队
const HEAL_COOLDOWN: float = 10.0 # 治疗术内置冷却
const BUFF_COOLDOWN: float = 30.0 # Buff 持续与刷新周期
const MERCENARY_DATA: Dictionary = {
MercenaryType.SHAMAN_HEALER: {
"name": "龙神萨满·苏菲",
"job": 3,
"base_hp": 8000,
"base_sp": 6000,
"atk": 350,
"def": 280,
"buffs": [
{"name": "巨龙之赐", "type": "def_bonus", "value": 0.20, "duration": 120.0},
{"name": "龙之协助", "type": "crit_bonus", "value": 0.15, "duration": 120.0}
]
},
MercenaryType.WARRIOR_TANK: {
"name": "近卫战狂·阿雷斯",
"job": 0,
"base_hp": 15000,
"base_sp": 2000,
"atk": 650,
"def": 500,
"buffs": [
{"name": "强身战气", "type": "atk_bonus", "value": 0.15, "duration": 120.0},
{"name": "狂暴姿态", "type": "speed_bonus", "value": 0.20, "duration": 120.0}
]
}
}
var active_type: int = 0
var merc_name: String = ""
var level: int = 1
var max_hp: int = 1000
var current_hp: int = 1000
var max_sp: int = 500
var current_sp: int = 500
var atk: int = 200
var def: int = 150
var is_active: bool = false
var is_alive: bool = false
var current_stance: int = Stance.ASSIST
var pos: Vector3 = Vector3.ZERO
# 计时器
var heal_timer: float = 0.0
var buff_timer: float = 0.0
var revive_timer: float = 0.0
# 召唤指定佣兵
func summon_mercenary(merc_type: int, player_pos: Vector3, player_level: int = 50) -> Dictionary:
if not MERCENARY_DATA.has(merc_type):
return {"ok": false, "reason": "INVALID_MERC_TYPE", "msg": "未知的佣兵类型!"}
var cfg = MERCENARY_DATA[merc_type]
active_type = merc_type
merc_name = cfg["name"]
level = player_level
# 等级动态成长数值
var growth_mult = 1.0 + float(level - 1) * 0.05
max_hp = int(float(cfg["base_hp"]) * growth_mult)
current_hp = max_hp
max_sp = int(float(cfg["base_sp"]) * growth_mult)
current_sp = max_sp
atk = int(float(cfg["atk"]) * growth_mult)
def = int(float(cfg["def"]) * growth_mult)
is_active = true
is_alive = true
current_stance = Stance.ASSIST
pos = player_pos + Vector3(-2.0, 0.0, -2.0)
heal_timer = 0.0
buff_timer = 0.0
revive_timer = 0.0
mercenary_summoned.emit(active_type, merc_name)
return {
"ok": true,
"type": active_type,
"name": merc_name,
"level": level,
"hp": current_hp,
"atk": atk,
"def": def,
"pos": pos,
"msg": "佣兵【%s】应召前来助战!" % merc_name
}
# 解散佣兵
func dismiss_mercenary() -> Dictionary:
if not is_active:
return {"ok": false, "reason": "NOT_SUMMONED"}
is_active = false
is_alive = false
active_type = 0
mercenary_dismissed.emit()
return {"ok": true, "msg": "佣兵已暂时离队归营。"}
# 切换战术姿态
func set_stance(stance: int) -> Dictionary:
if not is_active:
return {"ok": false, "reason": "NOT_SUMMONED"}
current_stance = stance
return {"ok": true, "stance": current_stance}
# 帧心跳更新 AI 与协同战斗
func update_mercenary(delta: float, player_pos: Vector3, player_hp: int, player_max_hp: int, target_id: String = "", target_pos: Vector3 = Vector3.ZERO) -> Dictionary:
if not is_active:
return {"status": "inactive"}
# 死亡复活倒计时
if not is_alive:
revive_timer -= delta
if revive_timer <= 0.0:
revive()
return {"status": "just_revived"}
return {"status": "downed", "remaining_revive": revive_timer}
# 计时器更新
if heal_timer > 0.0:
heal_timer = max(0.0, heal_timer - delta)
if buff_timer > 0.0:
buff_timer = max(0.0, buff_timer - delta)
# 1. 距离过远瞬移归队
var dist_to_player = pos.distance_to(player_pos)
if dist_to_player > TELEPORT_DISTANCE:
pos = player_pos + Vector3(-1.5, 0.0, -1.5)
return {"status": "teleported_to_player"}
# 2. 萨满智能辅助:给玩家刷新 Buff
if buff_timer <= 0.0:
buff_timer = BUFF_COOLDOWN
_apply_buffs_to_player()
# 3. 萨满智能救死扶伤:当玩家 HP < 60% 触发急救
if active_type == MercenaryType.SHAMAN_HEALER and heal_timer <= 0.0:
var hp_ratio = float(player_hp) / float(max(1, player_max_hp))
if hp_ratio < 0.60:
heal_timer = HEAL_COOLDOWN
var heal_amt = int(float(player_max_hp) * 0.35)
heal_applied_to_player.emit(heal_amt)
return {
"status": "cast_heal",
"heal_amount": heal_amt,
"msg": "【%s】吟唱巨龙圣歌,为你恢复了 %d 点生命值!" % [merc_name, heal_amt]
}
# 4. 战术行为分流
match current_stance:
Stance.STAY:
return {"status": "staying"}
Stance.FOLLOW:
# 保持紧随玩家身后 2.5 米
if dist_to_player > 3.0:
pos = pos.move_toward(player_pos, 5.0 * delta)
return {"status": "following", "dist": dist_to_player}
return {"status": "standing_by"}
Stance.ASSIST:
# 优先跟随;若有目标则协同集火
if not target_id.is_empty():
var dist_to_tgt = pos.distance_to(target_pos)
var desired_range = 10.0 if active_type == MercenaryType.SHAMAN_HEALER else 2.5
if dist_to_tgt > desired_range:
pos = pos.move_toward(target_pos, 5.5 * delta)
return {"status": "closing_in_target", "target": target_id}
else:
# 施展协同攻击
var dmg = atk
mercenary_attacked.emit(target_id, dmg)
return {
"status": "attacked_target",
"target": target_id,
"damage": dmg
}
else:
if dist_to_player > 3.0:
pos = pos.move_toward(player_pos, 5.0 * delta)
return {"status": "following_idle"}
return {"status": "idle"}
# 给主人施加专属辅助光环
func _apply_buffs_to_player() -> void:
var cfg = MERCENARY_DATA.get(active_type, {})
var buffs: Array = cfg.get("buffs", [])
for b in buffs:
buff_applied_to_player.emit(b["name"], b["duration"], b["value"])
# 佣兵受到伤害
func receive_damage(raw_dmg: int) -> Dictionary:
if not is_active or not is_alive:
return {"ok": false}
var net_dmg = max(1, raw_dmg - def)
current_hp = max(0, current_hp - net_dmg)
if current_hp <= 0:
is_alive = false
revive_timer = REVIVE_COOLDOWN
mercenary_downed.emit()
return {
"ok": true,
"downed": true,
"msg": "佣兵【%s】力竭倒下!将在 60 秒后恢复伤势复活。" % merc_name
}
return {
"ok": true,
"downed": false,
"remaining_hp": current_hp,
"net_dmg": net_dmg
}
# 复活佣兵
func revive(instant: bool = false) -> Dictionary:
if not is_active:
return {"ok": false, "reason": "NOT_SUMMONED"}
is_alive = true
current_hp = int(float(max_hp) * (1.0 if instant else 0.5))
revive_timer = 0.0
mercenary_revived.emit()
return {
"ok": true,
"hp": current_hp,
"msg": "佣兵【%s】重新站起投入战斗!" % merc_name
}
# 获取当前佣兵状态
func get_mercenary_info() -> Dictionary:
return {
"is_active": is_active,
"is_alive": is_alive,
"type": active_type,
"name": merc_name,
"level": level,
"current_hp": current_hp,
"max_hp": max_hp,
"current_sp": current_sp,
"max_sp": max_sp,
"atk": atk,
"def": def,
"stance": current_stance,
"pos": pos,
"revive_timer": revive_timer
}