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,175 @@
|
||||
# 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}
|
||||
Reference in New Issue
Block a user