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

176 lines
5.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# auto_combat_bot_system.gd —— Metin2 40250 自动战斗猎杀挂机助手 1:1
# 对照 40250 服务端 char_battle.cpp, bot.cpp, item_manager.cpp
class_name AutoCombatBotSystem
extends RefCounted
signal bot_toggled(enabled: bool)
signal potion_used(potion_vnum: int, ptype: String)
signal target_locked(target_id: String)
signal auto_attack_fired(target_id: String, damage: int)
signal auto_skill_fired(skill_vnum: int, target_id: String)
signal auto_loot_collected(item_name: String, slot: int)
# 红药与蓝药 VNUM
const HP_POTION_VNUMS: Array = [27001, 27002, 27003] # 小/中/大红
const SP_POTION_VNUMS: Array = [27004, 27005, 27006] # 小/中/大蓝
var is_enabled: bool = false
var auto_potion_enabled: bool = true
var hp_threshold: float = 0.50 # 50% HP 触发喝红
var sp_threshold: float = 0.40 # 40% SP 触发喝蓝
var auto_attack_enabled: bool = true
var scan_radius: float = 15.0
var attack_range: float = 3.0
var auto_skill_enabled: bool = true
var assigned_skills: Array = []
var skill_cooldowns: Dictionary = {}
var auto_loot_enabled: bool = true
var loot_radius: float = 8.0
var current_target_id: String = ""
var potion_cooldown: float = 0.0
# 开启/关闭挂机内挂
func toggle_bot(enable: bool) -> Dictionary:
is_enabled = enable
if not is_enabled:
current_target_id = ""
bot_toggled.emit(is_enabled)
return {"ok": true, "enabled": is_enabled, "msg": "自动战斗猎杀助手已%s" % ("开启" if is_enabled else "关闭")}
# 帧心跳驱动挂机逻辑
func update_bot(delta: float, player_data: Dictionary, inventory: Array, nearby_mobs: Array, nearby_drops: Array) -> Dictionary:
if not is_enabled:
return {"status": "disabled"}
if potion_cooldown > 0.0:
potion_cooldown = max(0.0, potion_cooldown - delta)
for sk in skill_cooldowns.keys():
if skill_cooldowns[sk] > 0.0:
skill_cooldowns[sk] = max(0.0, skill_cooldowns[sk] - delta)
var p_pos: Vector3 = player_data.get("pos", Vector3.ZERO)
var p_hp: int = int(player_data.get("hp", 100))
var p_max_hp: int = max(1, int(player_data.get("max_hp", 100)))
var p_sp: int = int(player_data.get("sp", 100))
var p_max_sp: int = max(1, int(player_data.get("max_sp", 100)))
# 1. 自动智能喝药判定
if auto_potion_enabled and potion_cooldown <= 0.0:
if float(p_hp) / float(p_max_hp) <= hp_threshold:
if _use_potion(HP_POTION_VNUMS, inventory, player_data, "hp"):
potion_cooldown = 1.0 # 喝药公共冷却 1 秒
if float(p_sp) / float(p_max_sp) <= sp_threshold:
if _use_potion(SP_POTION_VNUMS, inventory, player_data, "sp"):
potion_cooldown = 1.0
# 2. 自动拾取掉落物
if auto_loot_enabled:
_process_auto_loot(p_pos, nearby_drops, inventory)
# 3. 自动搜敌与战斗打击
if auto_attack_enabled:
var act = _process_auto_combat(delta, p_pos, player_data, nearby_mobs)
return act
return {"status": "idle"}
# 自动喝药
func _use_potion(potion_vnums: Array, inventory: Array, player_data: Dictionary, ptype: String) -> bool:
for i in range(inventory.size()):
if inventory[i] != null and int(inventory[i].get("vnum", 0)) in potion_vnums:
var pot = inventory[i]
var pvnum = int(pot["vnum"])
var cnt = int(pot.get("count", 1))
if cnt > 1:
pot["count"] = cnt - 1
else:
inventory[i] = null
var rec = 1500 if pvnum in [27003, 27006] else 800
if ptype == "hp":
player_data["hp"] = min(int(player_data["max_hp"]), int(player_data["hp"]) + rec)
else:
player_data["sp"] = min(int(player_data["max_sp"]), int(player_data["sp"]) + rec)
potion_used.emit(pvnum, ptype)
return true
return false
# 自动拾取
func _process_auto_loot(player_pos: Vector3, drops: Array, inventory: Array) -> void:
for d in drops:
if d.get("looted", false):
continue
var d_pos: Vector3 = d.get("pos", Vector3.ZERO)
if player_pos.distance_to(d_pos) <= loot_radius:
# 放入背包
for i in range(inventory.size()):
if inventory[i] == null:
inventory[i] = {
"vnum": d.get("vnum", 1),
"name": d.get("name", "战利品"),
"count": int(d.get("count", 1))
}
d["looted"] = true
auto_loot_collected.emit(d["name"], i)
break
# 自动锁敌与攻击
func _process_auto_combat(delta: float, player_pos: Vector3, player_data: Dictionary, mobs: Array) -> Dictionary:
var closest_mob = null
var min_dist = scan_radius + 1.0
for m in mobs:
if not m.get("alive", true):
continue
var m_pos: Vector3 = m.get("pos", Vector3.ZERO)
var dist = player_pos.distance_to(m_pos)
if dist <= scan_radius and dist < min_dist:
min_dist = dist
closest_mob = m
if closest_mob == null:
current_target_id = ""
return {"status": "no_targets"}
var target_id = str(closest_mob.get("id", "mob"))
if current_target_id != target_id:
current_target_id = target_id
target_locked.emit(target_id)
var target_pos: Vector3 = closest_mob.get("pos", Vector3.ZERO)
var dist_to_target = player_pos.distance_to(target_pos)
# 走位靠拢
if dist_to_target > attack_range:
var new_pos = player_pos.move_toward(target_pos, 5.0 * delta)
player_data["pos"] = new_pos
return {"status": "moving_to_target", "target": target_id, "dist": dist_to_target}
# 尝试释放技能
if auto_skill_enabled:
for sk in assigned_skills:
if skill_cooldowns.get(sk, 0.0) <= 0.0:
skill_cooldowns[sk] = 8.0 # 默认技能 8 秒冷却
auto_skill_fired.emit(sk, target_id)
var sk_dmg = int(float(player_data.get("atk", 500)) * 2.0)
closest_mob["hp"] = max(0, int(closest_mob.get("hp", 1000)) - sk_dmg)
if closest_mob["hp"] <= 0:
closest_mob["alive"] = false
return {"status": "skill_attack", "skill": sk, "damage": sk_dmg}
# 普通挥砍攻击
var atk_dmg = int(player_data.get("atk", 500))
closest_mob["hp"] = max(0, int(closest_mob.get("hp", 1000)) - atk_dmg)
if closest_mob["hp"] <= 0:
closest_mob["alive"] = false
auto_attack_fired.emit(target_id, atk_dmg)
return {"status": "basic_attack", "target": target_id, "damage": atk_dmg}