# 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 }