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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
# 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
|
||||
}
|
||||
Reference in New Issue
Block a user