Значения настроек клиент сохраняет сам. А всё остальное (счётчики, списки друзей, ответы API) надо где-то держать самому. Для этого есть storage, простое хранилище на JSON. Оно переживает и перезагрузку скрипта, и перезапуск игры.
db = Storage("myscript") # файл scripts/storage/myscript.json
db.set("kills", 10).save() # set возвращает само хранилище, можно цепочкой
count = db.get("kills", 0) # второй аргумент: что вернуть, если ключа нет
db["nick"] = "Steve" # как со словарём, тоже работает
if "nick" in db:
print(db["nick"])
db.remove("kills") # удалить ключ
print(db.all()) # копия всех данных как dict
db.save() # записать на диск, без save() всё останется в памяти| Метод | Что делает |
|---|---|
Storage(name) | Открыть или создать файл scripts/storage/<name>.json |
get(key, default=None) | Прочитать значение или вернуть значение по умолчанию |
set(key, value) | Записать значение в память, вернуть хранилище (для цепочек) |
remove(key) | Удалить ключ |
all() | Словарь со всеми данными (копия) |
clear() | Очистить всё |
save() | Сохранить на диск (UTF-8, читаемый JSON) |
load() | Перечитать файл с диска |
Данные лежат в JSON, значит хранить можно только то, что в JSON бывает: числа, строки, списки, словари, True/False, None. Объекты игры (игрок, сущность, цвет) сюда не кладут. Сохрани ник или три числа координат, этого хватит.
Файл читается в момент storage.open, поэтому открывай хранилище один раз, в теле скрипта, а не в @events.tick. И наоборот: save() пишет на диск, так что каждый тик его звать не надо. Копи изменения в памяти и сбрасывай по таймеру или когда модуль выключают.
mod = Module("KillCounter", "Combat")
mod.setDesc("Считает удары и помнит их между заходами")
db = Storage("killcounter")
hits = int(db.get("hits", 0))
save_timer = Timer()
@events.attack
def on_attack(event):
global hits
if not mod.isEnabled():
return
hits += 1
client.overlay("Ударов всего: %d" % hits)
# пишем на диск не чаще раза в 5 секунд
if save_timer.finished(5000):
db.set("hits", hits).save()
save_timer.reset()
@events.module_toggled
def on_toggle(event):
if event.getModule().getName() == "KillCounter" and not event.isState():
db.set("hits", hits).save() # выключили модуль, фиксируем результатTimer() нужен, когда действие должно происходить не постоянно, а раз в сколько-то времени. Раз в секунду написать в чат, раз в пять секунд сохранить файл, подождать полсекунды после удара.
Главное про него: таймер ничего не делает сам. Он не тикает, не зовёт твою функцию и не будит скрипт. Внутри него лежит одно-единственное число: момент времени, который он запомнил. Всё остальное это два вопроса к этому числу и одна команда.
| Строка | Что происходит на самом деле |
|---|---|
t = Timer() | Запомнить текущий момент. Отсчёт пошёл сразу |
t.finished(1000) | Спросить: с того момента уже прошла секунда? Ответ True или False |
t.reset() | Запомнить новый момент. Отсчёт начинается с нуля |
Отсюда правило, из-за которого чаще всего ломаются скрипты: после срабатывания таймер надо сбросить руками. Сам он не сбросится, и finished() будет отвечать True каждый следующий раз. То есть вместо одного сообщения в секунду ты получишь двадцать.
t = Timer() # запомнили момент, отсчёт пошёл
@events.tick
def on_tick(event):
# тик спрашивает 20 раз в секунду: секунда-то прошла?
if t.finished(1000):
t.reset() # ВАЖНО: сразу засекаем заново
client.msg("прошла секунда") # иначе это будет каждый тикЕщё это значит, что таймер нужно создавать один раз, в теле скрипта, рядом с модулем. Если написать Timer() внутри обработчика, на каждом тике будет рождаться новый таймер с нулём на счётчике, и условие не сработает никогда.
| Метод | Что делает |
|---|---|
Timer() | Создать таймер. Отсчёт идёт сразу с момента создания |
finished(ms) | True, если с последнего reset прошло не меньше ms миллисекунд |
passed(ms) | То же самое, просто второе имя для finished |
elapsed() | Сколько миллисекунд прошло с последнего reset |
reset() | Засечь время заново, отсчёт начинается с нуля |
Дальше из этих трёх строк собираются две вещи, ради которых таймер и нужен.
Кулдаун. Действие срабатывает не чаще раза в N миллисекунд, а между срабатываниями скрипт молчит. Так пишут в чат, сохраняют файл на диск, шлют запрос в сеть.
Прогресс. Тут вместо finished берут elapsed(): он отдаёт, сколько миллисекунд прошло. Поделив это число на длину эффекта, получаешь долю от 0 до 1, а из неё уже считают прозрачность, ширину полоски или смещение. Это и есть анимация.
flash = Timer()
# 250 из 500 мс прошло -> 0.5, то есть эффект отыгран наполовину
progress = min(flash.elapsed() / 500.0, 1.0) # min, чтобы не улетело за единицу
alpha = int(255 * (1.0 - progress)) # 255 в начале, 0 в концеСоберём оба приёма в одном скрипте: сообщение по кулдауну и вспышка, которая гаснет.
mod = Module("TimerDemo", "Other")
delay = Slider(mod, "Интервал").min(100).max(5000).step(100).set(1000).suffix(" мс")
cooldown = Timer()
flash = Timer()
@events.tick
def on_tick(event):
if not mod.isEnabled() or not world.ingame():
return
if cooldown.finished(int(delay.get())):
cooldown.reset()
flash.reset() # запускаем анимацию заново
client.msg("Сработало!")
@events.render_2d
def draw(event):
if not mod.isEnabled():
return
progress = min(flash.elapsed() / 500.0, 1.0) # 500 мс на затухание
if progress >= 1.0:
return
alpha = int(255 * (1.0 - progress))
ctx = event.getContext()
ctx.drawRoundedRect(20, 20, 120, 24, border(6), Color(255, 60, 90, alpha))Обрати внимание на пару строк cooldown.reset() и flash.reset(). Первый сброс отмеряет следующую паузу, второй запускает анимацию с начала. Метод один и тот же, а смысл разный: у кулдауна сброс отмеряет следующую паузу, у анимации отмечает, что эффект начался прямо сейчас.
Вектор это просто три числа, [x, y, z]. Точка в мире или направление, и то и другое записывается одинаково.
Объект vec это набор функций для таких троек. На вход список или кортеж, на выходе список. Никаких объектов игры, чистый Python, поэтому vec спокойно работает и в фоновом потоке.
| Функция | Результат |
|---|---|
vec.add(a, b) | Сумма векторов |
vec.sub(a, b) | Разность: направление из b в a |
vec.scale(a, s) | Умножение на число |
vec.dot(a, b) | Скалярное произведение (число) |
vec.length(a) | Длина вектора |
vec.distance(a, b) | Расстояние между точками |
vec.normalize(a) | Вектор того же направления, но длиной ровно 1 |
vec.lerp(a, b, t) | Точка между a и b: t=0 это a, t=1 это b |
vec.clamp(value, low, high) | Зажать ЧИСЛО в диапазон (не вектор) |
mod = Module("NearestInfo", "Other")
mod.setDesc("Ближайший игрок: дистанция и направление")
radius = Slider(mod, "Радиус").min(4).max(64).step(1).set(24)
def point_of(entity):
return [float(entity.getX()), float(entity.getY()), float(entity.getZ())]
@events.tick
def on_tick(event):
if not mod.isEnabled() or not world.ingame():
return
me = point_of(world.self())
best = None
best_dist = radius.get()
for player in world.players():
other = point_of(player)
distance = vec.distance(me, other)
if distance < best_dist:
best, best_dist = player, distance
if best is None:
return
# направление на цель: вектор длиной 1
direction = vec.normalize(vec.sub(point_of(best), me))
client.overlay("%s, %.1f м, направление %.2f/%.2f" % (
best.getNameForScoreboard(), best_dist, direction[0], direction[2]))Где это пригодится: отсечь дальних в ESP, найти ближайшую цель, посчитать точку перед игроком (нормализованное направление, умноженное на нужные метры), сделать плавное движение через vec.lerp. Для 3D-рендера точки уже нужного вида: render3d.line и render3d.marker принимают такие же [x, y, z].