- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
280 lines
7.7 KiB
GDScript
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
|
|
}
|