Python Apex Legends 武器自动识别与压枪 全过程记录
文章目录
本文为下面参考文章的学习与实践
环境准备
conda create -n apex python=3.9
操纵键鼠
由于绝地求生屏蔽了硬件驱动外的其他鼠标输入,因此我们无法直接通过py脚本来控制游戏内鼠标操作。为了实现游戏内的鼠标下移,我使用了罗技鼠标的驱动(ghub),而py通过调用ghub的链接库文件,将指令操作传递给ghub,最终实现使用硬件驱动的鼠标指令输入给游戏,从而绕过游戏的鼠标输入限制。值得一提的是,我们只是通过py代码调用链接库的接口将指令传递给罗技驱动的,跟实际使用的是何种鼠标没有关系,所以即便用户使用的是雷蛇、卓威、双飞燕等鼠标,对下面的代码并无任何影响。
驱动安装 链接库加载 代码准备和游戏外测试
罗技驱动使用 LGS_9.02.65_X64(请自行找资源安装,官网新版罗技驱动没找到对应的链接库文件),链接库文件在项目链接里面可以找到。下面是载入链接库的代码。
罗技驱动分LGS(老)和GHub(新), 必须装指定版本的LGS驱动(如已安装GHub可能需要卸载), 不然要么报未安装, 要么初始化成功但调用无效
try:
gm = CDLL(r'./ghub_device.dll')
gmok = gm.device_open() == 1
if not gmok:
print('未安装ghub或者lgs驱动!!!')
else:
print('初始化成功!')
except FileNotFoundError:
print('缺少文件')
装了该驱动后, 无需重启电脑, 当下就生效了. 遗憾的是, 没有对应的文档, 只能猜测参数了
toolkit.py
import time
from ctypes import CDLL
import win32api # conda install pywin32
try:
driver = CDLL(r'mouse.device.lgs.dll') # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
ok = driver.device_open() == 1
if not ok:
print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
print('初始化失败, 缺少文件')
class Mouse:
@staticmethod
def move(x, y, absolute=False):
if ok:
mx, my = x, y
if absolute:
ox, oy = win32api.GetCursorPos()
mx = x - ox
my = y - oy
driver.moveR(mx, my, True)
@staticmethod
def down(code):
if ok:
driver.mouse_down(code)
@staticmethod
def up(code):
if ok:
driver.mouse_up(code)
@staticmethod
def click(code):
"""
:param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
:return:
"""
if ok:
driver.mouse_down(code)
driver.mouse_up(code)
class Keyboard:
@staticmethod
def press(code):
if ok:
driver.key_down(code)
@staticmethod
def release(code):
if ok:
driver.key_up(code)
@staticmethod
def click(code):
"""
:param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
:return:
"""
if ok:
driver.key_down(code)
driver.key_up(code)
游戏内测试
在游戏里面试过后, 管用, 但是不准, 猜测可能和游戏内鼠标灵敏度/FOV等有关系
from toolkit import Mouse
import pynput # conda install pynput
def onClick(x, y, button, pressed):
if not pressed:
if pynput.mouse.Button.x2 == button:
Mouse.move(100, 100)
mouseListener = pynput.mouse.Listener(on_click=onClick)
mouseListener.start()
mouseListener.join()
键鼠监听
前面说到,要实现压枪就要对各种配件、状态做出识别。那么在写识别的函数之前,我们先要解决的是何时识别的问题。如果识别使用多线程\多进程的一直持续检测,无疑是一种巨大的开销,因此就需要对键盘、鼠标的状态进行监听。只有按下特定按键时,才触发特定相应的识别请求。
这里我使用的钩子是Pynput,其他可使用的库还有Pyhook3
def onClick(x, y, button, pressed):
print(f'button {button} {"pressed" if pressed else "released"} at ({x},{y})')
if pynput.mouse.Button.left == button:
return False # 正常不要返回False, 这样会结束监听并停止监听线程, 在关闭程序前返回False就好了
listener = pynput.mouse.Listener(on_click=onClick)
listener.start()
def onRelease(key):
print(f'{key} released')
if key == pynput.keyboard.Key.end:
return False # 正常不要返回False, 这样会结束监听并停止监听线程, 在关闭程序前返回False就好了
listener = pynput.keyboard.Listener(on_release=onRelease)
listener.start()
注意调试回调方法的时候, 不要打断点, 不要打断点, 不要打断点, 这样会卡死IO, 导致鼠标键盘失效
Listener中绑定on_press和on_release的函数( on_key_press、on_key_release),它们返回False的时候是结束监听,下文鼠标监听的函数同理,所以不要随便返回False
键盘的特殊按键采用keyboard.Key.tab这种写法,普通按键用keyboard.KeyCode.from_char(‘c’)这种写法
有些键不知道该怎么写, 可以 print(key)
查看写法
这里有一点非常坑,on_press和on_release的参数只能有一个key,这个key就是对应键盘按下的哪颗按键。但这是不足以满足我们的需求的,因为我们应该在钩子函数内部,在按下指定按键时对信号量做出修改,但因为参数的限制,我们无法把信号量传进函数内部,这里我也是想了很久,最后才想到用嵌套函数的写法解决这个问题。
另外,钩子函数本身是阻塞的。也就是说钩子函数在执行的过程中,用户正常的键盘/鼠标操作是无法输入的。所以在钩子函数里面必须写成有限的操作(即O(1)时间复杂度的代码),也就是说像背包内配件及枪械识别,还有下文会讲到的鼠标压枪这类时间开销比较大或者持续时间长的操作,都不适合写在钩子函数里面。这也解释了为什么在检测到Tab(打开背包)、鼠标左键按下时,为什么只是改变信号量,然后把这些任务丢给别的进程去做的原因。
武器识别
如何简单且高效判断是否在游戏内
找几个特征点取色判断, 血条左上角和生存物品框左下角
一般能用于取色的点, 它的颜色RGB都是相同的, 这种点的颜色非常稳定
我原本以为屏幕点取色应该不会超过1ms的耗时, 结果万万没想到, 取个色居然要1-10ms, 效率奇低, 暂无其他优雅方法
如何简单且高效判断背包状态 无武器/1号武器/2号武器
看武器边框上红色圈住的部分颜色, 灰色说明没有武器, 上下不同色说明使用2号武器, 上下同色说明使用1号武器
如何简单且高效判断武器子弹类别
因为不同子弹类型的武器, 边框颜色不一样. 所以可以和上面的放在一起, 同一个点直接判断出背包状态和子弹类别
如何简单且高效判断武器名称
在分类后的基础上, 通过 背包状态 确定要检查颜色的位置(1号位/2号位), 通过 武器子弹类别 缩小判断范围, 在每个武器的名字上找一个纯白色的点, 确保这个点只有这把武器是纯白色, 然后逐个对比
如何简单且高效判断武器模式 全自动/连发/单发
需要压枪的只有全自动和半自动两种模式的武器, 单发不需要压枪(后面有可能做自动单发, 到时候在考虑), 喷子和狙不需要压枪
所以需要找一个能区分三种模式的点(不同模式这个点的颜色不同但是稳定), 且这个点不能受和平和三重的特殊标记影响
一开始找了个不是纯白色的点, 后来发现这个点的颜色会被背景颜色影响到, 不是恒定不变的. 最终还是放弃了一个点即可分辨模式的想法, 采用了稳妥的纯白色点, 保证该点只在该模式下是纯白色的, 在其他模式都不是纯白色即可
收起武器, 部分武器可以通过[V]标判断, 放弃
何时触发识别
- 鼠标右键 按下, 识别武器. 和游戏内原本的按键功能不冲突
- 1/2/3/E/V/Tab/Esc/Alt 键释放, 识别武器
- Home 键释放, 切换开关
- end 键释放, 结束程序
压枪思路
apex 的压枪有3个思路, 因为 apex 不同武器的弹道貌似是固定的, 没有随机值?, 其他游戏也是??
- 左右抖动抵消水平后坐力, 下拉抵消垂直后坐力. 这种方法简单, 但是画面会抖动, 效果也不是很好
- 根据武器配件等测试不同情况下的武器后坐力数据, 然后做反向抵消.
可以通过取巧的方式, 只做无配件状态下的反向抵消, 还省了找配件的麻烦
这种方法太难太麻烦了, 但是做的好的话, 基本一条线, 强的离谱 - 还有就是现在很火的AI目标检测(yolov5), 我也有尝试做, 环境搭好了, 但是中途卡住了. 一是毕竟python是兴趣, 很多基础不到位, 相关专业知识更是空白, 参考内容也参差不齐, 导致对检测和训练的参数都很模糊. 二是数据集采集, 网上找了些, 自己做了些, 但是任然只有一点点, 不着急, 慢慢找吧. 据说要想效果好, 得几千张图片集 …
我先试试 抖枪大法
组织数据
武器数据, 通过子弹类型分组, 组里的每个成员指定序号, 名称, 压枪参数等信息
配置数据, 按分辨率分组, 再按是否在游戏中, 是否有武器, 武器位置, 武器子弹类型, 武器索引等信息分类
信号数据, 程序运行时, 进程线程间通讯
第一阶段实现 能自动识别出所有武器
目前测试下来, 一波识别大概六七十毫秒的样子, 最多也不会超过一百毫秒, 主要耗时在取色函数(1-10ms), 性能已经够用了
我的配置: AMD R7 2700x, Nvidia RTX 2080, 3440*1440 分辨率
cfg.py
mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack' # 背包
color = 'color'
point = 'point'
index = 'index'
bullet = 'bullet' # 子弹
differ = 'differ'
positive = 'positive' # 肯定的
negative = 'negative' # 否定的
# 检测数据
detect = {
"3440:1440": {
game: [ # 判断是否在游戏中
{
point: (236, 1344), # 点的坐标, 血条左上角
color: 0x00FFFFFF # 点的颜色, 255, 255, 255
},
{
point: (2692, 1372), # 生存物品右下角
color: 0x959595 # 149, 149, 149
}
],
pack: { # 背包状态, 有无武器, 选择的武器
point: (2900, 1372), # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
color: 0x808080, # 无武器时, 灰色, 128, 128, 128
'0x447bb4': 1, # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
'0x839b54': 2, # 重型弹药武器
'0x3da084': 3, # 能量弹药武器
'0xce5f6e': 4, # 狙击弹药武器
'0xf339b': 5, # 霰弹枪弹药武器
'0x5302ff': 6, # 空投武器
},
mode: { # 武器模式, 全自动/半自动/单发/其他
point: (3148, 1349),
'0xf8f8f8': 1, # 全自动
'0xfefefe': 2 # 半自动
},
name: { # 武器名称判断
color: 0x00FFFFFF,
'1': { # 1号武器
'1': [ # 轻型弹药武器
(2959, 1386), # 1: RE-45 自动手枪
(2970, 1385), # 2: 转换者冲锋枪
(2972, 1386), # 3: R-301 卡宾枪
(2976, 1386), # 4: R-99 冲锋枪
(2980, 1386), # 5: P2020 手枪
(2980, 1384), # 6: 喷火轻机枪
(2987, 1387), # 7: G7 侦查枪
(3015, 1386), # 8: CAR (轻型弹药)
],
'2': [ # 重型弹药武器
(2957, 1385), # 1: 赫姆洛克突击步枪
(2982, 1385), # 2: 猎兽冲锋枪
(2990, 1393), # 3: 平行步枪
(3004, 1386), # 4: 30-30
(3015, 1386), # 5: CAR (重型弹药)
],
'3': [ # 能量弹药武器
(2955, 1386), # 1: L-STAR能量机枪
(2970, 1384), # 2: 三重式狙击枪
(2981, 1385), # 3: 电能冲锋枪
(2986, 1384), # 4: 专注轻机枪
(2980, 1384), # 5: 哈沃克步枪
],
'4': [ # 狙击弹药武器
(2969, 1395), # 1: 哨兵狙击步枪
(2999, 1382), # 2: 充能步枪
(2992, 1385), # 3: 辅助手枪
(3016, 1383), # 4: 长弓
],
'5': [ # 霰弹枪弹药武器
(2957, 1384), # 1: 和平捍卫者霰弹枪
(2995, 1382), # 2: 莫桑比克
(3005, 1386), # 3: EVA-8
],
'6': [ # 空投武器
(2958, 1384), # 1: 克雷贝尔狙击枪
(2983, 1384), # 2: 敖犬霰弹枪
(3003, 1383), # 3: 波塞克
(3014, 1383), # 4: 暴走
]
},
'2': {
differ: 195 # 直接用1的坐标, 横坐标右移195就可以了
}
}
},
"2560:1440": {
},
"2560:1080": {
},
"1920:1080": {
}
}
# 武器数据
weapon = {
'1': { # 轻型弹药武器
'1': {
name: 'RE-45 自动手枪',
},
'2': {
name: '转换者冲锋枪',
},
'3': {
name: 'R-301 卡宾枪',
},
'4': {
name: 'R-99 冲锋枪',
},
'5': {
name: 'P2020 手枪',
},
'6': {
name: '喷火轻机枪',
},
'7': {
name: 'G7 侦查枪',
},
'8': {
name: 'CAR (轻型弹药)',
}
},
'2': { # 重型弹药武器
'1': {
name: '赫姆洛克突击步枪',
},
'2': {
name: '猎兽冲锋枪',
},
'3': {
name: '平行步枪',
},
'4': {
name: '30-30',
},
'5': {
name: 'CAR (重型弹药)',
}
},
'3': { # 能量弹药武器
'1': {
name: 'L-STAR能量机枪',
},
'2': {
name: '三重式狙击枪',
},
'3': {
name: '电能冲锋枪',
},
'4': {
name: '专注轻机枪',
},
'5': {
name: '哈沃克步枪',
},
},
'4': { # 狙击弹药武器
'1': {
name: '哨兵狙击步枪',
},
'2': {
name: '充能步枪',
},
'3': {
name: '辅助手枪',
},
'4': {
name: '长弓',
},
},
'5': { # 霰弹弹药武器
'1': {
name: '和平捍卫者霰弹枪',
},
'2': {
name: '莫桑比克',
},
'3': {
name: 'EVA-8',
},
},
'6': { # 空投武器
'1': {
name: '克雷贝尔狙击枪',
},
'2': {
name: '敖犬霰弹枪',
},
'3': {
name: '波塞克',
},
'4': {
name: '暴走',
},
}
}
toolkit.py
import mss # pip install mss
import ctypes
from ctypes import CDLL
import cfg
from cfg import detect, weapon
# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
hdc = user32.GetDC(None)
try:
driver = CDLL(r'mouse.device.lgs.dll') # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
ok = driver.device_open() == 1
if not ok:
print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
print('初始化失败, 缺少文件')
class Mouse:
@staticmethod
def point():
return user32.GetCursorPos()
@staticmethod
def move(x, y, absolute=False):
if ok:
mx, my = x, y
if absolute:
ox, oy = user32.GetCursorPos()
mx = x - ox
my = y - oy
driver.moveR(mx, my, True)
@staticmethod
def moveHumanoid(x, y, absolute=False):
"""
仿真移动(还没做好)
"""
if ok:
ox, oy = user32.GetCursorPos() # 原鼠标位置
mx, my = x, y # 相对移动距离
if absolute:
mx = x - ox
my = y - oy
tx, ty = ox + mx, oy + my
print(f'({ox},{oy}), ({tx},{ty}), x:{mx},y:{my}')
# 以绝对位置方式移动(防止相对位置丢失精度)
adx, ady = abs(mx), abs(my)
if adx 0 else -i
temp = int(ady / adx * abs(ix))
iy = temp if my > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
else:
# 垂直方向移动的距离短
for i in range(1, ady):
iy = i if my > 0 else -i
temp = int(adx / ady * abs(iy))
ix = temp if mx > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
@staticmethod
def down(code):
if ok:
driver.mouse_down(code)
@staticmethod
def up(code):
if ok:
driver.mouse_up(code)
@staticmethod
def click(code):
"""
:param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
:return:
"""
if ok:
driver.mouse_down(code)
driver.mouse_up(code)
class Keyboard:
@staticmethod
def press(code):
if ok:
driver.key_down(code)
@staticmethod
def release(code):
if ok:
driver.key_up(code)
@staticmethod
def click(code):
"""
键盘按键函数中,传入的参数采用的是键盘按键对应的键码
:param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
:return:
"""
if ok:
driver.key_down(code)
driver.key_up(code)
class Monitor:
"""
显示器
"""
sct = mss.mss()
@staticmethod
def grab(region):
"""
region: tuple, (left, top, width, height)
pip install mss
"""
left, top, width, height = region
return Monitor.sct.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})
@staticmethod
def pixel(x, y):
"""
效率很低且不稳定, 单点检测都要耗时1-10ms
获取颜色, COLORREF 格式, 0x00FFFFFF
结果是int,
可以通过 print(hex(color)) 查看十六进制值
可以通过 print(color == 0x00FFFFFF) 进行颜色判断
"""
# hdc = user32.GetDC(None)
return gdi32.GetPixel(hdc, x, y)
class Resolution:
"""
分辨率
"""
@staticmethod
def display():
"""
显示分辨率
"""
w = user32.GetSystemMetrics(0)
h = user32.GetSystemMetrics(1)
return w, h
@staticmethod
def virtual():
"""
多屏幕组合的虚拟显示器分辨率
"""
w = user32.GetSystemMetrics(78)
h = user32.GetSystemMetrics(79)
return w, h
@staticmethod
def physical():
"""
物理分辨率
"""
# hdc = user32.GetDC(None)
w = gdi32.GetDeviceCaps(hdc, 118)
h = gdi32.GetDeviceCaps(hdc, 117)
return w, h
class Game:
"""
游戏工具
"""
@staticmethod
def game():
"""
是否在游戏内
太耗时了, 所以不能调的多了
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.game)
for item in data:
x, y = item.get(cfg.point)
if Monitor.pixel(x, y) != item.get(cfg.color):
return False
return True
@staticmethod
def index():
"""
武器索引和子弹类型索引
:return: 武器位索引, 1:1号位, 2:2号位, None:无武器, 拳头(这个暂时无法判断)
子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.pack)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
if data.get(cfg.color) == color:
return None, None
else:
bullet = data.get(hex(color))
return (1, bullet) if color == Monitor.pixel(x, y + 1) else (2, bullet)
@staticmethod
def weapon(index, bullet):
"""
通过武器位和子弹类型识别武器, 参考:config.detect.name
:param index: 武器位, 1:1号位, 2:2号位
:param bullet: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
:return:
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.name)
color = data.get(cfg.color)
if index == 1:
lst = data.get(str(index)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x, y):
return i + 1
elif index == 2:
differ = data.get(str(index)).get(cfg.differ)
lst = data.get(str(1)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x + differ, y):
return i + 1
return None
@staticmethod
def mode():
"""
武器模式
:return: 1:全自动, 2:半自动, None:其他
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.mode)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
return data.get(hex(color))
@staticmethod
def detect():
"""
决策是否需要压枪, 向信号量写数据
"""
if Game.game() is False:
print('not in game')
return
index, bullet = Game.index()
if (index is None) | (bullet is None):
print('no weapon')
return
if Game.mode() is None:
print('not in full auto or semi auto mode')
return
arms = Game.weapon(index, bullet)
if arms is None:
print('detect weapon failure')
return
# 检测通过, 需要压枪
print(weapon.get(str(bullet)).get(str(arms)).get(cfg.name))
return weapon.get(str(bullet)).get(str(arms)).get(cfg.name)
apex.py
import time
import pynput # conda install pynput
import toolkit
ExitFlag = False
def down(x, y, button, pressed):
global ExitFlag
if ExitFlag:
print(ExitFlag)
return False # 结束监听线程
if pressed: # 按下
if pynput.mouse.Button.right == button:
toolkit.Game.detect()
mouseListener = pynput.mouse.Listener(on_click=down)
mouseListener.start()
def release(key):
if key == pynput.keyboard.Key.end:
print('end')
global ExitFlag
ExitFlag = True
return False
if key == pynput.keyboard.KeyCode.from_char('1'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('2'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('3'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('e'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('v'):
toolkit.Game.detect()
keyboardListener = pynput.keyboard.Listener(on_release=release)
keyboardListener.start()
keyboardListener.join()
第二阶段实现 能自动识别出所有武器并采用对应抖枪参数执行压枪
- 游戏内鼠标灵敏度越高越容易抖枪且效果更好, 但是开到5的话, 会感到有点晕
- 游戏内鼠标灵敏度越高, 代码里抖动的像素就需要设置的更小, 比如5的灵敏度, 抖动2像素就可以了
- 抖枪能减小后坐力, 但不能完全消除, 所以还需配合对应方向的移动
在2.5灵敏度下, 301使用下面这个参数, 二三十米还行, 五十米, 三倍效果还将就一倍就很差了. 后坐力越大的武器, 前几枪容易跳太高, 下压力度可以大点
还有就是延迟要低一点, 我这边裸连延迟300+, 经常子弹打出去, 过半秒才减血, 这样很难测的准
能量武器, 专注和哈沃克, 预热和涡轮有很大影响, 这里没管, 将就将就
total = 0 # 总计时 ms
delay = 1 # 延迟 ms
pixel = 4 # 抖动像素
while True:
if not data[fire]:
break
# 下压
if total < 30:
toolkit.Mouse.move(0, 5)
time.sleep(delay / 1000)
total += delay
else:
toolkit.Mouse.move(0, 1)
time.sleep(delay / 1000)
total += delay
# 抖枪
toolkit.Mouse.move(pixel, 0)
time.sleep(delay / 1000)
total += delay
toolkit.Mouse.move(-pixel, 0)
time.sleep(delay / 1000)
total += delay
cfg.py
mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack' # 背包
color = 'color'
point = 'point'
index = 'index'
shake = 'shake'
speed = 'speed'
count = 'count'
switch = 'switch'
bullet = 'bullet' # 子弹
differ = 'differ'
suppress = 'suppress'
strength = 'strength'
positive = 'positive' # 肯定的
negative = 'negative' # 否定的
# 检测数据
detect = {
"3440:1440": {
game: [ # 判断是否在游戏中
{
point: (236, 1344), # 点的坐标, 血条左上角
color: 0x00FFFFFF # 点的颜色, 255, 255, 255
},
{
point: (2692, 1372), # 生存物品右下角
color: 0x959595 # 149, 149, 149
}
],
pack: { # 背包状态, 有无武器, 选择的武器
point: (2900, 1372), # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
color: 0x808080, # 无武器时, 灰色, 128, 128, 128
'0x447bb4': 1, # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
'0x839b54': 2, # 重型弹药武器
'0x3da084': 3, # 能量弹药武器
'0xce5f6e': 4, # 狙击弹药武器
'0xf339b': 5, # 霰弹枪弹药武器
'0x5302ff': 6, # 空投武器
},
mode: { # 武器模式, 全自动/半自动/单发/其他
color: 0x00FFFFFF,
'1': (3151, 1347), # 全自动
'2': (3171, 1351), # 半自动
},
name: { # 武器名称判断
color: 0x00FFFFFF,
'1': { # 1号武器
'1': [ # 轻型弹药武器
(2959, 1386), # 1: RE-45 自动手枪
(2970, 1385), # 2: 转换者冲锋枪
(2972, 1386), # 3: R-301 卡宾枪
(2976, 1386), # 4: R-99 冲锋枪
(2980, 1386), # 5: P2020 手枪
(2980, 1384), # 6: 喷火轻机枪
(2987, 1387), # 7: G7 侦查枪
(3015, 1386), # 8: CAR (轻型弹药)
],
'2': [ # 重型弹药武器
(2957, 1385), # 1: 赫姆洛克突击步枪
(2982, 1385), # 2: 猎兽冲锋枪
(2990, 1393), # 3: 平行步枪
(3004, 1386), # 4: 30-30
(3015, 1386), # 5: CAR (重型弹药)
],
'3': [ # 能量弹药武器
(2955, 1386), # 1: L-STAR能量机枪
(2970, 1384), # 2: 三重式狙击枪
(2981, 1385), # 3: 电能冲锋枪
(2986, 1384), # 4: 专注轻机枪
(2980, 1384), # 5: 哈沃克步枪
],
'4': [ # 狙击弹药武器
(2969, 1395), # 1: 哨兵狙击步枪
(2999, 1382), # 2: 充能步枪
(2992, 1385), # 3: 辅助手枪
(3016, 1383), # 4: 长弓
],
'5': [ # 霰弹枪弹药武器
(2957, 1384), # 1: 和平捍卫者霰弹枪
(2995, 1382), # 2: 莫桑比克
(3005, 1386), # 3: EVA-8
],
'6': [ # 空投武器
(2958, 1384), # 1: 克雷贝尔狙击枪
(2983, 1384), # 2: 敖犬霰弹枪
(3003, 1383), # 3: 波塞克
(3014, 1383), # 4: 暴走
]
},
'2': {
differ: 195 # 直接用1的坐标, 横坐标右移195就可以了
}
}
},
"2560:1440": {
},
"2560:1080": {
},
"1920:1080": {
}
}
# 武器数据
weapon = {
'1': { # 轻型弹药武器
'1': {
name: 'RE-45 自动手枪', # 全程往右飘
shake: {
speed: 80,
count: 10,
strength: 5,
}
},
'2': {
name: '转换者冲锋枪',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'3': {
name: 'R-301 卡宾枪',
shake: {
speed: 74, # 74ms打一发子弹
count: 6, # 压制前6发
strength: 5, # 压制的力度(下移的像素)
},
suppress: {
speed: 74,
}
},
'4': {
name: 'R-99 冲锋枪',
shake: {
speed: 55.5,
count: 13,
strength: 8,
}
},
'5': {
name: 'P2020 手枪',
},
'6': {
name: '喷火轻机枪',
shake: {
speed: 111,
count: 8,
strength: 5,
}
},
'7': {
name: 'G7 侦查枪',
},
'8': {
name: 'CAR (轻型弹药)',
shake: {
speed: 64.5,
count: 10,
strength: 7,
}
}
},
'2': { # 重型弹药武器
'1': {
name: '赫姆洛克突击步枪',
shake: {
speed: 50,
count: 3,
strength: 6,
}
},
'2': {
name: '猎兽冲锋枪',
shake: {
speed: 50,
count: 5,
strength: 6,
}
},
'3': {
name: '平行步枪',
shake: {
speed: 100,
count: 5,
strength: 5,
}
},
'4': {
name: '30-30',
},
'5': {
name: 'CAR (重型弹药)',
shake: {
speed: 64.5,
count: 10,
strength: 7,
}
}
},
'3': { # 能量弹药武器
'1': {
name: 'L-STAR能量机枪',
shake: {
speed: 100,
count: 10,
strength: 5,
}
},
'2': {
name: '三重式狙击枪',
},
'3': {
name: '电能冲锋枪',
shake: {
speed: 83.3,
count: 10,
strength: 7,
}
},
'4': {
name: '专注轻机枪',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'5': {
name: '哈沃克步枪',
shake: {
speed: 100,
count: 8,
strength: 6,
}
},
},
'4': { # 狙击弹药武器
'1': {
name: '哨兵狙击步枪',
},
'2': {
name: '充能步枪',
},
'3': {
name: '辅助手枪',
},
'4': {
name: '长弓',
},
},
'5': { # 霰弹弹药武器
'1': {
name: '和平捍卫者霰弹枪',
},
'2': {
name: '莫桑比克',
},
'3': {
name: 'EVA-8',
},
},
'6': { # 空投武器
'1': {
name: '克雷贝尔狙击枪',
},
'2': {
name: '敖犬霰弹枪',
},
'3': {
name: '波塞克',
},
'4': {
name: '暴走',
shake: {
speed: 200,
count: 8,
strength: 2,
}
},
}
}
toolkit.py
import time
import mss # pip install mss
import ctypes
from ctypes import CDLL
import cfg
from cfg import detect, weapon
# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
try:
driver = CDLL(r'mouse.device.lgs.dll') # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
ok = driver.device_open() == 1
if not ok:
print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
print('初始化失败, 缺少文件')
class Mouse:
@staticmethod
def point():
return user32.GetCursorPos()
@staticmethod
def move(x, y, absolute=False):
if ok:
mx, my = x, y
if absolute:
ox, oy = user32.GetCursorPos()
mx = x - ox
my = y - oy
driver.moveR(mx, my, True)
@staticmethod
def moveHumanoid(x, y, absolute=False):
"""
仿真移动(还没做好)
"""
if ok:
ox, oy = user32.GetCursorPos() # 原鼠标位置
mx, my = x, y # 相对移动距离
if absolute:
mx = x - ox
my = y - oy
tx, ty = ox + mx, oy + my
print(f'({ox},{oy}), ({tx},{ty}), x:{mx},y:{my}')
# 以绝对位置方式移动(防止相对位置丢失精度)
adx, ady = abs(mx), abs(my)
if adx 0 else -i
temp = int(ady / adx * abs(ix))
iy = temp if my > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
else:
# 垂直方向移动的距离短
for i in range(1, ady):
iy = i if my > 0 else -i
temp = int(adx / ady * abs(iy))
ix = temp if mx > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
@staticmethod
def down(code):
if ok:
driver.mouse_down(code)
@staticmethod
def up(code):
if ok:
driver.mouse_up(code)
@staticmethod
def click(code):
"""
:param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
:return:
"""
if ok:
driver.mouse_down(code)
driver.mouse_up(code)
class Keyboard:
@staticmethod
def press(code):
if ok:
driver.key_down(code)
@staticmethod
def release(code):
if ok:
driver.key_up(code)
@staticmethod
def click(code):
"""
键盘按键函数中,传入的参数采用的是键盘按键对应的键码
:param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
:return:
"""
if ok:
driver.key_down(code)
driver.key_up(code)
class Monitor:
"""
显示器
"""
sct = mss.mss()
@staticmethod
def grab(region):
"""
region: tuple, (left, top, width, height)
pip install mss
"""
left, top, width, height = region
return Monitor.sct.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})
@staticmethod
def pixel(x, y):
"""
效率很低且不稳定, 单点检测都要耗时1-10ms
获取颜色, COLORREF 格式, 0x00FFFFFF
结果是int,
可以通过 print(hex(color)) 查看十六进制值
可以通过 print(color == 0x00FFFFFF) 进行颜色判断
"""
hdc = user32.GetDC(None)
return gdi32.GetPixel(hdc, x, y)
class Resolution:
"""
分辨率
"""
@staticmethod
def display():
"""
显示分辨率
"""
w = user32.GetSystemMetrics(0)
h = user32.GetSystemMetrics(1)
return w, h
@staticmethod
def virtual():
"""
多屏幕组合的虚拟显示器分辨率
"""
w = user32.GetSystemMetrics(78)
h = user32.GetSystemMetrics(79)
return w, h
@staticmethod
def physical():
"""
物理分辨率
"""
hdc = user32.GetDC(None)
w = gdi32.GetDeviceCaps(hdc, 118)
h = gdi32.GetDeviceCaps(hdc, 117)
return w, h
class Game:
"""
游戏工具
"""
@staticmethod
def game():
"""
是否在游戏内(顶层窗口是游戏,且正在进行一局游戏,且游戏界面上有血条)
"""
# 先判断是否是游戏窗口
hwnd = user32.GetForegroundWindow()
length = user32.GetWindowTextLengthW(hwnd)
buffer = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, buffer, length + 1)
if 'Apex Legends' != buffer.value:
return False
# 是在游戏中, 再判断下是否有血条和生存物品包
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.game)
for item in data:
x, y = item.get(cfg.point)
if Monitor.pixel(x, y) != item.get(cfg.color):
return False
return True
@staticmethod
def index():
"""
武器索引和子弹类型索引
:return: 武器位索引, 1:1号位, 2:2号位, None:无武器, 拳头(这个暂时无法判断)
子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.pack)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
if data.get(cfg.color) == color:
return None, None
else:
bullet = data.get(hex(color))
return (1, bullet) if color == Monitor.pixel(x, y + 1) else (2, bullet)
@staticmethod
def weapon(index, bullet):
"""
通过武器位和子弹类型识别武器, 参考:config.detect.name
:param index: 武器位, 1:1号位, 2:2号位
:param bullet: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
:return:
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.name)
color = data.get(cfg.color)
if index == 1:
lst = data.get(str(index)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x, y):
return i + 1
elif index == 2:
differ = data.get(str(index)).get(cfg.differ)
lst = data.get(str(1)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x + differ, y):
return i + 1
return None
@staticmethod
def mode():
"""
武器模式
:return: 1:全自动, 2:半自动, None:其他
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.mode)
color = data.get(cfg.color)
x, y = data.get('1')
if color == Monitor.pixel(x, y):
return 1
x, y = data.get('2')
if color == Monitor.pixel(x, y):
return 2
return None
@staticmethod
def detect(data):
"""
决策是否需要压枪, 向信号量写数据
"""
if data[cfg.switch] is False:
print('开关已关闭')
return
t1 = time.perf_counter_ns()
if Game.game() is False:
print('不在游戏中')
data[cfg.shake] = None
return
index, bullet = Game.index()
if (index is None) | (bullet is None):
print('没有武器')
data[cfg.shake] = None
return
if Game.mode() is None:
print('不是自动/半自动武器')
data[cfg.shake] = None
return
arms = Game.weapon(index, bullet)
if arms is None:
print('识别武器失败')
data[cfg.shake] = None
return
# 检测通过, 需要压枪
gun = weapon.get(str(bullet)).get(str(arms))
data[cfg.shake] = gun.get(cfg.shake) # 记录当前武器抖动参数
t2 = time.perf_counter_ns()
print(f'耗时:{t2-t1}ns, 约{(t2-t1)//1000000}ms, {gun.get(cfg.name)}')
apex.py
第三阶段实现 放弃抖枪术 转常规后座抵消法
调了几把枪, 301, RE45, CAR, 平行, 哈沃克(有/无涡轮), 这些使用压枪参数, 其他使用抖枪参数, 压枪与抖枪并存
我的游戏内鼠标设置是这样的, 鼠标DPI是3200
最终的效果是, 20米很稳, 30米将就, 50米不太行, 有几率一梭子打倒, 再往后就没意义了. 差不多够用了, 就没再认真调
如何调压枪参数
我觉得调参数最重要的一点, 就是先算出正确的子弹射速(平均每发子弹耗时), 如果用了错误的数据, 那很可能调了半天白费功夫
测试方法我总结了下, 首先, 每发子弹耗时通常都是50到150毫秒, 先假设是100, 看有多少发子弹, 就复制多少条压枪数据, 举例
R-301 这把枪, 加上金扩容, 能放28发子弹, 那就先准备下面的初始数据, 三个参数分别是, 鼠标水平移动的值/垂直移动的值/移动后休眠时间, 当然也可以有其他的参数
先把对应最后一发子弹的鼠标移动值设置为1000, 看是否打完子弹时, 鼠标正好产生大幅位移, 然后调后面的100, 直到恰好匹配, 然后就可以开始调鼠标参数了
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
调鼠标参数时, 要从上往下逐个调, 因为上面的一个变动, 对下面的影响非常大, 很可能导致下面的白调了
游戏中实测
平行是真的准, 我都有点害怕, 301也不错
就是跑一会儿就变得很卡了, 我怀疑是 压枪进程的原因, 所以加一个重启压枪进程的开关试试效果
详细代码
cfg.py
mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack'
color = 'color'
point = 'point'
index = 'index'
shake = 'shake'
speed = 'speed'
count = 'count'
switch = 'switch'
bullet = 'bullet' # 子弹
differ = 'differ'
turbo = 'turbo'
restrain = 'restrain'
strength = 'strength'
positive = 'positive' # 肯定的
negative = 'negative' # 否定的
# 检测数据
detect = {
"3440:1440": {
game: [ # 判断是否在游戏中
{
point: (236, 1344), # 点的坐标, 血条左上角
color: 0x00FFFFFF # 点的颜色, 255, 255, 255
},
{
point: (2692, 1372), # 生存物品右下角
color: 0x959595 # 149, 149, 149
}
],
pack: { # 背包状态, 有无武器, 选择的武器
point: (2900, 1372), # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
color: 0x808080, # 无武器时, 灰色, 128, 128, 128
'0x447bb4': 1, # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
'0x839b54': 2, # 重型弹药武器
'0x3da084': 3, # 能量弹药武器
'0xce5f6e': 4, # 狙击弹药武器
'0xf339b': 5, # 霰弹枪弹药武器
'0x5302ff': 6, # 空投武器
},
mode: { # 武器模式, 全自动/半自动/单发/其他
color: 0x00FFFFFF,
'1': (3151, 1347), # 全自动
'2': (3171, 1351), # 半自动
},
name: { # 武器名称判断
color: 0x00FFFFFF,
'1': { # 1号武器
'1': [ # 轻型弹药武器
(2959, 1386), # 1: RE-45 自动手枪
(2970, 1385), # 2: 转换者冲锋枪
(2972, 1386), # 3: R-301 卡宾枪
(2976, 1386), # 4: R-99 冲锋枪
(2980, 1386), # 5: P2020 手枪
(2980, 1384), # 6: 喷火轻机枪
(2987, 1387), # 7: G7 侦查枪
(3015, 1386), # 8: CAR (轻型弹药)
],
'2': [ # 重型弹药武器
(2957, 1385), # 1: 赫姆洛克突击步枪
(2982, 1385), # 2: 猎兽冲锋枪
(2990, 1393), # 3: 平行步枪
(3004, 1386), # 4: 30-30
(3015, 1386), # 5: CAR (重型弹药)
],
'3': [ # 能量弹药武器
(2955, 1386), # 1: L-STAR 能量机枪
(2970, 1384), # 2: 三重式狙击枪
(2981, 1385), # 3: 电能冲锋枪
(2986, 1384), # 4: 专注轻机枪
(2980, 1384), # 5: 哈沃克步枪
],
'4': [ # 狙击弹药武器
(2969, 1395), # 1: 哨兵狙击步枪
(2999, 1382), # 2: 充能步枪
(2992, 1385), # 3: 辅助手枪
(3016, 1383), # 4: 长弓
],
'5': [ # 霰弹枪弹药武器
(2957, 1384), # 1: 和平捍卫者霰弹枪
(2995, 1382), # 2: 莫桑比克
(3005, 1386), # 3: EVA-8
],
'6': [ # 空投武器
(2958, 1384), # 1: 克雷贝尔狙击枪
(2959, 1384), # 2: 手感卓越的刀刃
(2983, 1384), # 3: 敖犬霰弹枪
(3003, 1383), # 4: 波塞克
(3014, 1383), # 5: 暴走
]
},
'2': {
differ: 195 # 直接用1的坐标, 横坐标右移195就可以了
}
},
turbo: { # 涡轮
color: 0x00FFFFFF,
differ: 2, # 有涡轮和没涡轮的索引偏移
'3': {
'4': (3072, 1358), # 专注轻机枪 涡轮检测位置
'5': (3034, 1358), # 哈沃克步枪 涡轮检测位置
}
}
},
"2560:1440": {
},
"2560:1080": {
},
"1920:1080": {
}
}
# 武器数据
weapon = {
'1': { # 轻型弹药武器
'1': {
name: 'RE-45 自动手枪', # 全程往右飘
shake: {
speed: 80,
count: 10,
strength: 5,
},
restrain: [
[80, -2, 10], #
[80, -2, 10],
[80, -2, 10],
[80, -4, 10],
[80, -6, 10],
[80, -7, 8], #
[80, -7, 8],
[80, -7, 8],
[80, -7, 8],
[80, -7, 8],
[80, -1, 5], #
[80, -1, 5],
[80, -1, 5],
[80, -1, 5],
[80, -1, 5],
[80, -1, 5], #
[80, -1, 3],
[80, -1, 3],
[80, -1, 3],
[80, -1, 3],
[80, -1, 3], #
[80, -2, 3],
[80, -2, 3],
[80, -2, 3],
[80, -2, 3],
[80, -5, 3], #
[80, -5, 3],
[80, -10, 3],
[80, -10, 3],
[80, -10, 3],
]
},
'2': {
name: '转换者冲锋枪',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'3': {
name: 'R-301 卡宾枪',
shake: {
speed: 74, # 74ms打一发子弹
count: 6, # 压制前6发
strength: 5, # 压制的力度(下移的像素)
},
restrain: [
[75, -5, 10], #
[75, -2, 10],
[75, -2, 10],
[75, -1, 10],
[75, -1, 10],
[75, -1, 10], #
[75, -3, 0],
[75, -2, 0],
[75, -1, 0],
[75, -1, 0],
[75, 0, 5], #
[75, 1, 5],
[75, 3, 5],
[75, 3, 0],
[75, 3, 0],
[75, 5, 0], #
[75, 3, 5],
[75, 3, 5],
[75, 3, 5],
[75, -2, 5],
[75, -4, -5], #
[75, -4, -5],
[75, 0, -5],
[75, 0, 0],
[75, 0, 0],
[75, 1, 0], #
[75, 1, 0],
[75, 1, 0],
[75, 1, 0],
[75, 0, 0],
]
},
'4': {
name: 'R-99 冲锋枪',
shake: {
speed: 55.5,
count: 13,
strength: 8,
}
},
'5': {
name: 'P2020 手枪',
},
'6': {
name: '喷火轻机枪',
shake: {
speed: 111,
count: 8,
strength: 5,
}
},
'7': {
name: 'G7 侦查枪',
},
'8': {
name: 'CAR (轻型弹药)',
shake: {
speed: 64.5,
count: 10,
strength: 7,
},
restrain: [
[58, 0, 10], #
[58, 3, 10],
[58, 3, 10],
[58, 3, 10],
[58, 3, 10],
[58, 3, 10], #
[58, 3, 10],
[58, 3, 10],
[58, -5, 10],
[58, -5, 10],
[58, -5, 5], #
[58, -5, 10],
[58, -5, 0],
[58, 0, 0],
[58, 5, 0],
[58, 5, 3], #
[58, 5, 3],
[58, -5, 3],
[58, -5, 3],
[58, -5, 3],
[58, 0, 0], #
[58, 0, 0],
[58, 0, 0],
[58, 0, 3],
[58, 0, 3],
[58, 0, 3], #
[58, 0, 3],
]
}
},
'2': { # 重型弹药武器
'1': {
name: '赫姆洛克突击步枪',
shake: {
speed: 50,
count: 3,
strength: 6,
}
},
'2': {
name: '猎兽冲锋枪',
shake: {
speed: 50,
count: 5,
strength: 6,
}
},
'3': {
name: '平行步枪',
shake: {
speed: 100,
count: 5,
strength: 5,
},
restrain: [
[100, 0, 10], #
[100, 5, 10],
[100, 5, 10],
[100, 5, 10],
[100, 5, 10],
[100, -5, 10], #
[100, -5, 0],
[100, -5, 0],
[100, -5, 0],
[100, 0, 5],
[100, 5, 5], #
[100, 5, 5],
[100, 5, 0],
[100, 5, 0],
[100, 0, 0],
[100, 5, 5], #
[100, 5, 5],
[100, 5, 5],
[100, 0, 0],
[100, 0, 0],
[100, -5, 5], #
[100, -5, 5],
[100, -5, 5],
[100, -0, 5],
[100, 5, 5],
[100, 5, 5], #
[100, 5, 5],
[100, -5, -5],
[100, -5, 5],
[100, -5, 5],
]
},
'4': {
name: '30-30',
},
'5': {
name: 'CAR (重型弹药)',
shake: {
speed: 58,
count: 10,
strength: 7,
},
restrain: [
[58, 0, 10], #
[58, 3, 10],
[58, 3, 10],
[58, 3, 10],
[58, 3, 10],
[58, 3, 10], #
[58, 3, 10],
[58, 3, 10],
[58, -5, 10],
[58, -5, 10],
[58, -5, 5], #
[58, -5, 10],
[58, -5, 0],
[58, 0, 0],
[58, 5, 0],
[58, 5, 3], #
[58, 5, 3],
[58, -5, 3],
[58, -5, 3],
[58, -5, 3],
[58, 0, 0], #
[58, 0, 0],
[58, 0, 0],
[58, 0, 3],
[58, 0, 3],
[58, 0, 3], #
[58, 0, 3],
]
}
},
'3': { # 能量弹药武器
'1': {
name: 'L-STAR 能量机枪',
shake: {
speed: 100,
count: 10,
strength: 5,
}
},
'2': {
name: '三重式狙击枪',
},
'3': {
name: '电能冲锋枪',
shake: {
speed: 83.3,
count: 10,
strength: 7,
}
},
'4': {
name: '专注轻机枪',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'5': {
name: '哈沃克步枪',
shake: {
speed: 100,
count: 8,
strength: 6,
},
restrain: [
[400, 0, 0], # 延迟
[88, -5, 10], # 1
[88, -5, 15],
[88, 0, 15],
[88, 0, 15],
[88, 0, 15],
[88, 5, 10], #
[88, 5, 10],
[88, 5, 10],
[88, 5, 10],
[88, -5, 5],
[88, -5, 0], # 1
[88, -5, 0],
[88, -10, 0],
[88, -10, 0],
[88, -5, 0],
[88, 0, 5], #
[88, 10, 5],
[88, 10, 5],
[88, 0, 0],
[88, 0, 0],
[88, 5, 10], # 1
[88, 5, 10],
[88, 0, 10],
[88, 5, 10],
[88, 5, 10],
[88, 5, 10], #
[88, 5, 5],
[88, 5, 5],
[88, 0, 5],
[88, 0, 0],
[88, 0, 0], # 1
[88, 0, 0],
[88, 0, 5],
[88, 0, 5],
[88, 0, 5],
[88, 0, 5], #
]
},
'6': {
name: '专注轻机枪 (涡轮)',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'7': {
name: '哈沃克步枪 (涡轮)',
shake: {
speed: 100,
count: 8,
strength: 6,
},
restrain: [
[88, -5, 10], # 1
[88, -5, 15],
[88, 0, 15],
[88, 0, 15],
[88, 0, 15],
[88, 5, 10], #
[88, 5, 10],
[88, 5, 10],
[88, 5, 10],
[88, -5, 5],
[88, -5, 0], # 1
[88, -5, 0],
[88, -10, 0],
[88, -10, 0],
[88, -5, 0],
[88, 0, 5], #
[88, 10, 5],
[88, 10, 5],
[88, 0, 0],
[88, 0, 0],
[88, 5, 10], # 1
[88, 5, 10],
[88, 0, 10],
[88, 5, 10],
[88, 5, 10],
[88, 5, 10], #
[88, 5, 5],
[88, 5, 5],
[88, 0, 5],
[88, 0, 0],
[88, 0, 0], # 1
[88, 0, 0],
[88, 0, 5],
[88, 0, 5],
[88, 0, 5],
[88, 0, 5], #
]
},
},
'4': { # 狙击弹药武器
'1': {
name: '哨兵狙击步枪',
},
'2': {
name: '充能步枪',
},
'3': {
name: '辅助手枪',
},
'4': {
name: '长弓',
},
},
'5': { # 霰弹弹药武器
'1': {
name: '和平捍卫者霰弹枪',
},
'2': {
name: '莫桑比克',
},
'3': {
name: 'EVA-8',
},
},
'6': { # 空投武器
'1': {
name: '克雷贝尔狙击枪',
},
'2': {
name: '手感卓越的刀刃',
},
'3': {
name: '敖犬霰弹枪',
},
'4': {
name: '波塞克',
},
'5': {
name: '暴走',
shake: {
speed: 200,
count: 8,
strength: 2,
}
},
}
}
toolkit.py
apex.py