Документация · всё в одном месте
Как настроить модули сервера и как с нуля научиться собирать свои команды: от переменных до игр с прокачкой, магазинами и таблицами лидеров. Каждый пример ниже запущен на настоящем движке бота, а кнопки в превью можно нажимать.
- Уроков и проектов
- 28
- Функций движка
- 190
- Слэш-команд
- 77
- Модулей в панели
- 8
Маршрут обучения
Идите по порядку: каждый урок опирается на предыдущий. Прочитанные главы отмечаются галочкой, прогресс сохраняется в этом браузере.
Знакомство
SilentBot — многофункциональный бот для Discord. В нём есть готовые модули (модерация, уровни, экономика, кланы, музыка, тикеты и другие) и конструктор своих команд с собственным языком шаблонов. Всё настраивается на этом сайте, а в Discord вы просто пользуетесь результатом.
Как устроена документация
Документация разделена на пять частей. Читать всё подряд не обязательно, выбирайте свою дорогу:
- Администратору сервера хватит частей «Знакомство» и «Модули сервера»: там описаны все слэш-команды и каждая настройка панели.
- Тем, кто хочет делать свои команды, подойдёт «Учебник». Он начинается с самых простых вещей и шаг за шагом доходит до игр с сохранением прогресса. Знать программирование заранее не нужно.
- Тем, кто уже умеет, пригодятся «Проекты» и «Справочник» со всеми функциями движка.
За пять минут: добавить бота, войти на сайт и разобраться, где какие настройки.
- Добавьте бота на сервер. На главной странице нажмите «Добавить на сервер» и выберите сервер. Нужны права «Управлять сервером». Оставьте боту все права, которые он запрашивает: без них не будут работать модерация, роли и тикеты.
- Войдите на сайт через Discord. Кнопка «Войти» вверху страницы. В разделе «Мои серверы» появятся серверы, где вы администратор.
- Поднимите роль бота. В настройках сервера Discord → «Роли» перетащите роль SilentBot выше ролей, которые бот должен выдавать. Discord не даёт ботам управлять ролями выше их собственной.
- Откройте панель сервера и пройдитесь по вкладкам ниже.
Вкладки панели управления
!), язык ответов и язык названий команд (русский или английский), часовой пояс, цвет системных сообщений. Здесь же включаются слэш-команды и текстовые команды на сервере.servervar_*) и макросы: кусочки шаблонов, которые можно вызывать из разных команд.store_*: поиск, правка и удаление значений, таблицы лидеров с закрытием сезона и экспорт в JSON.schedule_in, schedule_cron) и журнал запусков команд с ошибками.Два способа вызвать команду
/ в чате, и Discord покажет меню команд с подсказками и полями ввода. Это удобнее всего.!баланс, !привет @друг: префикс, имя и аргументы через пробел. Работает, только если текстовые команды включены во вкладке «Основное».Модули сервера
Модерация состоит из трёх слоёв. Первый — команды для модераторов. Второй — журнал аудита, который записывает, что происходит на сервере. Третий — автомод и лестница наказаний, которые работают сами, без участия людей.
Команды модераторов
Каждое наказание сохраняется как дело с номером. Его можно найти в /дела история и отменить через /дела отменить: снять предупреждение, досрочно закончить мут или разбанить. Длительность пишется так: 30m, 2h, 1d2h, также понимаются 30м и 2ч. У /бан можно указать срок, и тогда бан снимется сам.
/бан <пользователь> [причина] [дней_удаления] [длительность]
Забанить пользователя.
/кик <пользователь> [причина]
Кикнуть пользователя.
/анбан <id_пользователя> [причина]
Снять бан с пользователя по ID.
/мут <пользователь> <длительность> [причина]
Выдать тайм-аут пользователю.
/предупреждение <пользователь> [причина]
Выдать предупреждение пользователю.
/преды <пользователь>
Показать список предупреждений пользователя.
/дела история <пользователь>
Все наказания участника.
/дела отменить <номер> [причина]
Отменить дело: снять предупреждение, мут или бан.
/очистить <количество>
Удалить сообщения в текущем канале.
/медленный_режим <секунды>
Установить медленный режим в текущем канале.
В угловых скобках указаны обязательные параметры, в квадратных — необязательные. Наведите на параметр, чтобы увидеть подсказку.
Лестница наказаний
Команда /предупреждение копит предупреждения, а бот сам наказывает, когда их становится слишком много. Например: 3 предупреждения дают мут на час, 5 — кик, 7 — бан. Значение 0 выключает ступень. Предупреждения могут сгорать через заданное число дней, чтобы старые нарушения не тянулись вечно.
- Мут после N предупреждений
- Длительность мута, минут
- Кик после N предупреждений
- Бан после N предупреждений
- Предупреждения сгорают через, дней (0 — никогда)
- Сообщать участнику в личку о наказании
Журнал аудита
Вкладка «Модерация и журнал». Укажите канал и отметьте нужные события:
Автомод
Автомод настраивается на вкладке «Автомод» и включает пять фильтров. Каждый включается отдельно:
Для доверенных ролей и каналов есть исключения: например, модераторы могут кидать инвайты, а в канале #мемы можно писать капсом.
Два модуля во вкладке «Модули» защищают сервер от ботов и массовых набегов.
Проверка новичков
Бот публикует панель с кнопкой «Я не бот». Новичок нажимает её и решает простой пример вроде «Сколько будет 4 + 7?», после чего получает роль доступа. Можно заодно снимать роль «Новичок». Если пример не нужен, выключите его, и хватит одного нажатия.
- Создайте роль, например «Проверен», и откройте ей каналы сервера. У роли @everyone оставьте доступ только к каналу проверки.
- Заполните модуль «Проверка новичков» и нажмите «Опубликовать панель проверки».
- Проверка включена
- Канал с панелью проверки
- Роль после проверки
- Снять роль после проверки (например «Новичок»)
- Текст панели
- Спрашивать пример (иначе достаточно нажать кнопку)
Защита от рейдов
Если за короткое время заходит слишком много аккаунтов, например 10 за 15 секунд, бот поднимает тревогу. Он может только оповестить модераторов, кикнуть всех, кто зашёл во время рейда, или поднять уровень проверки сервера до «Высокого». Отдельная настройка кикает слишком молодые аккаунты, созданные меньше N дней назад.
- Защита включена
- Сколько входов считать рейдом
- За сколько секунд
- Что делать при рейде
- Кикать аккаунты моложе N дней (0 — выключено)
- Канал для оповещений
Модуль встречает новичков сообщением и картинкой-карточкой с аватаркой, может написать им в личку, прощается с ушедшими и сразу выдаёт стартовые роли.
В текстах можно использовать подстановки: {user} (упоминание), {user.name}, {server}, {members} (номер участника) и \n для переноса строки.
Добро пожаловать на **{server}**, {user}! Ты наш {members}-й участник 🎉
Загляни в #правила и выбери роли в #роли.
- Приветствовать новых участников
- Канал приветствий
- Текст приветствия
- Картинка-карточка с аватаркой
- Заголовок карточки
- Цвет карточки
- Также написать новичку в личку
- Текст в личку
- Прощаться с ушедшими
- Канал прощаний (пусто — канал приветствий)
- Текст прощания
- Роли новым участникам
- Роли добавленным ботам
В текстах: {user} — упоминание, {user.name} — имя, {user.id}, {server} — сервер, {members} — число участников, \n — перенос строки
Участники получают опыт за сообщения и растут в уровнях. За уровни можно выдавать роли, а некоторым ролям давать ускоренный опыт.
Опыт — вкладка «Уровни»
Здесь задаются опыт за одно сообщение, кулдаун (сообщения чаще, чем раз в N секунд, опыта не дают, так спамом не прокачаться) и общий множитель. Там же включаются объявления о новом уровне и выбирается их канал.
Награды — модуль «Награды за уровни»
- Роль за уровень: например, «Активный» на 5 уровне и «Ветеран» на 20.
- Оставлять прошлые роли или держать только роль за самый высокий уровень.
- Множители ролей: бустерам 150% опыта. Если подходит несколько ролей, берётся самый большой множитель.
- Каналы без опыта: флуд-каналы, боты, #spam.
- Поздравление с подстановкой
{level}и карточка ранга для/ранг.
- Роль за достижение уровня (роль → уровень)
- Оставлять роли за прошлые уровниВыключите, чтобы у участника была только роль за самый высокий достигнутый уровень.
- Множитель опыта для ролей, % (роль → процент)Берётся самый большой множитель из ролей участника, например бустерам 150%.
- Каналы без опыта
- Поздравление с новым уровнем
- Показывать /ранг картинкой
- Цвет карточки ранга
В текстах: {user} — упоминание, {user.name} — имя, {level} — новый уровень, {server} — сервер
Команды
/ранг [user]
Показать ваш уровень и опыт.
/топ-уровней
Топ пользователей по уровню.
Встроенная экономика: кошелёк и банк, ежедневные и еженедельные награды, работа, магазин, переводы, азартные игры и достижения. Новичок начинает с 500 монет.
/ежедневно: раз в 24 часа, +25 за каждый день подряд (до +300)/еженедельно: раз в 7 дней, больше с уровнем/работа: раз в часКошелёк и банк
Монеты в кошельке можно потратить, проиграть или потерять при ограблении. Монеты в банке в безопасности. Переводите их командами /депозит и /снять.
Слоты
Три одинаковых символа умножают ставку: 🍒 ×2, 🍋 ×3, 🍇 ×4, ⭐ ×5, 💎 ×8, 👑 ×12. Чем ценнее символ, тем реже он выпадает. Ещё есть /монетка (50 на 50), /дуэль против другого игрока и рискованное /ограбить.
Все команды экономики
/баланс [пользователь]
Показать баланс пользователя.
/ежедневно
Получить ежедневную награду.
/еженедельно
Получить еженедельную награду.
/работа
Заработать монеты работой.
/перевести <пользователь> <сумма>
Перевести монеты другому пользователю.
/топ [страница]
Топ пользователей по капиталу.
/депозит <сумма>
Положить монеты в банк.
/снять <сумма>
Снять монеты из банка.
/инвентарь [пользователь]
Показать инвентарь пользователя.
/магазин
Показать товары магазина.
/купить <товар> [количество]
Купить товар в магазине.
/продать <товар> [количество]
Продать товар из инвентаря.
/профиль [пользователь]
Показать профиль экономики.
/достижения [пользователь]
Показать прогресс достижений экономики.
/промокод <код>
Активировать промокод на монеты.
/монетка <ставка>
Подбросить монетку на ставку.
/слоты <ставка>
Прокрутить слот-машину на ставку.
/дуэль <пользователь> <ставка>
Дуэль на ставку с другим пользователем.
/ограбить <пользователь>
Попробовать ограбить другого пользователя.
/эко-админ выдать <пользователь> <сумма>
Выдать монеты пользователю.
/эко-админ забрать <пользователь> <сумма>
Снять монеты у пользователя.
/эко-админ установить <пользователь> <сумма>
Установить баланс кошелька пользователю.
/эко-админ сброс <пользователь>
Сбросить экономику пользователя.
/эко-админ товар-добавить <название> <цена> <описание>
Добавить товар в магазин.
/эко-админ товар-удалить <товар>
Удалить товар из магазина.
Кланы — это MMO внутри Discord. Игроки объединяются, копят казну, выполняют задания, строят цитадель, воюют с другими кланами и вместе ходят в рейды на боссов. Всё управляется одной командой /клан: она открывает интерактивную панель с разделами.
- Создание клана стоит 100 000 монет экономики. В клане может быть до 30 участников, улучшения добавляют места.
- Роли внутри клана: лидер (всё), офицеры (помощники лидера), участники.
- Если включена публичная страница сервера, лучшие кланы видны и на сайте.
/клан
Открыть интерактивную панель клана.
Пять модулей для жизни сообщества. Все, кроме розыгрышей, настраиваются во вкладке «Модули» и переживают перезапуск бота: кнопки продолжают работать.
Тикеты поддержки
В выбранном канале появляется панель с кнопкой «Создать обращение». Нажатие создаёт приватный канал, который видят только автор и роли поддержки. Когда вопрос решён, тикет закрывают, и расшифровка всей переписки уходит в канал логов. Можно ограничить число открытых тикетов на одного участника.
- Тикеты включены
- Канал с кнопкой «Создать обращение»
- Заголовок панели
- Текст панели
- Надпись на кнопке
- Категория для каналов тикетов
- Роли поддержки (видят все тикеты)
- Первое сообщение в тикете
- Канал для расшифровок закрытых тикетов
- Открытых тикетов на участника
В текстах: {user} — автор обращения, {user.name}, {server}
Розыгрыши
Команда /розыгрыш создать публикует карточку с кнопкой участия. Длительность — от 1 минуты до 60 дней (30m, 2h, 3d), победителей — до 20. Участие можно ограничить ролью или минимальным уровнем. Итоги подводятся автоматически. Если победитель не отозвался, есть /розыгрыш перевыбрать. Нужно право «Управлять сервером».
/розыгрыш создать <приз> <длительность> [победителей] [роль] [уровень]
Запустить розыгрыш в этом канале.
/розыгрыш завершить <номер>
Подвести итоги досрочно.
/розыгрыш перевыбрать <номер>
Выбрать новых победителей завершённого розыгрыша.
Роли по кнопкам
Меню из кнопок, по одной на каждую роль: «🎮 Геймер», «🎨 Художник»… Режим «можно несколько» переключает роли, режим «только одна» снимает остальные роли этого меню. На сервере может быть до 25 меню, в каждом до 25 ролей. Роли с правами модерации или администратора бот выдавать откажется, это защита от ошибок.
Приватные голосовые комнаты
Участник заходит в канал «➕ Создать комнату» и попадает в свою комнату. В её текстовом чате появляются кнопки 🔒 закрыть или открыть, 👥 лимит и ✏️ название. Пустая комната удаляется сама.
- Комнаты включены
- Голосовой канал «➕ Создать комнату»
- Категория для комнат (пусто — как у канала-создателя)
- Название комнаты
- Лимит участников по умолчанию (0 — без лимита)
В текстах: {user.name} — имя владельца
Оповещения YouTube
Вставьте ссылку на канал или @handle и выберите канал Discord. Бот проверяет новые видео каждые 5 минут, API-ключ не нужен. Текст оповещения можно изменить, по умолчанию он такой:
📺 Новое видео на канале **{author}**: {title}
{url}
Музыка в голосовых каналах, мини-игры и полезные инструменты.
Музыка
Зайдите в голосовой канал и напишите /играть с названием трека или ссылкой, плейлисты тоже подходят. /поиск покажет несколько вариантов в меню. Очередью управляют кнопки плеера и команды ниже.
/музыка
Показать статус музыкального модуля.
/играть <запрос>
Добавить трек или плейлист в очередь.
/поиск <запрос>
Найти треки и выбрать нужный в меню.
/очередь
Показать текущую очередь треков.
/текущий
Показать текущий трек.
/пауза
Поставить воспроизведение на паузу.
/продолжить
Продолжить воспроизведение.
/пропустить
Пропустить текущий трек.
/стоп
Остановить музыку и очистить очередь.
/выйти
Отключить бота от голосового канала.
/очиститьочередь
Очистить очередь, не трогая текущий трек.
Развлечения
/пейнт — общий холст, на котором рисует весь сервер. /бродилка — процедурный мир 12×12, по которому можно гулять.
/мем
Получить случайный мем с Reddit.
/шар <вопрос>
Спросить магический шар.
/орел_решка
Подбросить монетку.
/кубик <бросок>
Бросить кубики в формате NdM (например, 2d6).
/шутка
Получить случайную шутку.
/цитата
Получить мотивационную цитату.
/бродилка
Открыть процедурно сгенерированный мир 12x12.
/пейнт
Многопользовательская рисовалка на общем холсте.
Полезное
/напоминание <время> <сообщение>
Создать напоминание.
/опрос <вопрос> <варианты>
Создать опрос.
/перевод <текст> <язык>
Перевести текст на другой язык.
/ascii <изображение> [ширина]
Преобразовать изображение в ASCII-арт.
/погода <город>
Показать погоду в городе.
/калькулятор <выражение>
Вычислить арифметическое выражение.
/роль <роль>
Выдать или снять себе роль.
/таймер <время>
Запустить таймер.
/идея <текст>
Опубликовать идею для сервера.
Информация
/хелп показывает команды по категориям на языке, выбранном в панели. Ваши собственные команды туда тоже попадают.
/хелп
Показать команды по категориям.
/пользователь [участник]
Показать информацию о пользователе.
/сервер
Показать информацию о сервере.
/сервера
Показать серверы, на которых находится бот.
/аватар [участник]
Показать аватар пользователя.
/бот
Показать информацию о боте.
/пинг
Проверить задержку бота.
Возможности сайта помимо панели настроек.
- Страница открыта для всех
- Описание сервера
- Ссылка-приглашение (discord.gg/...)
- Показывать топ по уровням
- Показывать кланы
- Показывать имена участников (иначе — скрытые)
/нейросеть [вопрос]
Silent AI: ответит на вопросы о боте, своих командах, сайте и панели управления.
Учебник: свои команды
Своя команда — это шаблон: текст, в котором некоторые места бот заполняет сам. Начнём с самого простого приветствия и к концу учебника дойдём до игр с сохранением прогресса.
- Откройте панель сервера → «Команды и триггеры» → «Создать команду».
- Придумайте имя, например
привет. По нему команду будут вызывать:/приветили!привет. - Напишите шаблон, посмотрите результат в эмуляторе справа и нажмите «Сохранить». Через пару секунд команда появится в меню
/на сервере.
Главное правило: фигурные скобки
Всё, что вне фигурных скобок, бот отправит как есть. То, что внутри {…}, он вычислит и подставит результат. {user.mention} превратится в упоминание того, кто вызвал команду, а {guild.name} — в название сервера.
Привет, {user.mention}! 👋
Добро пожаловать на **{guild.name}**.
Добро пожаловать на Silent Server.
Сообщение и панель
В редакторе два поля, и у каждого своя роль:
**жирный**, *курсив*, `код`, > цитата, ||спойлер||. Может быть пустым.title:, description:, color:…title: 👋 Привет, {user.display_name}!
description: Рады видеть тебя на **{guild.name}**.\nНас уже **{guild.member_count}**.
color: #5865F2
thumbnail: {user.avatar}
Попробуйте сами Команда /правила
Сделайте команду правила: карточку с заголовком «📜 Правила {guild.name}», тремя пунктами в описании через \n и цветом #e74c3c.
title: 📜 Правила {guild.name}
description: 1. Уважайте друг друга\n2. Без спама и рекламы\n3. Споры решаем в личке
color: #e74c3c
Переменная — это имя в фигурных скобках, вместо которого бот подставит значение: кто вызвал команду, на каком сервере и в каком канале. Путь пишется через точку: {объект.свойство}.
user
- {user.mention}
- упоминание (синяя подсветка), то же, что просто
{user} - {user.name}
- имя аккаунта
- {user.display_name}
- ник на этом сервере
- {user.id}
- числовой ID
- {user.avatar}
- ссылка на аватарку (для
thumbnail:иimage:) - {user.top_role}
- самая высокая роль
- {user.color}
- цвет ника, например
#5865f2 - {user.joined_at}
- когда зашёл на сервер (Discord покажет в часовом поясе читателя)
- {user.created_at}
- когда создан аккаунт
- {user.bot}
true, если это бот
guild
- {guild.name}
- название
- {guild.member_count}
- сколько участников
- {guild.icon}
- ссылка на иконку
- {guild.id}
- ID сервера
- {guild.boost_count}
- число бустов,
{guild.boost_level}— уровень - {guild.roles_count}
- ролей,
{guild.channels_count}— каналов
channel
- {channel.mention}
- ссылка на канал
- {channel.name}
- название
- {channel.id}
- ID,
{channel.topic}— тема канала
title: 🪪 {user.display_name}
thumbnail: {user.avatar}
color: #82e0ff
field: Упоминание | {user.mention} | true
field: ID | {user.id} | true
field: Главная роль | {user.top_role} | true
field: Сервер | {guild.name} ({guild.member_count} участников) | false
field: Канал | {channel.mention} | false
footer: Команда вызвана в #{channel.name}
Попробуйте сами Карточка сервера
Соберите карточку «О сервере»: иконка сервера в thumbnail:, название в заголовке, в полях — число участников и бустов.
title: 🏠 {guild.name}
thumbnail: {guild.icon}
field: 👥 Участники | {guild.member_count} | true
field: 🚀 Бусты | {guild.boost_count} (уровень {guild.boost_level}) | true
color: #82e0ff
Аргументы — это то, что игрок написал после названия команды. В !обнять @Маша аргумент — @Маша. Благодаря аргументам одна команда умеет разные вещи.
Слова по номерам
- {args}
- всё, что написали после команды
- {args.1}, {args.2}…
- первое, второе… слово (есть и короткая запись
{arg1}) - {args.2+}
- всё, начиная со второго слова: удобно для причины или текста
- {args.count}
- сколько слов написали
Всё целиком: **{args}**
Первое слово: **{args.1}**
Второе слово: **{args.2}**
Со второго до конца: **{args.2+}**
Сколько слов: **{args.count}**
Первое слово: меч
Второе слово: огненный
Со второго до конца: огненный 3 штуки
Сколько слов: 4
Если аргумента нет
Если слова нет, переменная будет пустой. Функция {default(значение, 'запасное')} подставит запасной вариант. Упоминание @друг приходит как настоящее упоминание, так что его можно просто вывести.
{user.mention} крепко обнимает {default({args.1}, 'всех вокруг')} 🤗
Слэш-опции: поля ввода в меню «/»
Вместо того чтобы помнить порядок слов, можно объявить опции. Discord покажет их как поля ввода с подсказками: выбор участника из списка, только числа и так далее. Опции задаются в редакторе, в разделе «Слэш-команда»:
- Типы опций:
text,integer(целое),number,bool,user,channel,role. Для текста и чисел можно задать варианты на выбор. - В шаблоне опция читается как
{args.имя}. У участника, канала и роли есть ещё{args.имя.id}и{args.имя.name}. - В текстовом вызове (
!подарить @друг 50) опции заполняются по порядку слов, так что одна команда работает обоими способами.
кому (user), сумма (integer)🎁 {user.mention} дарит {args.кому} **{args.сумма}** монет!
Попробуйте сами Команда «кто лучший»
Команда !лучший отвечает «🏆 Лучший на сервере — …». Выводит того, кого указали, а если никого не указали — автора команды.
🏆 Лучший на сервере — {default({args.1}, {user.mention})}!
Функция — это маленькая готовая программа. Вы отдаёте ей значения, а она возвращает результат: делает текст заглавным, считает длину, заменяет слова. Записывается так: {имя(аргумент1, аргумент2)}.
Три правила аргументов
- Текст берётся в кавычки:
'мир'. Числа пишутся без кавычек:5,3.14. - Переменные и другие функции оборачиваются в свои фигурные скобки:
{upper({user.name})}. Так функции вкладываются друг в друга сколько угодно раз. - Плюс внутри аргументов склеивает текст:
{upper('привет, ' + {user.name})}даст «ПРИВЕТ, TESTER».
Громко: {upper({args})}
С большой буквы: {capitalize({args})}
Задом наперёд: {reverse({args})}
Длина: {len({args})} символов
Звёзды: {repeat('⭐', 5)}
Замена: {replace({args}, 'мир', 'Discord')}
С большой буквы: Привет мир
Задом наперёд: рим тевирп
Длина: 10 символов
Звёзды: ⭐⭐⭐⭐⭐
Замена: привет Discord
Твой ник капсом: **{upper({user.display_name})}**
Первые 3 буквы: **{slice({user.display_name}, 0, 3)}**
Первые 3 буквы: Tes
Самые нужные функции для текста
upper / lowerЗАГЛАВНЫЕ / строчныеcapitalizeС большой буквыreplaceзаменить частьlenдлинаsliceкусок текстаtruncateобрезать с «…»repeatповторить N разcontainsесть ли внутриpad_left7 → 007escape_mdобезвредить Markdownclean_mentionsобезвредить @everyoneВсе функции с описанием собраны в справочнике.
Если внутри фигурных скобок стоит арифметика, бот её посчитает. {2 + 2} превратится в 4. Считать можно и значения переменных и функций, например {{store_get('coins', 0)} * 2}.
+сложить-вычесть*умножить/разделить//разделить нацело%остаток**степень( )скобки2 + 2 × 2 = **{2 + 2 * 2}**
Деление: {10 / 4} · целочисленное: {10 // 4} · остаток: {10 % 4}
Округление: {round(3.14159, 2)}
Большое число: {format_number(1234567, 'ru')}
Процент: {percent(37, 120)}%
Прогресс: {progress_bar(7, 10, 10)}
Деление: 2.5 · целочисленное: 2 · остаток: 2
Округление: 3.14
Большое число: 1 234 567
Процент: 30.8%
Прогресс: ▰▰▰▰▰▰▰▱▱▱
Самое важное правило этого урока
Числа по-русски
plural_ru подбирает нужную форму слова к числу: 1 монета, 2 монеты, 5 монет, 21 монета.
У тебя {args.1} {plural_ru({args.1}, 'монета', 'монеты', 'монет')}.
round(x, 2)округлить до 2 знаковfloor / ceilвниз / вверхmin / maxменьшее / большееclamp(x, 0, 100)зажать в рамкиabsмодульpercent(a, b)процент a от bprogress_bar(7, 10)▰▰▰▰▰▰▰▱▱▱format_number(n, 'ru')1 234 567to_int(x, 0)число из текста или 0even / oddчётное / нечётноеПопробуйте сами Калькулятор скидки
!скидка 2500 15 выводит цену со скидкой 15% и сумму экономии.
{set('price', {to_int({args.1}, 0)})}
{set('off', {calc({get('price')} * {to_int({args.2}, 0)} / 100)})}
Цена со скидкой: **{format_number({calc({get('price')} - {get('off')})}, 'ru')}** ₽
Экономия: {format_number({get('off')}, 'ru')} ₽
Экономия: 375 ₽
Случайность оживляет команды: кубики, лут из сундуков, шанс критического удара, ответы магического шара.
random(1, 6)целое от 1 до 6 включительноchoice('а', 'б', 'в')один вариант из списка, все равновероятныrandom(['обычный', 'редкий'], [90, 10])взвешенный выбор: шансы в процентахlist_random(список)случайный элемент спискаlist_shuffle(список)перемешать список🎲 Выпало: **{random(1, 6)}**
🪙 Монетка: **{choice('Орёл', 'Решка')}**
🎁 Из сундука: **{random(['🪵 Палка', '⚔️ Меч', '💎 Алмаз'], [70, 25, 5])}**
🪙 Монетка: Решка
🎁 Из сундука: 🪵 Палка
title: 🎱 Магический шар
description: **Вопрос:** {default({args}, 'а вопрос-то где?')}\n**Ответ:** {choice('Бесспорно', 'Скорее да', 'Спроси позже', 'Даже не думай', 'Звёзды молчат')}
color: #7d92e8
Условие позволяет команде вести себя по-разному: пускать только взрослых, отвечать модераторам иначе, чем обычным участникам, проверять, хватает ли монет.
Блок {#if}
{#if условие}
то, что выполнится, если условие верно
{#elif другое условие}
если первое неверно, а это верно (веток elif может быть сколько угодно)
{#else}
если ничего не подошло
{/if}
Ветки {#elif} и {#else} необязательны. Внутри веток может быть всё что угодно: текст, директивы карточки, другие условия.
{#if {to_int({args.1}, 0)} >= 18}
🔞 Доступ открыт, тебе {args.1}.
{#elif {to_int({args.1}, 0)} > 0}
🧒 Подрасти немного — тебе всего {args.1}.
{#else}
❓ Напиши возраст числом: `!возраст 20`
{/if}
Сравнения и связки
==равно!=не равно> <больше, меньше>= <=больше или равно…containsсодержит текстstartswithначинается сendswithзаканчивается наandи то, и другоеorхотя бы одноnotнаоборот{#if {args} contains 'кот'}🐱 Котики — лучшие!{#else}🤔 А где котики?{/if}
{#if {args.count} > 3 and not {args} contains 'собак'}Длинное сообщение без собак.{/if}
Как бот читает условие
Условие вычисляется в два шага. Сначала бот подставляет всё, что стоит в фигурных скобках внутри условия, и только потом сравнивает то, что получилось:
{#if {store_get('coins', 0)} >= 100 and {args.1} == 'меч'}— так написано.{#if 250 >= 100 and меч == 'меч'}— так выглядит после подстановки (у игрока 250 монет, он написал «меч»).верно и верно→ верно, выполняется первая ветка.
Поэтому любое значение в условии — функция, переменная, аргумент — пишется в своих фигурных скобках, а сравниваемый текст — в кавычках.
Что считается «да», а что «нет»
0, false. Например, {#if {args.1}} не сработает, если аргумент не передан.true.Числа, текст и регистр
- Числа сравниваются как числа, даже если записаны в кавычках:
'10' > '9'— верно. - Текст сравнивается буква в букву, с учётом регистра:
'Привет' == 'привет'— неверно. То же сcontains,startswithиendswith. - Со списком (
["меч","щит"]) эти три оператора смотрят на элементы, а не на буквы:contains 'щит'— такой элемент есть,startswith 'меч'— первый элемент «меч»,endswith 'щит'— последний «щит». - Чтобы регистр не мешал, приведите значение к нижнему регистру:
{lower({args.1})} == 'секрет'.
Как написали: {#if {args.1} == 'секрет'}✅ совпало{#else}❌ не совпало{/if}
Без учёта регистра: {#if {lower({args.1})} == 'секрет'}✅ совпало{#else}❌ не совпало{/if}
Без учёта регистра: ✅ совпало
Несколько условий сразу
Связки объединяют проверки в одну:
A and B— верно, только если верны оба.A or B— верно, если верно хотя бы одно.not A— переворачивает результат.
Порядок важен: and связывает сильнее, чем or, как умножение сильнее сложения. Запись A or B and C читается как A or (B and C). Если нужно иначе, ставьте круглые скобки:
1 == 1 or 1 == 2 and 1 == 2верно: сначала and (ложь), потом or(1 == 1 or 1 == 2) and 1 == 2неверно: скобки считаются первымиnot (1 == 2 or 2 == 2)неверно: внутри скобок верно, not переворачивает{x} >= 3 and {x} <= 7число в диапазоне от 3 до 7Пример ниже проверяет сразу несколько вещей: передан ли товар, есть ли он в магазине и хватает ли уровня или монет для меча. Попробуйте в редакторе поменять значения coins и lvl.
{set('coins', {store_get('coins', 40)})}{set('lvl', {store_get('lvl', 3)})}
{#if {args.1} == ''}
🛒 Укажите товар: `!купить меч` или `!купить щит`.
{#elif {args.1} != 'меч' and {args.1} != 'щит'}
❓ Товара «{args.1}» нет. Есть только меч и щит.
{#elif {args.1} == 'меч' and ({get('lvl')} < 5 or {get('coins')} < 100)}
🔒 Меч продаётся с **5 уровня** за **100** монет.
У вас: уровень {get('lvl')}, монет {get('coins')}.
{#else}
✅ Покупка «{args.1}» оформлена!
{/if}
У вас: уровень 3, монет 40.
Порядок веток
Бот проверяет ветки сверху вниз и выполняет только первую подходящую, остальные пропускает. Поэтому самые строгие проверки ставьте выше общих. Если первой поставить {#if {coins} >= 10}, а под ней {#elif {coins} >= 1000}, вторая ветка не выполнится никогда: 1000 монет — это тоже «больше 10».
Условие внутри условия
Внутри любой ветки можно открыть ещё один {#if}. Это удобно, когда вторая проверка нужна только в одном случае. У каждого внутреннего блока должен быть свой {/if}.
{set('hour', 14)}{set('rod', 'bamboo')}
{#if {args.1} == 'река'}
🏞️ Вы на реке.
{#if {get('hour')} >= 5 and {get('hour')} < 10}🐟 Утренний клёв: поймали щуку!{#elif {get('rod')} == 'bamboo'}🎣 Бамбуковая удочка: поймали карася.{#else}🌊 Ничего не клюёт.{/if}
{#elif {args.1} == 'море'}
🌊 В море нужна лодка.
{#else}
🗺️ Куда пойдём? `!рыбалка река` или `!рыбалка море`
{/if}
🎣 Бамбуковая удочка: поймали карася.
Роли и права
{has_role('VIP')} и {has_perm('manage_messages')} возвращают true или false. Роль можно указать названием, ID или упоминанием.
{#if {has_perm('manage_messages')} == true}
🛡️ Ты модератор — тебе можно всё.
{#elif {has_role('VIP')} == true}
👑 Привет, VIP!
{#else}
🙂 Обычный участник. Хочешь VIP? Спроси у админов.
{/if}
Условие в одну строку
Когда нужно выбрать одно из двух слов прямо в тексте, удобнее функция if(условие, 'да', 'нет'). Для нескольких вариантов подойдёт switch, а для запасного значения — default.
Число {args.1} — {if({even({args.1})} == true, 'чётное', 'нечётное')}.
Оценка: {switch({args.1}, '5', 'отлично 🏆', '4', 'хорошо 👍', 'надо подтянуть 📚')}
Оценка: отлично 🏆
Попробуйте сами Приветствие по времени суток
Команда здоровается «Доброе утро», «Добрый день» или «Добрый вечер» в зависимости от часа по Москве. Подсказка: {date('%H', '+3h')} вернёт текущий час.
{set('h', {to_int({date('%H', '+3h')}, 0)})}
{#if {get('h')} < 6}🌙 Доброй ночи{#elif {get('h')} < 12}☀️ Доброе утро{#elif {get('h')} < 18}🌤️ Добрый день{#else}🌆 Добрый вечер{/if}, {user.mention}!
Иногда значение нужно вычислить один раз, а использовать несколько: случайный бросок, итоговую цену, выбранный предмет. Для этого есть переменные команды.
{set('имя', значение)}запомнить (ничего не выводит){get('имя', 'если нет')}прочитать{имя}короткая запись для чтения; в арифметике: {урон * 2}{set('base', {to_int({args.1}, 10)})}
{set('roll', {random(1, 100)})}
{set('damage', {if({get('roll')} <= 25, {calc({get('base')} * 2)}, {get('base')})})}
⚔️ Удар на **{get('damage')}** урона{if({get('roll')} <= 25, ' — КРИТ! 💥', '')} (бросок {get('roll')})
Переменные в условиях
Переменную удобно вычислить один раз и потом проверять в нескольких местах. В условии её пишут в фигурных скобках, как любое значение: {#if {урон} > 50} или {#if {get('урон')} > 50}. Подробно про условия — в уроке «Условия и логика».
set, store или servervar: что выбрать
Три вида «переменных» отличаются тем, как долго живёт значение и чьё оно:
set / getодно выполнение команды, потом забываетсяstore_*навсегда, у каждого игрока своёservervar_*навсегда, одно на весь серверПример: команду, которая прибавляет 1, по разу вызвали Вася и Петя. С store_inc('clicks') у Васи будет 1 и у Пети 1: у каждого свой счётчик. С servervar получится 2: счётчик общий. С set — 1 каждый раз, потому что значение не сохраняется между вызовами.
void: выполнить молча
Функции, которые что-то записывают (store_inc, store_set, list_push, lb_add…), возвращают результат, например новое значение счётчика, и он попадает в текст. Если он не нужен, оберните вызов в {void(…)}.
Без void: {store_inc('visits', 1)} ← функция напечатала новое значение
{void(store_inc('visits', 1))}С void: ничего не напечатано, но счётчик вырос.
Сейчас посещений: **{store_get('visits', 0)}**
С void: ничего не напечатано, но счётчик вырос.
Сейчас посещений: 2
Карточка (в Discord её называют embed) — это рамка с цветной полосой, заголовком, текстом, полями и картинками. Она описывается в шаблоне панели: каждая строка начинается с директивы.
title:заголовокurl:сделать заголовок ссылкойdescription:основной текст, до 4096 символовcolor:цвет полосы, #RRGGBBfield: Имя | Значение | trueполе; true — в ряд с соседнимиthumbnail:маленькая картинка справаimage:большая картинка снизуauthor: Имя | ссылка | иконкастрока над заголовкомfooter: Текст | иконкаподпись внизуtimestamp: nowвремя отправки в подписиauthor: {guild.name} | https://discord.com | {guild.icon}
title: 📰 Новости сервера
url: https://discord.com
description: **Большое обновление!**\nДобавили кланы, магазин и еженедельные турниры.
color: #f1c40f
thumbnail: {user.avatar}
field: 🏰 Кланы | Создай свой — `/клан` | true
field: 🛒 Магазин | `/магазин` | true
field: 🏆 Турнир | Каждую субботу в 18:00 | false
image: https://cdn.discordapp.com/embed/avatars/2.png
footer: Опубликовал {user.display_name} | {user.avatar}
timestamp: now
Мелочи, которые важно знать
- Строка без директивы приклеивается к описанию. Поэтому текст, который выводит функция на отдельной строке, попадёт в карточку. Здесь снова помогает
void. - После
|вauthor:иfooter:должна стоять настоящая ссылкаhttps://…или ничего.{user.avatar}и{guild.icon}— это ссылки, а{user.name}— нет. - В значении поля нельзя использовать символ
|: он разделяет части директивы. - Ограничения Discord: до 25 полей, значение поля до 1024 символов.
Попробуйте сами Карточка «Анонс»
Сделайте анонс турнира: жёлтая полоса, заголовок со ссылкой, три поля в ряд (дата, приз, взнос) и подпись с аватаркой автора.
title: 🏆 Турнир по шахматам
url: https://discord.com
description: Регистрация открыта! Жмите на заголовок.
color: #f1c40f
field: 📅 Дата | Суббота, 18:00 | true
field: 🎁 Приз | 5 000 💰 | true
field: 💳 Взнос | бесплатно | true
footer: Организатор: {user.display_name} | {user.avatar}
Кнопки превращают карточку в маленькое приложение. У каждой кнопки есть ключ, и при нажатии выполняется блок {#action ключ} … {/action} с тем же ключом.
button: Надпись | ключ | стиль | режим | only_author | disabled
edit — обновить эту же карточку (по умолчанию), reply — новое сообщение, ephemeral — ответ видит только нажавший, modal — открыть формуonly={args.1} — см. «Игры на несколько игроков»https://… и стиль link, кнопка откроет сайтПервая интерактивная команда
Счётчик нажатий: кнопка прибавляет единицу. Нажмите «➕ Нажать» в превью, это настоящий результат движка.
title: 🔢 Счётчик
description: {user.mention}, нажатий: **{store_get('clicks', 0)}**
color: #5865F2
button: ➕ Нажать | inc | primary | edit | only_author
button: ♻️ Сброс | reset | danger | edit | only_author
{#action inc}
{void(store_inc('clicks', 1))}
{/action}
{#action reset}
{void(store_set('clicks', 0))}
{/action}
Что показывает карточка после нажатия
Это главная тонкость кнопок. Результат зависит от того, что выводит блок действия:
void и set)Основная панель перерисуется со свежими данными. Так сделан счётчик выше: это самый простой и удобный вариант.description:, field:…)Карточка заменится ими. Заголовок, цвет и кнопки, которых нет в блоке, возьмутся из основной панели, так что повторять их не нужно.title: 🧪 Режимы кнопок
description: Нажмите каждую кнопку и сравните, куда приходит ответ.
color: #7d92e8
button: ✏️ edit | e | primary | edit
button: 💬 reply | r | success | reply
button: 🙈 ephemeral | h | secondary | ephemeral
newline:
button: 📖 Документация | https://discord.com | link
{#action e}
description: Карточка **изменилась**, а заголовок, цвет и кнопки остались — они берутся из основной панели.
{/action}
{#action r}
📨 Это новое сообщение в чате, его видят все.
{/action}
{#action h}
🤫 Это видишь только ты.
{/action}
Экраны и кнопка «Назад»
Если в блоке действия объявить свои кнопки, набор кнопок сменится. Так делаются меню с разделами. Пустой блок home возвращает основную панель.
title: 🏠 Главное меню
description: Выберите раздел.
color: #2ecc71
button: 🛒 Магазин | shop | primary | edit | only_author
button: ℹ️ О сервере | about | secondary | edit | only_author
{#action shop}
title: 🛒 Магазин
description: 🗡️ Меч — 100 💰\n🛡️ Щит — 80 💰
button: ⬅️ Назад | home | secondary | edit | only_author
{/action}
{#action about}
title: ℹ️ {guild.name}
description: Участников: **{guild.member_count}**
button: ⬅️ Назад | home | secondary | edit | only_author
{/action}
{#action home}
{/action}
Попробуйте сами Камень, ножницы, бумага
Три кнопки. Бот выбирает случайный вариант и пишет, кто победил.
title: ✊ Камень, ножницы, бумага
description: Ваш ход!
color: #9b59b6
button: 🪨 Камень | rock | secondary | edit | only_author
button: ✂️ Ножницы | scissors | secondary | edit | only_author
button: 📄 Бумага | paper | secondary | edit | only_author
{#action rock}
{set('bot', {choice('rock', 'scissors', 'paper')})}
description: Вы: 🪨 · Бот: {switch({get('bot')}, 'rock', '🪨', 'scissors', '✂️', '📄')}\n**{switch({get('bot')}, 'scissors', '🎉 Победа!', 'rock', '🤝 Ничья', '😢 Поражение')}**
{/action}
{#action scissors}
{set('bot', {choice('rock', 'scissors', 'paper')})}
description: Вы: ✂️ · Бот: {switch({get('bot')}, 'rock', '🪨', 'scissors', '✂️', '📄')}\n**{switch({get('bot')}, 'paper', '🎉 Победа!', 'scissors', '🤝 Ничья', '😢 Поражение')}**
{/action}
{#action paper}
{set('bot', {choice('rock', 'scissors', 'paper')})}
description: Вы: 📄 · Бот: {switch({get('bot')}, 'rock', '🪨', 'scissors', '✂️', '📄')}\n**{switch({get('bot')}, 'rock', '🎉 Победа!', 'paper', '🤝 Ничья', '😢 Поражение')}**
{/action}
Если нужно, чтобы игрок что-то написал, нужна форма (модальное окно). Если нужно, чтобы он что-то выбрал из списка, — выпадающее меню.
Формы
button: Надпись | ключ | стиль | modal
modal: ключ | Заголовок формы | Поле*, Ещё поле, >Длинное поле | reply
- Кнопка в режиме
modalи строкаmodal:должны иметь один и тот же ключ. - До 5 полей.
*в конце делает поле обязательным,>в начале — многострочным. - Последняя часть говорит, куда придёт ответ:
edit,replyилиephemeral. - Ответы доступны в блоке действия как
{modal_1},{modal_2}… по порядку полей.
title: 📝 Идеи для сервера
description: Последняя идея: {servervar_get('last_idea', 'пока нет')}
color: #9b59b6
button: 💡 Предложить | idea | primary | modal
modal: idea | Новая идея | Коротко*, >Подробнее | reply
{#action idea}
{set('text', {escape_md({clean_mentions({modal_1})})})}
{void(servervar_set('last_idea', {get('text')}))}
title: 💡 Идея от {user.display_name}
description: **{get('text')}**\n{escape_md({clean_mentions({modal_2})})}
color: #2ecc71
{/action}
Выпадающие меню
select: Подсказка | Вариант 1, Вариант 2, Вариант 3
select_action: Вариант 1 | что ответить на выбор
Всё, что мы делали до этого, забывалось сразу после ответа. Память хранит данные между вызовами и нажатиями: монеты, уровни, инвентарь, время последнего бонуса. Именно она превращает команды в игры.
store_*: монеты, здоровье, прогресс. Данные отдельно для каждого игрока и каждого сервера, делить их вручную не нужно.servervar_*: общий счётчик, последняя идея, цель сервера. Видна и в панели, во вкладке «Переменные».lb_*: общие таблицы лидеров. О них отдельный урок.Что значит звёздочка и где лежит запись
Звёздочка — сокращение для семейства функций с одним началом: store_* — это store_get, store_set, store_inc и остальные. Все они работают с одним и тем же хранилищем.
Каждая запись в памяти — это значение с «адресом» из нескольких частей. Вы пишете в команде только ключ — имя записи, например 'coins'. Остальные части бот подставляет сам:
'coins'
→
150
'cookies'
→
42
'clicker'
+
игрокID игрока
→
120 очков
- Одинаковый ключ у разных игроков — разные записи. У Васи
coins= 150, у Петиcoins= 20, и они друг другу не мешают. - На другом сервере — своя память. Та же команда на другом сервере начнёт с нуля.
store_getчитает только данные того, кто вызвал команду. Чужие записи видны черезstore_scan(…, 'all')иstore_sort, а общие для всех данные удобнее хранить вservervar_*или таблице лидеровlb_*.- Ключ не зависит от регистра (
Coinsиcoins— одно и то же), до 64 символов. У одного игрока на сервере — до 300 ключей, значение — до 1200 символов.
Чтение и запись
store_get('coins', 0)прочитать; 0 — что показать, если ещё не записаноstore_set('coins', 100)записать значениеstore_inc('coins', 5)прибавить (отсутствующий ключ считается нулём)store_dec('coins', 5)отнять (может уйти в минус!)store_mul('coins', 2)умножитьstore_del('coins')удалитьstore_type('coins')пусто, если ключа ещё нетstore_set('buff', 1, 3600)третий аргумент — срок жизни в секундахЕжедневный бонус: память + время
Запоминаем, когда игрок брал бонус, и пускаем снова только через сутки. unix() — текущее время в секундах, 86400 — секунд в сутках.
{set('last', {store_get('daily_at', 0)})}
{#if {calc({unix()} - {get('last')})} < 86400}
title: ⏳ Рано!
description: Следующий бонус {discord_time({calc({get('last')} + 86400)}, 'R')}.\nБаланс: **{store_get('coins', 0)}** 💰
color: #e67e22
{#else}
{void(store_set('daily_at', {unix()}))}
{void(store_inc('coins', 100))}
title: 🎁 Ежедневный бонус
description: +100 💰 зачислено! Баланс: **{store_get('coins', 0)}** 💰
color: #2ecc71
{/if}
Стартовые значения
Второй аргумент store_get('hp', 100) только показывается, в память он не записывается. Если игроку нужно начать со 100 здоровья, запишите это один раз, при первом запуске. Проверить, что ключа ещё нет, помогает store_type.
{#if {store_type('hp')} == ''}{void(store_set('hp', 60))}{void(store_set('coins', 50))}{/if}
title: 🧙 Персонаж {user.display_name}
description: ❤️ Здоровье: **{store_get('hp', 0)}**/100\n💰 Монеты: **{store_get('coins', 0)}**
color: #e74c3c
button: 🍖 Поесть (−10 💰, +20 ❤️) | eat | success | edit | only_author
{#action eat}
{#if {store_get('coins', 0)} < 10}
description: ❌ Не хватает монет.
{#elif {store_get('hp', 0)} >= 100}
description: 😋 Вы и так сыты.
{#else}
{void(store_dec('coins', 10))}
{void(store_set('hp', {min(100, {calc({store_get('hp', 0)} + 20)})}))}
{/if}
{/action}
Запись с несколькими значениями
Одна запись хранит одно значение. Когда у объекта несколько свойств (у героя — уровень, здоровье и золото) или нужно хранить много однотипных вещей, есть два способа.
hero_lvl, hero_hp, hero_gold. Каждое свойство — отдельное число: его легко прибавлять через store_inc и проверять в {#if}. Подходит для характеристик, счётчиков и «сколько у меня предметов каждого вида».bag хранит ["Меч", "Щит", "Зелье"]. Подходит, когда важен порядок или элементы повторяются: инвентарь, история, очередь. Подробно — в уроке «Списки и циклы».Первый способ в деле: три характеристики героя. Кнопка «Все записи героя» показывает их разом через store_scan('hero_') — он находит все ключи игрока, которые начинаются с hero_.
{#if {store_type('hero_lvl')} == ''}{void(store_set('hero_lvl', 1))}{void(store_set('hero_hp', 100))}{void(store_set('hero_gold', 25))}{/if}
title: 🧙 Герой {user.display_name}
color: #9b59b6
field: Уровень | {store_get('hero_lvl', 1)} | true
field: Здоровье | {store_get('hero_hp', 100)} ❤️ | true
field: Золото | {store_get('hero_gold', 0)} 🪙 | true
button: ⚔️ Тренировка (−20 ❤️, +1 ур.) | train | primary | edit | only_author
button: 📋 Все записи героя | all | secondary | ephemeral
{#action train}
{#if {store_get('hero_hp', 0)} < 20}
description: 😵 Слишком мало здоровья для тренировки.
{#else}
{void(store_dec('hero_hp', 20))}
{void(store_inc('hero_lvl'))}
{/if}
{/action}
{#action all}
{store_scan('hero_')}
{/action}
hero_hp = 100
hero_lvl = 1
Общие данные сервера
{void(servervar_set('cookies', {servervar_get('cookies', 0)} + 1))}
🍪 {user.mention} съел печеньку. Сервер съел уже **{servervar_get('cookies', 0)}**!
Переменную сервера можно прочитать и короче: {servervar.cookies}. Во вкладке «Переменные» панели их видно и можно менять вручную.
На одном сервере — до 500 переменных, имя — до 64 символов, значение — до 1200 символов. Менять существующие переменные можно без ограничений. Если лимит заполнен, новая переменная не сохранится, servervar_set ничего не выведет, а эмулятор покажет предупреждение. Все лимиты собраны в справочнике.
Данные игроков в панели
Вкладка «Данные игроков» в панели сервера показывает всё, что команды запомнили: поиск по ключу и ID игрока, правка значения (тип сохраняется: число останется числом), удаление у одного игрока или сразу у всех. Там же таблицы лидеров с кнопкой «Закрыть сезон» (как lb_reset) и экспорт всего в JSON-файл.
Иногда одного значения мало: инвентарь, полка с книгами, коллекция значков. Для этого есть списки и наборы, а чтобы вывести их по пунктам — циклы. В этом уроке — как список создать, прочитать, изменить, проверить и сохранить.
Где живёт список
bag → ["Меч", "Щит"]. Переживает вызовы и нажатия. С ним работают функции list_* по ключу: list_push('bag', …).list_create('a', 'b'), range(1, 6), строка 'a,b,c' или список из JSON внешнего API. Его можно вывести, отсортировать или записать в память.Внутри список хранится как JSON-строка: ["📕 Дюна", "📗 Хоббит"]. Номера элементов начинаются с нуля: первый элемент — № 0, второй — № 1, а −1 означает последний.
Создать и добавить
list_push('bag', 'Меч')добавить в конец; ключа ещё нет — список создастся. Выводит новую длинуlist_push('log', 'текст', 20)третий аргумент — максимальная длина: лишние старые элементы удаляютсяstore_set('bag', list_create('Меч', 'Щит'))записать сразу весь список (прежний заменится)list_create('a', 'b', 'c')временный список из значенийПрочитать
store_get('bag', '[]')весь список; '[]' — пустой, если ещё ничего нет. Используйте его в циклахlist_len('bag')сколько элементовlist_get('bag', 0)элемент по номеру: 0 — первый, −1 — последний. Третий аргумент — что вернуть, если элемента нетlist_slice('bag', 0, 3, ', ')часть списка текстом: с № 0 до № 3 (не включая), через разделительlist_first('bag')первый элементlist_last('bag')последний элементlist_random('bag')случайный элементЧтобы вывести все элементы по пунктам, используйте цикл {#for книга in {store_get('shelf', '[]')}}…{/for}: внутри {книга} — элемент, {index} — его номер с единицы. Если нужна одна строка, есть функция for(список, 'шаблон', 'разделитель').
{#if {store_type('shelf')} == ''}{void(store_set('shelf', list_create('📕 Дюна', '📗 Хоббит', '📘 Солярис', '📙 Пикник')))}{/if}
Весь список (так он хранится): `{store_get('shelf', '[]')}`
Сколько книг: **{list_len('shelf')}**
Первая: **{list_get('shelf', 0)}** · последняя: **{list_get('shelf', -1)}**
Первые две: {list_slice('shelf', 0, 2, ', ')}
Случайная: {list_random('shelf')}
По порядку:
{#for book in {store_get('shelf', '[]')}}{index}. {book}
{/for}
["📕 Дюна", "📗 Хоббит", "📘 Солярис", "📙 Пикник"]Сколько книг: 4
Первая: 📕 Дюна · последняя: 📙 Пикник
Первые две: 📕 Дюна, 📗 Хоббит
Случайная: 📕 Дюна
По порядку:
1. 📕 Дюна
2. 📗 Хоббит
3. 📘 Солярис
4. 📙 Пикник
Изменить
list_set('bag', 0, 'Лук')заменить элемент по номеру (−1 — последний). Выводит новое значениеlist_remove('bag', 'Зелье')убрать первый такой элемент; третий аргумент — сколько (0 — все). Выводит новую длинуlist_pop('bag')снять последний элемент и вывести его; 'left' — первыйstore_set('bag', list_sort('bag'))отсортировать и сохранить. Так же работают list_reverse, list_shuffle, list_uniquestore_del('bag')удалить список целикомlist_sort и похожие функции возвращают новый список и ничего не сохраняют. Чтобы изменение осталось, запишите результат обратно через store_set.
Пример ниже собирает всё вместе. «Выпить зелье» убирает одно зелье из двух, «Улучшить меч» находит номер меча и заменяет его, «Сортировать» сохраняет отсортированный список. Флаг pack_ready нужен, чтобы стартовый набор выдавался один раз: без него после «Выбросить всё» пустой рюкзак снова выглядел бы как первый запуск.
{#if {store_type('pack_ready')} == ''}{void(store_set('pack', list_create('🧪 Зелье', '🗡️ Меч', '🧪 Зелье', '🍞 Хлеб')))}{void(store_set('pack_ready', 1))}{/if}
title: 🎒 Рюкзак ({list_len('pack')}/8)
description: {#if {list_len('pack')} == 0}Пусто{#else}{for({store_get('pack', '[]')}, '{index}. {item}', '\n')}{/if}
color: #16a085
button: 🧪 Выпить зелье | use | success | edit | only_author
button: ✨ Улучшить меч | upgrade | primary | edit | only_author
button: 🔤 Сортировать | sort | secondary | edit | only_author
button: 🗑️ Выбросить всё | clear | danger | edit | only_author
{#action use}
{#if {list_contains('pack', '🧪 Зелье')} == true}
{void(list_remove('pack', '🧪 Зелье'))}
🧪 Вы выпили одно зелье
{#else}
❌ Зелий не осталось
{/if}
{/action}
{#action upgrade}
{set('pos', {list_index('pack', '🗡️ Меч')})}
{#if {get('pos')} >= 0}
{void(list_set('pack', {get('pos')}, '⚔️ Меч +1'))}
✨ Меч улучшен
{#else}
❌ Обычного меча нет
{/if}
{/action}
{#action sort}
{void(store_set('pack', list_sort('pack')))}
{/action}
{#action clear}
{void(store_del('pack'))}
{/action}
Проверить
list_contains('bag', 'Меч')true, если элемент естьlist_index('bag', 'Меч')номер элемента (с нуля) или −1, если его нетlist_count('bag', 'Зелье')сколько раз элемент встречается{#if {list_len('bag')} == 0}список пуст{#if {store_type('keys')} == ''}{void(store_set('keys', list_create('🗝️ Медный', '🗝️ Медный', '🔑 Серебряный')))}{/if}
В кармане: {list_slice('keys', 0, 10, ', ')}
{#if {list_contains('keys', '🔑 Серебряный')} == true}🚪 Серебряная дверь открыта! Ключ лежит на месте №{calc({list_index('keys', '🔑 Серебряный')} + 1)}.{#else}🔒 Нужен серебряный ключ.{/if}
Медных ключей: **{list_count('keys', '🗝️ Медный')}**{#if {list_count('keys', '🗝️ Медный')} >= 2} — хватит на сундук{/if}
🚪 Серебряная дверь открыта! Ключ лежит на месте №3.
Медных ключей: 2 — хватит на сундук
Цикл {#for} по числам
Цикл умеет проходить не только по списку, но и по числам. range(1, 6) — это числа от 1 до 5 (последнее не включается).
Таблица умножения на {args.1}:
{#for i in {range(1, 6)}}{args.1} × {i} = **{{args.1} * {i}}**
{/for}
7 × 1 = 7
7 × 2 = 14
7 × 3 = 21
7 × 4 = 28
7 × 5 = 35
{for({list_create('🍎 Яблоко', '🍌 Банан', '🍇 Виноград')}, '{index}. {item}', '\n')}
2. 🍌 Банан
3. 🍇 Виноград
Ещё один пример: сумка с лутом
title: 🎒 Сумка ({list_len('bag')}/10)
description: {#if {list_len('bag')} == 0}Пусто{#else}{for({store_get('bag', '[]')}, '{index}. {item}', '\n')}{/if}
color: #1abc9c
button: 🎁 Открыть сундук | loot | primary | edit | only_author
button: 🗑️ Выбросить последний | drop | danger | edit | only_author
{#action loot}
{#if {list_len('bag')} >= 10}
description: ❌ Сумка полна!
{#else}
{void(list_push('bag', {random(['🗡️ Меч', '🛡️ Щит', '🧪 Зелье'], [30, 30, 40])}))}
{/if}
{/action}
{#action drop}
{void(list_pop('bag'))}
{/action}
Наборы: без повторов
Набор (set_*) похож на список, но один и тот же элемент в нём хранится только раз. Он подходит для значков, открытых локаций и полученных достижений: повторное добавление ничего не меняет.
set_add('badges', 'Рыбак')добавить (если ещё нет); выводит размерset_remove('badges', 'Рыбак')убрать; выводит размерset_has('badges', 'Рыбак')true или falseset_values('badges', ' · ')все элементы текстом через разделительset_size('badges')сколько элементов{void(set_add('badges', '🌟 Новичок'))}{void(set_add('badges', '🎣 Рыбак'))}{void(set_add('badges', '🌟 Новичок'))}
Значков: **{set_size('badges')}**: {set_values('badges', ' · ')}
Есть «Рыбак»? {if({set_has('badges', '🎣 Рыбак')} == true, 'да ✅', 'нет')}
Есть «Рыбак»? да ✅
Цикл повторяет кусок шаблона несколько раз: для каждого предмета в сумке, для чисел от 1 до 10 или пока выполняется условие. Есть два цикла: {#for} идёт по списку, а {#while} повторяет, пока условие верно.
{#for x in список} … {/for}по каждому элементу списка; {x} — текущий элемент{#while условие} … {/while}пока условие верно{index}номер шага, с единицы{break}прервать цикл сразу{continue}пропустить остаток шага и перейти к следующемуЦикл for: по каждому элементу
После for пишется имя для текущего элемента, затем in и список: {#for вещь in {get('bag')}}. Внутри цикла {вещь} — очередной элемент, {index} — его номер. Если имя не указать ({#for {список}}), элемент доступен как {item}.
{set('bag', {list_create('🗡️ Меч', '🛡️ Щит', '🏹 Лук')})}
title: 🎒 Твоя сумка
description: {#for вещь in {get('bag')}}{index}. {вещь}\n{/for}
color: #8b5cf6
Список можно взять откуда угодно: list_create(…), store_get('bag', '[]'), split({args}, ','), file_list('#канал') или прямо написать [1, 2, 3].
Повторить N раз: range
Чтобы повторить действие несколько раз, пройдите по числам: range(1, 6) даёт 1, 2, 3, 4, 5 (последнее число не входит). range(5) — от 0 до 4, range(10, 0, -2) — 10, 8, 6, 4, 2.
Таблица умножения на {args.1}:
{#for i in {range(1, 6)}}{args.1} × {i} = **{{args.1} * {i}}**
{/for}
7 × 1 = 7
7 × 2 = 14
7 × 3 = 21
7 × 4 = 28
7 × 5 = 35
Счётчики и суммы внутри цикла
Переменные, записанные через {set} внутри цикла, сохраняются и после него. Так удобно считать сумму, количество или собирать текст по кусочкам. А имя элемента (цена в примере ниже) и index после цикла возвращают прежние значения.
{set('prices', {list_create(120, 45, 300, 80)})}
{set('total', 0)}
{set('lines', '')}
{#for цена in {get('prices')}}{set('total', {math({get('total')} + {цена})})}{set('lines', {get('lines')} + 'Товар ' + {index} + ': ' + {цена} + ' 🪙\n')}{/for}
title: 🧾 Чек
description: {get('lines')}**Итого: {get('total')} 🪙**
color: #22c55e
Цикл while: пока условие верно
{#while условие} проверяет условие перед каждым шагом. Если оно верно — выполняет тело и проверяет снова, если нет — цикл заканчивается. Условие пишется так же, как в {#if}: можно сравнивать, использовать and, or, not.
{set('n', {default({args.1}, 100)})}
{set('steps', 0)}
{#while {get('n')} >= 2}
{set('n', {math({get('n')} // 2)})}
{set('steps', {math({get('steps')} + 1)})}
{/while}
Число {args.1} можно разделить пополам **{get('steps')}** раз, пока не останется меньше 2.
break и continue
{break} сразу выходит из цикла: оставшиеся шаги не выполняются. {continue} пропускает остаток текущего шага и переходит к следующему (у while — к новой проверке условия). Обычно их ставят внутрь {#if}:
{set('found', '')}
{#for n in {range(1, 20)}}{#if {math({n} % 2)} == 0}{continue}{/if}{#if {math({n} * {n})} > {math({args.1} * 10)}}{set('found', {n})}{break}{/if}Проверил {n}: {math({n} * {n})} — мало
{/for}
Первое нечётное число, квадрат которого больше {math({args.1} * 10)}: **{get('found')}**
Проверил 3: 9 — мало
Проверил 5: 25 — мало
Первое нечётное число, квадрат которого больше 30: 7
Оба действуют на ближайший цикл, в котором стоят: break во внутреннем цикле не прерывает внешний. Вне цикла они ничего не делают, редактор подсветит это предупреждением.
Цикл в цикле
Циклы можно вкладывать друг в друга — например, чтобы пройти по строкам и столбцам таблицы. Внешний цикл делает шаг, и на каждом его шаге внутренний проходит целиком.
{#for a in {range(1, 4)}}{#for b in {range(1, 4)}}{a}×{b}={math({a} * {b})} {/for}
{/for}
2×1=2 2×2=4 2×3=6
3×1=3 3×2=6 3×3=9
Ограничения
Циклы выполняются на сервере бота, поэтому у них есть пределы: одна неудачная команда не должна тормозить бота на всех серверах.
50элементов проходит один for; остальные пропускаются100повторов у одного while; потом цикл останавливается3000шагов всех циклов за один запуск команды вместе3цикла можно вложить друг в друга: цикл, в нём второй, в нём третий. Четвёртый не выполнитсяКогда предел достигнут, цикл просто останавливается, а команда продолжает работу. Эмулятор в редакторе покажет, какой предел сработал, а редактор заранее подсветит слишком глубокую вложенность.
Таблица лидеров — общий рейтинг сервера по любым очкам: кликам, пойманной рыбе, победам. У каждой таблицы есть имя, и в команде их может быть сколько угодно.
lb_add('clicker', 1)прибавить очки себеlb_set('clicker', 50)установить счётlb_get('clicker', '', 0)свой счёт (0 — если ещё нет)lb_top('clicker', 10)топ: «1. @игрок — 120»lb_rank('clicker')своё место (0 — не в таблице)lb_near('clicker', '', 2)соседи сверху и снизуlb_reset('clicker', 'Сезон 1')закрыть сезон: сохранить в архив и обнулитьlb_season_top('clicker', 'Сезон 1')топ прошлого сезонаtitle: 🏆 Кликер-турнир
description: {default({lb_top('clicker', 5)}, 'Пока никто не играл — стань первым!')}
color: #f1c40f
button: 👆 +1 очко | tap | success | edit
button: 📊 Моё место | me | secondary | ephemeral
{#action tap}
{void(lb_add('clicker', 1))}
{/action}
{#action me}
Ваш счёт: **{lb_get('clicker', '', 0)}**, место: **#{lb_rank('clicker')}**
{/action}
Кнопка «Моё место» работает в режиме ephemeral: личный результат видит только нажавший, а общая карточка остаётся нетронутой.
Голосование, дуэль, общий босс, игра на двоих: панелью пользуется несколько человек сразу. Для этого есть две вещи: данные панели, свои у каждого отправленного сообщения, и кнопки для конкретного игрока.
Данные панели
store_* хранит данные игрока, servervar_* — данные всего сервера. msg_* хранит данные одной панели: если открыть два голосования одной командой, у каждого будет свой счёт.
msg_get('votes', 0)значение этой панели (0 — если ещё нет)msg_set('turn', {user.mention})записать; печатает значение, поэтому оборачивайте в voidmsg_inc('votes')прибавить 1 (или сколько указано)msg_del('turn')удалитьtitle: 📊 {default({args}, 'Голосование')}
description: 👍 За: **{msg_get('yes', 0)}** · 👎 Против: **{msg_get('no', 0)}**\nГолосов: {calc({msg_get('yes', 0)} + {msg_get('no', 0)})}
color: #5865F2
button: 👍 За | yes | success
button: 👎 Против | no | danger
{#action yes}
{#if {contains({msg_get('voters')}, {user.id})} == true}
Вы уже голосовали.
{#else}
{void(msg_set('voters', {msg_get('voters')},{user.id}))}{void(msg_inc('yes'))}
{/if}
{/action}
{#action no}
{#if {contains({msg_get('voters')}, {user.id})} == true}
Вы уже голосовали.
{#else}
{void(msg_set('voters', {msg_get('voters')},{user.id}))}{void(msg_inc('no'))}
{/if}
{/action}
При первом запуске панель ещё не отправлена, поэтому msg_set в основной панели запоминает значения и сохраняет их за сообщением сразу после отправки. У панели до 50 ключей, данные живут 30 дней.
Кнопка для одного игрока
button: Принять | accept | success | edit | only={args.1}
only= — упоминание или ID участника: остальным кнопка ответит «Эта кнопка только для …». Если игрок не указан (only= пустое), кнопку не нажмёт никто. А only_author по-прежнему оставляет кнопку тому, кто открыл панель.
В действиях кнопок {user} — тот, кто нажал, а {owner} и {owner_id} — тот, кто открыл панель.
title: ⚔️ Дуэль
description: {owner} вызывает {args.1}!\n{#if {msg_get('win')} == ''}Ждём, примет ли вызов…{#else}🎲 {owner} — **{msg_get('a')}**, {args.1} — **{msg_get('b')}**. Победа: {msg_get('win')}{/if}
color: #e74c3c
button: ✅ Принять вызов | accept | success | edit | only={args.1} | disabled={#if {msg_get('win')} == ''}false{#else}true{/if}
{#action accept}
{void(msg_set('a', {random(1, 6)}))}{void(msg_set('b', {random(1, 6)}))}
{#if {msg_get('a')} > {msg_get('b')}}{void(msg_set('win', {owner}))}{#else}{#if {msg_get('a')} < {msg_get('b')}}{void(msg_set('win', {args.1}))}{#else}{void(msg_set('win', 'ничья'))}{/if}{/if}
{/action}
Команды могут работать с тем же, что и встроенные модули бота: монетами из /баланс, уровнями из /ранг и кланами из /клан. Магазин, где покупают за настоящие монеты, или квест, который даёт опыт, — пара строк.
eco_balance()монеты в кошельке ('bank' — в банке, 'total' — всего)eco_add(50)дать монеты, минус — забрать (не ниже 0); печатает балансeco_take(100)списать, только если хватает: true / falseeco_pay(@игрок, 20)перевести свои монеты другому: true / falselevel()уровень на сервере; 'xp', 'need', 'total', 'rank'xp_add(25)дать опыт; печатает новый уровеньclan_name()клан игрока (пусто — не в клане), ещё clan_tag(), clan_role()Все функции принимают игрока последним аргументом: eco_balance('total', {args.1}), level('rank', @игрок). Без него — тот, кто вызвал команду.
Профиль игрока
title: 🪪 {user.display_name}
thumbnail: {user.avatar}
color: #D4A017
field: 💰 Кошелёк | {format_number({eco_balance()})} | true
field: 🏦 Всего | {format_number({eco_balance('total')})} | true
field: ⭐ Уровень | {level()} ({level('xp')}/{level('need')} XP), место #{level('rank')} | false
field: 🛡️ Клан | {default({clan_name()}, 'без клана')} | false
Магазин за монеты бота
eco_take сначала проверяет, хватает ли денег, и только тогда списывает — купить «в минус» нельзя, даже если два игрока жмут кнопку одновременно. Забрать монеты у другого игрока (eco_take(100, @кто)) можно, только если он участник этого сервера, и не больше 10 000 в день с одного игрока: кошелёк общий для всего бота, и команды чужого сервера не должны его опустошать.
title: 🛒 Лавка
description: Зелье опыта — **100** монет, даёт **+100 XP**.\nУ вас: **{eco_balance()}** монет.
color: #2ecc71
button: 🧪 Купить зелье | buy | success | ephemeral
{#action buy}
{#if {eco_take(100)} == true}
{set('lvl', {xp_add(100)})}
Куплено! Теперь у вас **{eco_balance()}** монет и **{get('lvl')}** уровень.
{#else}
Не хватает монет: нужно 100, у вас {eco_balance()}.
{/if}
{/action}
Перевод другому игроку
{#if {eco_pay({args.1}, {args.2})} == true}
🎁 {user.mention} подарил {args.1} **{args.2}** монет!
{#else}
Не получилось: проверьте сумму и хватает ли монет.
{/if}
Бот считает время в UTC. Для таймеров и кулдаунов удобнее всего UNIX-время: число секунд с 1970 года. Его легко складывать, вычитать и хранить в памяти.
unix()сейчас в секундах; unix('+1h') — через часdiscord_time(t, 'R')«через 5 минут» / «2 часа назад», Discord покажет у каждого в его часовом поясеnow()дата и время меткой Discorddate('%d.%m %H:%M', '+3h')дата по формату со сдвигом (+3h — Москва)time_ago(t)«5 мин. назад»time_diff(a, b, 'hours')разница в часах, минутах, дняхday_of_week()«Понедельник»…Сдвиги записываются как +30m, -2h, +1d, +1w.
Сейчас: {now()}
Через час: {discord_time({unix('+1h')}, 'R')}
Дата по Москве: {date('%d.%m.%Y %H:%M', '+3h')}
Сегодня: {day_of_week()}
Неделю назад это было: {time_ago({unix('-7d')})}
Через час: 4 ч. назад
Дата по Москве: 28.09.2026 16:32
Сегодня: Понедельник
Неделю назад это было: 7 дн. назад
Кулдаун своими руками
Схема всегда одна и та же. Запоминаем unix() в момент действия, а в следующий раз сравниваем разницу с нужной паузой. Готовый пример с ежедневным бонусом — в уроке про память, энергия, которая восстанавливается со временем, — в проекте «Шахта».
Команда может не только отвечать, но и действовать: выдавать роли, писать в другие каналы и в личку, ставить реакции, менять ник. Действия выполняются после того, как ответ собран, и сами ничего не выводят.
role_add('Роль')выдать роль (название, ID или упоминание)role_remove('Роль')снять рольrole_toggle('Роль')выдать, если нет, иначе снятьsend_to('#канал', 'текст')сообщение в другой каналdm('текст')личное сообщение вызвавшемуreact('✅')реакция на сообщение с командой или на панельnick_set('ник')сменить ник; пустой — сброситьtitle: 🎭 Выбери роли
description: Нажми кнопку, чтобы получить роль или снять её.
color: #5865F2
button: 🎮 Геймер | gamer | primary | ephemeral
button: 🎨 Художник | artist | secondary | ephemeral
{#action gamer}
{role_toggle('Геймер')}
{#if {has_role('Геймер')} == true}Роль «Геймер» снята.{#else}Роль «Геймер» выдана!{/if}
{/action}
{#action artist}
{role_toggle('Художник')}
{#if {has_role('Художник')} == true}Роль «Художник» снята.{#else}Роль «Художник» выдана!{/if}
{/action}
Обратите внимание: has_role в примере проверяется до того, как действие выполнится, поэтому показывает прошлое состояние. Отсюда и «снята» в первой ветке.
{#if {args.count} < 2}
Напиши так: `!жалоба @участник причина`
{#else}
{send_to('#модерация', '🚨 Жалоба от ' + {user.mention} + ' на ' + {args.1} + ': ' + {clean_mentions({args.2+})})}
{react('✅')}
Спасибо! Жалоба отправлена модераторам.
{/if}
Команда может вести файлы прямо в каналах сервера: заметки, журналы, таблицы результатов. Бот выкладывает файл сообщением с вложением, а при каждой записи обновляет это сообщение. Файл можно скачать и открыть в блокноте или Excel.
file_create('#канал', 'имя.txt', текст)создать новый файл; если он уже есть — ошибкаfile_save('#канал', 'имя.txt', текст)перезаписать файл, а если его нет — создатьfile_append('#канал', 'имя.txt', строка)дописать строку в конец (создаёт файл, если нет)file_load('#канал', 'имя.txt')текст файлаfile_delete('#канал', 'имя.txt')удалить файл и его сообщениеfile_copy('#откуда', 'имя.txt', '#куда')копия в другом каналеfile_move('#откуда', 'имя.txt', '#куда')перенести в другой каналfile_exists('#канал', 'имя.txt')true или falsefile_list('#канал')список имён файлов каналаКанал и имя файла
Канал пишут как #название, упоминанием <#ID> или числом ID. Слово здесь означает канал, где вызвали команду; file_list() без аргументов тоже смотрит в текущий канал.
Имя — буквы, цифры, _ и - (до 60 символов, без пробелов) и расширение:
.txtобычный текст.csvтаблица: строка — это ряд, ячейки через ; или ,.xlsxтаблица Excel; пишется так же, как csv. .xls тоже можно указать — файл сохранится как .xlsxРегистр в имени не важен: Notes.TXT и notes.txt — один файл. В разных каналах могут лежать файлы с одинаковыми именами, это разные файлы.
{#if {args.count} == 0}
{#if {file_exists('#заметки', 'notes.txt')} == true}📄 Заметки:
{file_load('#заметки', 'notes.txt')}{#else}Заметок пока нет — напиши `!заметка текст`.{/if}
{#else}
{file_append('#заметки', 'notes.txt', {user.name} + ': ' + {args})}
✅ Записал в **notes.txt** (канал #заметки). Теперь там:
{file_load('#заметки', 'notes.txt')}
{/if}
Tester: купить печенье
Функции, которые пишут (file_create, file_save, file_append, file_delete, file_copy, file_move), сами ничего не выводят. void для них не нужен.
Таблицы: csv и xlsx
Таблицу удобно пополнять через file_append: каждая строка — новый ряд, ячейки разделены ;. file_load возвращает текст таблицы в том же виде, так что его можно разобрать через split и {#for}. У xlsx числа становятся числами, остальное — текстом.
{file_append('#отчёты', 'продажи.csv', {user.name} + ';' + {args.1} + ';' + {args.2})}
title: 🧾 Продажа записана
description: Таблица **продажи.csv** в канале #отчёты:
```
{file_load('#отчёты', 'продажи.csv')}
```
color: #2ecc71
footer: Файл можно скачать из канала и открыть в Excel
Копирование и перенос
file_copy оставляет файл на месте и кладёт копию в другой канал, file_move убирает его из старого канала. Имя файла сохраняется.
{file_save('#черновики', 'план.txt', 'Шаг 1: придумать')}
{file_append('#черновики', 'план.txt', 'Шаг 2: сделать')}
{file_move('#черновики', 'план.txt', '#готово')}
Файлы в #готово: {join({file_list('#готово')}, ', ')}
Файлы в #черновики: {join({file_list('#черновики')}, ', ')}
{file_move('#готово', 'план.txt', '#готово')}
Файлы в #черновики:
⚠️ file_move: канал-источник и канал назначения совпадают (#готово)
Строки текстового файла
Файл можно читать и менять по строкам. Строки и символы считаются с 1, границы включительно. -1 (или last) — последняя строка, -2 — предпоследняя. Номер можно сдвинуть прямо в аргументе: 3+1, {get('n')}-1.
file_line('#канал', 'имя.txt', N)строка N целикомfile_line('#канал', 'имя.txt', N, a, b)символы строки N с a по bfile_line('#канал', 'имя.txt', N, a)строка N с символа a до концаfile_line('#канал', 'имя.txt', N, '', b)строка N от начала до символа bfile_lines('#канал', 'имя.txt')сколько строк в файлеfile_line_set('#канал', 'имя.txt', N, текст)заменить строку N; N на 1 больше числа строк — дописать в конецfile_line_insert('#канал', 'имя.txt', N, текст)вставить новую строку перед строкой Nfile_line_delete('#канал', 'имя.txt', N)удалить строку Nfile_replace('#канал', 'имя.txt', что, на_что)заменить все вхождения (с учётом регистра); выводит число заменfile_count('#канал', 'имя.txt', что)сколько раз встречается, без учёта регистра и в том числе внутри словУ file_count и file_replace есть режим 'word': тогда ищутся только целые слова с учётом регистра. file_count('#чат', 'log.txt', 'кот', 'word') не посчитает «котик» и «Кот». Чтобы искать в одной строке, укажите её номер: file_count('#чат', 'log.txt', 'кот', 3).
{file_save('#задачи', 'todo.txt', 'Купить хлеб')}
{file_append('#задачи', 'todo.txt', 'Позвонить маме')}
{file_append('#задачи', 'todo.txt', 'Сделать уроки')}
{file_line_set('#задачи', 'todo.txt', 2, '✅ Позвонить маме')}
{file_line_insert('#задачи', 'todo.txt', 1, '📋 Список дел')}
{set('n', {math({file_lines('#задачи', 'todo.txt')} + 1)})}
{#for i in {range(1, {get('n')})}}{i}. {file_line('#задачи', 'todo.txt', {i})}
{/for}
Первые 6 букв последней строки: «{file_line('#задачи', 'todo.txt', -1, 1, 6)}»
2. Купить хлеб
3. ✅ Позвонить маме
4. Сделать уроки
Первые 6 букв последней строки: «Сделат»
Ячейки таблицы csv и xlsx
Первая строка таблицы — заголовки столбцов. Ячейку можно указать двумя способами:
- по заголовку и номеру строки:
table_get('#канал', 'очки.csv', 'Очки', 2)— столбец «Очки», вторая строка под заголовками. Вместо заголовка можно написать букву столбца (B) или его номер; - по адресу, как в Excel:
table_get('#канал', 'очки.csv', 'B3'). Здесь строки считаются вместе с заголовками:B1— заголовок второго столбца.
table_get(…, 'Столбец', N)значение ячейкиtable_set(…, 'Столбец', N, значение)записать в ячейку; N на 1 больше числа строк — новая строкаtable_add_row(…, значение, значение, …)добавить строку в конец; выводит её номер (оберните в void)table_add_column(…, 'Заголовок', позиция)новый столбец; без позиции — в конецtable_delete_row(…, N)удалить строкуtable_delete_column(…, 'Столбец')удалить столбец со всеми значениямиtable_find(…, 'Столбец', значение)номер первой строки с таким значением, 0 — не найденоtable_rows(…) · table_columns(…)число строк под заголовками · список заголовковtable_column(…, 'Столбец')все значения столбца списком — удобно для {#for}table_range(…, 'A2:C5')значения диапазона: список строк, каждая — список ячеекtable_fill(…, 'A2:C5', значение)записать значение во все ячейки диапазона; пустое '' — очистить{file_save('#рейтинг', 'очки.csv', 'Игрок;Очки')}
{void(table_add_row('#рейтинг', 'очки.csv', 'Аня', 10))}
{void(table_add_row('#рейтинг', 'очки.csv', 'Борис', 7))}
{set('row', {table_find('#рейтинг', 'очки.csv', 'Игрок', 'Борис')})}
{table_set('#рейтинг', 'очки.csv', 'Очки', {get('row')}, {math({table_get('#рейтинг', 'очки.csv', 'Очки', {get('row')})} + 5)})}
Борис — строка {get('row')}, теперь у него {table_get('#рейтинг', 'очки.csv', 'Очки', {get('row')})} очков.
Ячейка B2 (как в Excel): {table_get('#рейтинг', 'очки.csv', 'B2')}
Все игроки: {join({table_column('#рейтинг', 'очки.csv', 'Игрок')}, ', ')}
Ячейка B2 (как в Excel): 10
Все игроки: Аня, Борис
Ошибки
Если что-то не так, вместо вызова в ответе появляется строка с ⚠️ — её видно и в эмуляторе, и в Discord. Ошибка будет, если:
- канала нет на сервере или это не текстовый канал;
- файла нет (
file_load,file_delete,file_copy,file_move); file_createвызван для файла, который уже есть (перезаписываетfile_save);- при копировании или переносе канал-источник совпадает с каналом назначения, или там уже есть файл с таким именем;
- у бота в канале нет прав видеть его, писать сообщения или прикреплять файлы;
- расширение не txt, csv или xlsx, или в имени есть пробелы и точки.
Через бесплатные API команда может брать данные из интернета: погоду, курсы, факты, картинки. Ответ API приходит в формате JSON, и из него нужно достать нужное поле.
http_get_json('https://адрес', 'путь.к.полю', 'если ошибка', ttl)
- Путь — где лежит значение:
current_weather.temperature, у массивов номер в скобках:data.items[0].name. - Запасное значение обязательно: сайты бывают недоступны. Для картинок запасной вариант должен быть рабочей ссылкой на другом сайте.
- ttl — сколько секунд хранить ответ, по умолчанию 180. Для API со случайным ответом (факты, картинки) ставьте
0, иначе три минуты будет приходить одно и то же.
{set('t', {http_get_json('https://api.open-meteo.com/v1/forecast?latitude=55.75&longitude=37.62¤t_weather=true', 'current_weather.temperature', '?', 300)})}
title: 🌤️ Погода в Москве
description: Температура: **{get('t')}°C**\nВетер: {http_get_json('https://api.open-meteo.com/v1/forecast?latitude=55.75&longitude=37.62¤t_weather=true', 'current_weather.windspeed', '?', 300)} км/ч
color: #3498db
footer: Данные: open-meteo.com
title: 🐱 Факт о кошках
description: {http_get_json('https://catfact.ninja/fact', 'fact', 'Сервис фактов сейчас недоступен.', 0)}
color: #f39c12
button: 🔄 Ещё факт | more | primary | edit
{#action more}
{/action}
Отправка данных: POST
http_post_json(адрес, данные, путь, запасное) отправляет JSON на сервер (форму заявки, бота-посредника, таблицу) и возвращает поле ответа по пути. Данные удобно собирать через json_stringify. Ответ POST никогда не кэшируется, а при ошибке придёт запасное значение — по нему удобно показать сообщение об ошибке.
{set('ok', {http_post_json('https://httpbin.org/post', {json_stringify({args})}, 'json', 'нет')})}
{#if {get('ok')} == 'нет'}
⚠️ Сервис не ответил, попробуйте позже.
{#else}
✅ Идея отправлена: **{clean_mentions({args})}**
{/if}
JSON без интернета
Имя: {json_get('{"name": "Артур", "stats": {"hp": 120, "items": ["меч", "щит"]}}', 'name')}
Здоровье: {json_get('{"name": "Артур", "stats": {"hp": 120, "items": ["меч", "щит"]}}', 'stats.hp')}
Первый предмет: {json_get('{"name": "Артур", "stats": {"hp": 120, "items": ["меч", "щит"]}}', 'stats.items[0]')}
Поиск: https://duckduckgo.com/?q={url_encode('котики и собаки')}
Здоровье: 120
Первый предмет: меч
Поиск: https://duckduckgo.com/?q=%D0%BA%D0%BE%D1%82%D0%B8%D0%BA%D0%B8%20%D0%B8%20%D1%81%D0%BE%D0%B1%D0%B0%D0%BA%D0%B8
Параметры, которые вставляются в адрес (город, поисковый запрос), пропускайте через url_encode, иначе пробелы и кириллица сломают ссылку. Для отправки данных есть http_post_json(адрес, данные, путь, запасное): его ответ никогда не кэшируется.
Холст рисует настоящую картинку: карточки профиля, баннеры, прогресс-бары. Картинка описывается командами рисования внутри блока {#canvas('имя')} и показывается через image: {canvas('имя')}.
{#canvas('card')}
set(size=600x200, background="#0e1728")
rect(x=10, y=10, w=580, h=180, fill="#162034", outline="#82e0ff", radius=18)
avatar(url="{user.avatar}", x=30, y=40, size=120, circle=true)
text("{user.display_name}", x=175, y=50, size=34, color="#eff4f8", font="bold")
text("Уровень 7 · 1 240 опыта", x=175, y=100, size=20, color="#a1acbb")
progress(x=175, y=140, w=380, h=18, current=62, max=100, fill="#82e0ff", bg="#243047", radius=9)
{/canvas}
title: 🪪 Карточка игрока
image: {canvas('card')}
color: #82e0ff
Команды рисования
set(size=600x200, background="#hex")размер и фон (первой строкой)rect(x, y, w, h, fill, outline, radius)прямоугольник, можно со скруглениемcirc(x, y, r, fill)кругline(x1, y1, x2, y2, color, width)линияtrian(x1, y1, x2, y2, x3, y3, fill)треугольникtext("…", x, y, size, color, font="bold")текстavatar(url, x, y, size, circle=true)аватаркаimg(url, x, y, w, h)картинка по ссылкеprogress(x, y, w, h, current, max, fill, bg, radius)полоса прогрессаВнутри холста работают переменные и функции: {user.display_name}, {store_get('xp', 0)}. Так картинка у каждого игрока своя.
Три способа запустить шаблон без того, чтобы кто-то вызывал команду: по времени, по событию на сервере и по сигналу извне.
Расписание
schedule_in('30m', 'текст')отправить сообщение через 30 минутschedule_cron('0 9 * * 1', 'текст')повторять по расписанию cron (UTC), здесь — по понедельникам в 9:00schedule_cancel(3)отменить задачу №3schedule_list()список задач сервера{schedule_in({args.1}, '⏰ ' + {user.mention} + ', напоминание: ' + {clean_mentions({args.2+})})}
Необязательный третий аргумент — ID канала, куда отправить сообщение. Все задачи видны и отменяются во вкладке «Задачи». Они не теряются при перезапуске бота. Повторяющиеся задачи запускаются не чаще раза в 5 минут.
Триггеры событий
Вкладка «Команды и триггеры» → «Триггеры». Триггер срабатывает на событие и выполняет свой шаблон или одну из ваших команд. Фильтр сужает срабатывание: по слову в сообщении (или /регулярке/), по эмодзи, по роли. Можно указать канал.
on_member_joinУчастник зашёлФильтр: —Данные: {user.*} — новичокon_member_removeУчастник вышелФильтр: —Данные: {user.*} — ушедшийon_messageСообщение в чатеФильтр: слово или /регулярка/Данные: {user.*} — автор, {message.content}on_message_editСообщение изменилиФильтр: слово или /регулярка/Данные: {old_content}, {new_content}, {jump_url}on_message_deleteСообщение удалилиФильтр: слово или /регулярка/Данные: {content}, {channel_name}on_reaction_addПоставили реакциюФильтр: эмодзиДанные: {user.*} — кто поставилon_reaction_removeСняли реакциюФильтр: эмодзиДанные: {user.*} — кто снялon_member_role_addВыдали рольФильтр: название или ID ролиДанные: {role}, {role_id}, {role_mention}on_member_role_removeСняли рольФильтр: название или ID ролиДанные: {role}, {role_id}, {role_mention}on_member_boostБуст сервераФильтр: —Данные: {boost_count}, {boost_level}on_nick_changeСмена никаФильтр: —Данные: {old_nick}, {new_nick}on_voice_state_updateВход/выход в голосовойФильтр: —Данные: {user.*}title: 👋 Новичок на сервере!
description: {user.mention}, добро пожаловать на **{guild.name}**!\nТы наш **{guild.member_count}**-й участник.
thumbnail: {user.avatar}
color: #2ecc71
{void(store_set('joined_at', {unix()}))}
title: ✏️ Сообщение изменено
description: **Было:** {old_content}\n**Стало:** {new_content}\n[Перейти к сообщению]({jump_url})
color: #f1c40f
{#if {message.content} contains 'помогите'}
🆘 {user.mention}, модераторы уже спешат на помощь! Опиши проблему подробнее.
{/if}
Входящие вебхуки
Создайте вебхук в той же вкладке, и вы получите секретный адрес вида https://сайт/api/webhook/<сервер>/<токен>. Любой сервис (GitHub, донаты, ваш скрипт) шлёт на него POST-запрос с JSON, а бот публикует шаблон в выбранный канал. Поля запроса доступны как {webhook.путь.к.полю}.
title: 🚀 Новый коммит
description: **{webhook.head_commit.message}**\nАвтор: {webhook.sender.login}
url: {webhook.head_commit.url}
color: #2ea44f
Когда команд много, одни и те же куски начинают повторяться: шапка профиля, расчёт уровня, формат цены. Макросы позволяют написать такой кусок один раз.
Макросы
- Панель → вкладка «Переменные» → «Макросы шаблонов» → создайте макрос, например
шапка. - В его шаблоне переданные значения доступны как
{arg1},{arg2}…:**{arg1}** · класс: {arg2}. - Вызывайте его из любой команды через
macro('имя', значение1, значение2).
Исправили макрос — исправились все команды, которые его вызывают. Внутри макроса работает весь язык: память, условия, функции.
Переменные сервера в панели
В той же вкладке видны все servervar_* сервера. Их можно создать и поменять руками: удобно для настроек, которые меняет админ, а читают команды (цена в магазине, текст объявления, ID канала для логов).
{servervar_get('shop_price', 100)} или короче {servervar.shop_price}
Не хотите писать код? В редакторе есть режим «Блоки»: команда собирается из цветных блоков, как в Scratch. Блоки и код — одна и та же команда в двух видах: переключайтесь когда угодно, ничего не потеряется.
Как это устроено
Полезные кнопки на панели блоков
- 📦 Данные команды — все ключи памяти, которые использует команда. Здесь же их можно переименовать сразу во всей команде.
- 📚 Рецепты — готовые команды (счётчик, рыбалка, роли, инвентарь…), которые можно открыть и переделать.
- 👁 Эмулятор — выдвижная панель с результатом, как в коде.
- ✨ Собрать с ИИ — описать задачу словами.
- Переменные — в категории «Переменные» кнопка «Создать переменную» заводит переменную команды (
set/get). - Отменить / Повторить — или Ctrl+Z / Ctrl+Y.
Что делать, если команда работает не так, как задумано.
Эмулятор
Справа от кода эмулятор показывает, что увидит игрок. В поле «Аргументы» можно написать то, что игрок напишет после команды. Есть готовые пресеты: упоминание, число, несколько слов. Кнопки в эмуляторе нажимаются по-настоящему. Память команды (store_*, таблицы лидеров) эмулятор ведёт от вашего имени на этом сервере, так что счётчики копятся между запусками; почистить их можно во вкладке «Данные игроков». Настоящие монеты и опыт бота (eco_*, xp_add) эмулятор не меняет.
Отладка по шагам
Кнопка Отладка в заголовке эмулятора включает журнал движка. После каждого запуска и нажатия кнопки под предпросмотром видно по порядку:
- какие переменные и функции посчитались и что они вернули (
get('n') → 5); - какие условия выполнились: «да → выполняется» или «нет → пропуск»;
- сколько элементов обошёл каждый цикл;
- итоговые значения переменных
set().
Вложенные блоки показаны с отступом, а шаги разбиты по частям: действие кнопки, текст сообщения, панель. Так проще всего найти, почему условие не сработало: обычно рядом видно, что функция вернула пустую строку или текст вместо числа.
Подсказки об ошибках
Редактор подчёркивает ошибки прямо в коде: незакрытую скобку или {#if} без {/if}, неизвестную функцию («может, вы имели в виду…»), кнопку без блока действия. В режиме «Блоки» такой блок подсвечивается красным. При сохранении команда проверяется ещё раз.
Автоподстановка скобок
Если в конце строки осталась незакрытая скобка или кавычка, после курсора появляется серая подсказка с недостающими символами, например )}. Нажмите Enter — они подставятся. Выражение {…} всегда пишется в одну строку, поэтому Enter посреди незакрытой скобки почти всегда означает «закрой». После строки с {#if …}, {#for …} или {#action …} без закрывающего тега подсказка предложит {/if} и оставит пустую строку для кода внутри. Esc скрывает подсказку, Tab по-прежнему выбирает вариант из списка автодополнения.
Горячие клавиши
На Mac вместо Ctrl — ⌘. Список всегда под рукой: Ctrl + / или кнопка с клавиатурой рядом с «Сохранить».
Ctrl + Sсохранить команду; в панели сервера — настройки открытой вкладкиCtrl + Enterзапустить команду в эмулятореEnterподставить серую подсказкуEscскрыть подсказку или списокCtrl + Spaceоткрыть автодополнениеTabвыбрать вариант автодополненияCtrl + B / I / Uжирный / курсив / подчёркиваниеCtrl + Dдублировать строкуAlt + ↑ / ↓переместить строкуCtrl + /окно со всеми горячими клавишамиНа странице фидбека Ctrl + Enter отправляет анкету или комментарий.
Команда работает в эмуляторе, но молчит в Discord
- Текстовые команды выключены во вкладке «Основное», или вы пишете не тот префикс.
- Ограничения команды: кулдаун, «только для ролей», «только в каналах», «активные часы» (они в UTC!).
- У бота нет прав писать или встраивать ссылки в этом канале, выдавать роль или его роль ниже выдаваемой.
- API не ответил вовремя: включите «Отложенный ответ».
- Посмотрите вкладку «Задачи» → «Журнал выполнений»: там записана каждая ошибка с текстом.
Вкладка «Аналитика»
Сколько раз вызывали каждую команду, сколько раз она сработала без ошибок, топ ошибок и самые активные игроки. Помогает понять, что людям нравится, а что ломается.
Проекты
Соберём настоящую игру за шесть шагов. Каждый шаг добавляет одну механику и опирается на уроки учебника. На каждом шаге можно остановиться: команда уже работает. Нажимайте кнопки в превью!
title: ⛏️ Шахта
description: Камней в сумке: **{store_get('stone', 0)}**
color: #7f8c8d
button: ⛏️ Копать | dig | primary | edit | only_author
{#action dig}
{void(store_inc('stone', 1))}
{/action}
Разная руда и шансы
Взвешенный random выбирает руду: 70% камень, 25% железо, 5% золото. Имя руды и есть ключ памяти, поэтому одна строка store_inc({get('ore')}, 1) работает для всех видов. Урок: случайность.
title: ⛏️ Шахта
description: 🪨 Камень: **{store_get('stone', 0)}** · ⚙️ Железо: **{store_get('iron', 0)}** · 🥇 Золото: **{store_get('gold', 0)}**
color: #7f8c8d
button: ⛏️ Копать | dig | primary | edit | only_author
{#action dig}
{set('ore', {random(['stone', 'iron', 'gold'], [70, 25, 5])})}
{void(store_inc({get('ore')}, 1))}
description: Вы нашли: **{switch({get('ore')}, 'stone', '🪨 камень', 'iron', '⚙️ железо', '🥇 ЗОЛОТО!')}**\n🪨 {store_get('stone', 0)} · ⚙️ {store_get('iron', 0)} · 🥇 {store_get('gold', 0)}
{/action}
Продажа: руда → монеты
Считаем стоимость через calc (помните правило про арифметику в аргументах?). Действие «Продать» пересчитывает её само, потому что set основной панели ему не виден. И проверяем, есть ли что продавать.
{set('worth', {calc({store_get('stone', 0)} + {store_get('iron', 0)} * 5 + {store_get('gold', 0)} * 25)})}
title: ⛏️ Шахта
description: 🪨 {store_get('stone', 0)} · ⚙️ {store_get('iron', 0)} · 🥇 {store_get('gold', 0)}\n💰 Монеты: **{store_get('coins', 0)}** · руда стоит **{get('worth')}**
color: #7f8c8d
button: ⛏️ Копать | dig | primary | edit | only_author
button: 💰 Продать всё | sell | success | edit | only_author
{#action dig}
{void(store_inc({random(['stone', 'iron', 'gold'], [70, 25, 5])}, 1))}
{/action}
{#action sell}
{set('worth', {calc({store_get('stone', 0)} + {store_get('iron', 0)} * 5 + {store_get('gold', 0)} * 25)})}
{#if {get('worth')} == 0}
description: 🎒 Продавать нечего — сначала покопайте.
{#else}
{void(store_inc('coins', {get('worth')}))}
{void(store_set('stone', 0))}{void(store_set('iron', 0))}{void(store_set('gold', 0))}
description: 💰 Продано на **{get('worth')}** монет! Баланс: **{store_get('coins', 0)}**
{/if}
{/action}
Прокачка кирки
Уровень кирки — это множитель добычи. Цена растёт квадратично: 50, 200, 450… Сначала проверка денег, потом списание. Урок: условия.
{set('pick', {store_get('pickaxe', 1)})}
{set('price', {calc({get('pick')} * {get('pick')} * 50)})}
title: ⛏️ Шахта · кирка ур. {get('pick')}
description: 🪨 {store_get('stone', 0)} · ⚙️ {store_get('iron', 0)} · 🥇 {store_get('gold', 0)} · 💰 **{store_get('coins', 0)}**\nЗа удар добываете **{get('pick')}** руды. Улучшение: **{get('price')}** 💰
color: #7f8c8d
button: ⛏️ Копать | dig | primary | edit | only_author
button: 💰 Продать | sell | success | edit | only_author
button: ⬆️ Улучшить кирку | up | secondary | edit | only_author
{#action dig}
{void(store_inc({random(['stone', 'iron', 'gold'], [70, 25, 5])}, {store_get('pickaxe', 1)}))}
{/action}
{#action sell}
{void(store_inc('coins', {calc({store_get('stone', 0)} + {store_get('iron', 0)} * 5 + {store_get('gold', 0)} * 25)}))}
{void(store_set('stone', 0))}{void(store_set('iron', 0))}{void(store_set('gold', 0))}
{/action}
{#action up}
{set('price', {calc({store_get('pickaxe', 1)} * {store_get('pickaxe', 1)} * 50)})}
{#if {store_get('coins', 0)} < {get('price')}}
description: ❌ Нужно **{get('price')}** 💰, у вас **{store_get('coins', 0)}**.
{#else}
{void(store_dec('coins', {get('price')}))}
{void(store_set('pickaxe', {calc({store_get('pickaxe', 1)} + 1)}))}
description: ⬆️ Кирка улучшена до **{store_get('pickaxe', 1)}** уровня!
{/if}
{/action}
Энергия, которая восстанавливается
Каждый удар тратит энергию, а она копится сама: +1 в минуту, максимум 10. Храним не энергию «сейчас», а сколько её было и когда, и досчитываем разницу при каждом показе. Никаких фоновых таймеров не нужно. Урок: время.
{set('now', {unix()})}
{set('energy', {min(10, {calc({store_get('energy', 10)} + ({get('now')} - {store_get('energy_at', {get('now')})}) // 60)})})}
title: ⛏️ Шахта
description: ⚡ Энергия: {progress_bar({get('energy')}, 10, 10)} **{get('energy')}/10**\n🪨 Камней: **{store_get('stone', 0)}** · +1 энергии в минуту
color: #7f8c8d
button: ⛏️ Копать (−1 ⚡) | dig | primary | edit | only_author
{#action dig}
{set('now', {unix()})}
{set('energy', {min(10, {calc({store_get('energy', 10)} + ({get('now')} - {store_get('energy_at', {get('now')})}) // 60)})})}
{#if {get('energy')} < 1}
description: 😮💨 Нет сил! Энергия восстановится {discord_time({calc({get('now')} + 60)}, 'R')}.
{#else}
{void(store_set('energy', {calc({get('energy')} - 1)}))}
{void(store_set('energy_at', {get('now')}))}
{void(store_inc('stone', 1))}
{/if}
{/action}
Соревнование
Каждый удар добавляет очко в общую таблицу сервера, а кнопка «Моё место» показывает соседей по рейтингу только нажавшему. Урок: таблицы лидеров.
title: ⛏️ Шахта
description: 🪨 Камней: **{store_get('stone', 0)}** · 📈 Ваш рекорд за сезон: **{lb_get('miners', '', 0)}**
field: 🏆 Лучшие шахтёры | {default({lb_top('miners', 5)}, 'Пока пусто — будьте первым!')} | false
color: #7f8c8d
button: ⛏️ Копать | dig | primary | edit | only_author
button: 📊 Моё место | rank | secondary | ephemeral
{#action dig}
{void(store_inc('stone', 1))}
{void(lb_add('miners', 1))}
{/action}
{#action rank}
Вы на **#{lb_rank('miners')}** месте. Ближайшие соперники:
{lb_near('miners', '', 1)}
{/action}
1. @Tester — 2
Магазин, в котором за монеты покупают роль на сервере и предметы в инвентарь. В нём сходятся память, условия, наборы и действия в Discord.
{#if {store_type('coins')} == ''}{void(store_set('coins', 300))}{/if}
title: 🛒 Магазин сервера
description: Ваш баланс: **{format_number({store_get('coins', 0)}, 'ru')}** 💰
field: 🎨 Цветной ник | 250 💰 — роль «Цветной» | true
field: 🧪 Зелье удачи | 100 💰 — в инвентарь | true
field: 🎒 Инвентарь | {default({set_values('items', ', ')}, 'пусто')} | false
color: #e67e22
button: 🎨 Купить ник | nick | primary | edit | only_author
button: 🧪 Купить зелье | potion | success | edit | only_author
{#action nick}
{#if {has_role('Цветной')} == true}
description: ✨ У вас уже есть цветной ник.
{#elif {store_get('coins', 0)} < 250}
description: ❌ Не хватает **{250 - {store_get('coins', 0)}}** 💰.
{#else}
{void(store_dec('coins', 250))}
{role_add('Цветной')}
description: 🎨 Готово! Роль «Цветной» выдана. Баланс: **{store_get('coins', 0)}** 💰
{/if}
{/action}
{#action potion}
{#if {store_get('coins', 0)} < 100}
description: ❌ Не хватает монет.
{#else}
{void(store_dec('coins', 100))}
{void(set_add('items', '🧪 Зелье удачи'))}
{/if}
{/action}
Разбор
- Стартовый капитал выдаётся один раз: проверка
store_typeв первой строке. - Три ветки покупки ника: роль уже есть → денег не хватает (показываем, сколько именно, через
{250 - {…}}) → покупка. - Роль выдаётся через
role_addпосле списания. Роль «Цветной» должна существовать, а роль бота должна стоять выше неё. - Инвентарь — это набор, так что одинаковые зелья не копятся дублями. Если нужны количества, заведите для зелий обычный счётчик:
store_inc('potions', 1). - Действие зелья без директив ничего не выводит, и панель просто перерисовывается с новым балансом и инвентарём.
Попробуйте сами Добавьте продажу
Кнопка «Продать зелье» возвращает 60 💰 и убирает зелье из инвентаря. Подсказка: set_has и set_remove.
{#if {store_type('coins')} == ''}{void(store_set('coins', 300))}{/if}
title: 🧪 Лавка зелий
description: 💰 **{store_get('coins', 0)}** · Инвентарь: {default({set_values('items', ', ')}, 'пусто')}
color: #1abc9c
button: 🧪 Купить (100 💰) | buy | success | edit | only_author
button: 💱 Продать (60 💰) | sell | secondary | edit | only_author
{#action buy}
{#if {store_get('coins', 0)} >= 100}{void(store_dec('coins', 100))}{void(set_add('items', '🧪 Зелье удачи'))}{/if}
{/action}
{#action sell}
{#if {set_has('items', '🧪 Зелье удачи')} == true}
{void(set_remove('items', '🧪 Зелье удачи'))}
{void(store_inc('coins', 60))}
{#else}
description: ❌ Продавать нечего.
{/if}
{/action}
Справочник
Все 190 функций движка. Список собирается прямо из движка: здесь есть каждая функция, которая реально работает, и нет несуществующих.
Вывод и переменные рендера
void(выражение, ...)
{void(store_inc('coins', 5))}silent(...)
pass(...)
set('имя', значение)
get('имя', 'дефолт')
{a + b}if(условие, 'да', 'нет')
{if({store_get('vip', 0)} == 1, '👑', '')}Случайность
random(мин, макс) | random(N) | random(['a','b'], [70, 30])
choice('a', 'b', ...)
pick('a', 'b', ...)
random_choice('a', 'b', ...)
range(стоп) | range(старт, стоп[, шаг])
Аргументы команды
arg(N)
arg(2)); проще писать переменную {args.2}args()
{args})join(список, 'разделитель')
join() — аргументы через пробелВремя (UTC)
now() | now('%H:%M', '+3h')
<t:…:f> (показывается в часовом поясе читателя); с форматом — strftime в UTC со сдвигомtime(...)
date('%d.%m.%Y', '+1d')
+30m, -2h, +1d, +1wunix('+1h')
timestamp(...)
time_diff(t1, t2, 'seconds|minutes|hours|days')
time_ago(unix)
discord_time(unix, 'R')
day_of_week(unix?)
format_date('+0', 'ru', '%d.%m')
Числа и математика
math(выражение)
+ - * / // % **, скобки, внутри только abs/round/min/max/int/float. Голый островок {a * 2 + 1} работает так жеcalc(выражение)
abs(x)
even(x)
odd(x)
round(x, знаков=0)
floor(x)
ceil(x)
min(a, b, ...)
max(a, b, ...)
clamp(x, мин, макс)
is_numeric(x)
percent(часть, всего, знаков=1)
percent(1, 3) → 33.3progress_bar(тек, макс=100, длина=10, '▰', '▱')
format(число, ',')
1,234,567; шаблоны {0} внутри format НЕ работаютformat_number(число, 'ru')
1 234 567 (ru) / 1,234,567 (en)to_int(x, дефолт)
to_float(x, дефолт)
to_bool(x)
Логика
default(x, 'запасное')
store_get(key, дефолт))or(x, 'запасное')
is_empty(x)
switch(значение, 'в1', 'р1', 'в2', 'р2', 'иначе')
coalesce(a, b, c)
Строки
split('a,b,c', ',', индекс?)
replace(текст, 'что', 'на что', сколько?)
contains(текст, 'часть')
x contains 'y')startswith(текст, 'нач')
endswith(текст, 'кон')
lower(текст)
upper(текст)
capitalize(текст)
title(текст)
trim(текст, 'символы'?)
strip(...)
len(текст|JSON-список)
length(...)
slice(текст, от, до)
substr(текст, от, длина)
pad_left('7', 3, '0')
007pad_right('hi', 5, '.')
hi...repeat('🟩', n)
reverse(текст)
truncate(текст, 50, '…')
escape_md(текст)
plural_ru(n, 'монета', 'монеты', 'монет')
clean_mentions(текст)
url_encode(текст)
hash_md5(текст)
hash_sha256(текст)
base64_encode(текст)
base64_decode(текст)
Регулярные выражения (флаги: i, m, s)
regex_match(текст, 'шаблон', 'i')
regex_find(текст, 'шаблон', группа=0, 'i')
regex_replace(текст, 'шаблон', 'замена', 'i')
JSON и HTTP
json_parse(строка)
json_get(json, 'a.b[0].c', 'дефолт')
json_stringify(x)
http_get_json('https://…', 'путь[0].ключ', 'fallback', ttl=180)
http_post_json('https://…', '{"k":"v"}', 'путь', 'fallback')
Списки-значения (JSON-список, ключ store со списком или 'a,b,c')
list_create('a', 'b', 'c')
list_contains(список, 'x')
list_index(список, 'x')
list_count(список, 'x')
list_random(список)
list_shuffle(список)
list_sort(список, 'asc|desc')
list_reverse(список)
list_unique(список)
list_first(список)
list_last(список)
Циклы
for(список, '{index}. {item}', ' | ')
each(...)
Личное хранилище игрока (изолировано по серверу И пользователю)
store_get('ключ', дефолт)
store_set('ключ', значение, ttl=0)
{void(store_set(...))}; ttl — срок жизни в секундахstore_inc('ключ', n=1)
store_dec('ключ', n=1)
store_mul('ключ', k)
store_del('ключ')
okstore_type('ключ')
store_ttl('ключ')
store_expire('ключ', сек)
okstore_persist('ключ')
okstore_scan('префикс', 10, 0, 'self|all', '\n')
ключ = значение (all — всех игроков сервера)store_sort('префикс', 10, 0, true, 'global|self', '\n')
store_audit('префикс', 10, 0, 'self|all', '\n')
list_push('ключ', значение, макс_длина=0)
list_pop('ключ', 'right|left')
list_remove('ключ', значение, сколько=1)
list_set('ключ', индекс, значение)
list_get('ключ', индекс, 'дефолт')
list_slice('ключ', от, до, ', ')
list_len('ключ')
set_add('ключ', значение)
set_remove('ключ', значение)
set_has('ключ', значение)
set_values('ключ', ', ')
set_size('ключ')
Таблицы лидеров (общие для сервера)
lb_add('доска', очки=1, @игрок?)
lb_set('доска', очки, @игрок?)
lb_get('доска', @игрок?, 'дефолт')
lb_top('доска', 10, 0, '\n')
1. <@id> — 120lb_rank('доска', @игрок?)
lb_near('доска', @игрок?, 2, '\n')
lb_reset('доска', 'Сезон 1')
lb_seasons('доска', 5)
lb_season_top('доска', 'id|метка', 10)
Общие данные сервера и макросы
servervar_get('ключ', 'дефолт')
servervar_set('ключ', значение)
servervar_del('ключ')
okmacro('имя', a1, a2)
Данные одной панели (у каждого отправленного сообщения-панели свои; для игр на двоих, голосований)
msg_get('ключ', 'дефолт')
msg_set('ключ', значение)
msg_inc('ключ', 1)
msg_del('ключ')
okФайлы в каналах (txt, csv, xlsx; бот держит файл сообщением с вложением в канале и обновляет его после команды)
file_create(#канал, 'имя.txt', текст?)
file_save(#канал, 'имя.txt', текст)
file_append(#канал, 'log.csv', строка)
file_load(#канал, 'имя.txt')
file_delete(#канал, 'имя.txt')
file_copy(#откуда, 'имя.txt', #куда)
file_move(#откуда, 'имя.txt', #куда)
file_exists(#канал, 'имя.txt')
file_list(#канал)
Строки текстового файла (номера с 1; -1 — последняя; можно 3+1)
file_line(#канал, 'имя.txt', N, начало?, конец?)
file_lines(#канал, 'имя.txt')
file_line_set(#канал, 'имя.txt', N, текст)
file_line_insert(#канал, 'имя.txt', N, текст)
file_line_delete(#канал, 'имя.txt', N)
file_replace(#канал, 'имя.txt', что, замена, 'word'?)
file_count(#канал, 'имя.txt', что, N?, 'word'?)
Ячейки таблицы csv/xlsx (первая строка — заголовки; строки данных с 1)
table_get(#канал, 't.csv', 'Столбец', N) | table_get(#канал, 't.csv', 'B3')
table_set(#канал, 't.csv', 'Столбец', N, значение) | table_set(#канал, 't.csv', 'B3', значение)
table_add_row(#канал, 't.csv', значение, значение, …)
table_add_column(#канал, 't.csv', 'Заголовок', позиция?)
table_delete_row(#канал, 't.csv', N)
table_delete_column(#канал, 't.csv', 'Столбец')
table_find(#канал, 't.csv', 'Столбец', значение)
table_rows(#канал, 't.csv')
table_columns(#канал, 't.csv')
table_column(#канал, 't.csv', 'Столбец')
table_range(#канал, 't.csv', 'A2:C5')
table_fill(#канал, 't.csv', 'A2:C5', значение)
Экономика, уровни и кланы бота (общие с /баланс, /ранг, /клан)
eco_balance('wallet' | 'bank' | 'total', @игрок?)
eco_add(монеты, @игрок?)
eco_take(монеты, @игрок?)
{#if {eco_take(100)} == true}eco_pay(@кому, монеты)
level('level' | 'xp' | 'need' | 'total' | 'rank', @игрок?)
xp_add(опыт, @игрок?)
clan_name(@игрок?)
clan_tag(@игрок?)
clan_role(@игрок?)
Права
has_role('Название' | ID | '<@&ID>')
has_perm('administrator' | 'manage_messages' | …)
Действия в Discord (выполняются после рендера, ничего не печатают; до 5 за запуск)
role_add('Роль' | ID | '<@&ID>', @участник?)
role_remove('Роль', @участник?)
role_toggle('Роль', @участник?)
send_to('#канал' | ID, 'текст')
dm('текст', @участник?)
react('✅')
nick_set('ник', @участник?)
Холст и расписание
canvas('имя')
image: {canvas('имя')}schedule_in('10m', 'текст', ID_канала?)
schedule_cron('0 12 * * *', 'текст', ID_канала?)
schedule_cancel(N)
schedule_list()
Директивы панели
title: текстзаголовок карточкиurl: https://…ссылка на заголовкеdescription: текстописание, перенос — \ncolor: #5865F2цвет полосыfield: Имя | Значение | true/falseполе, true — в рядthumbnail: URLкартинка справаimage: URLбольшая картинкаauthor: Имя | URL | иконкаавтор над заголовкомfooter: Текст | иконкаподписьtimestamp: nowвремя в подписиbutton: Текст | ключ | стиль | режим | only_author | only=@игрок | disabledкнопкаnewline:следующие кнопки — с новой строкиmodal: ключ | Заголовок | Поле*, >Длинное | режимформа для modal-кнопкиselect: Подсказка | А, Б, Ввыпадающее менюselect_action: А | ответответ на выбор пунктаБлоки
{#if усл} … {#elif усл} … {#else} … {/if}условие{#for x in {список}} … {x} … {/for}цикл{#action ключ} … {/action}что делает кнопка / форма{#canvas('имя')} … {/canvas}рисунок, показ — image: {canvas('имя')}{# комментарий #}заметка в коде, в ответ не попадаетПеременные
{user} {user.mention} {user.name} {user.display_name} {user.id} {user.avatar}вызвавший команду (member.* — то же){user.tag} {user.nick} {user.color} {user.top_role} {user.created_at} {user.joined_at} {user.bot}ещё о нём{guild.name} {guild.id} {guild.icon} {guild.member_count} {guild.owner_id}сервер (server.* — то же){guild.roles_count} {guild.channels_count} {guild.boost_count} {guild.boost_level} {guild.description}ещё о сервере{channel.mention} {channel.name} {channel.id} {channel.topic}канал{args} {args.1} {args.2+} {args.count} {arg1}аргументы{args.опция} {args.опция.id} {args.опция.name}слэш-опции{modal_1} … {modal_5}ответы формы{item} {index}внутри for(){servervar.ключ}переменная сервера{owner} {owner_id}кто открыл панель (в действиях кнопок {user} — кто нажал){webhook.путь}данные входящего вебхука{message.content}текст сообщения (текстовая команда, on_message)События для триггеров
on_member_joinУчастник зашёлФильтр: —Данные: {user.*} — новичокon_member_removeУчастник вышелФильтр: —Данные: {user.*} — ушедшийon_messageСообщение в чатеФильтр: слово или /регулярка/Данные: {user.*} — автор, {message.content}on_message_editСообщение изменилиФильтр: слово или /регулярка/Данные: {old_content}, {new_content}, {jump_url}on_message_deleteСообщение удалилиФильтр: слово или /регулярка/Данные: {content}, {channel_name}on_reaction_addПоставили реакциюФильтр: эмодзиДанные: {user.*} — кто поставилon_reaction_removeСняли реакциюФильтр: эмодзиДанные: {user.*} — кто снялon_member_role_addВыдали рольФильтр: название или ID ролиДанные: {role}, {role_id}, {role_mention}on_member_role_removeСняли рольФильтр: название или ID ролиДанные: {role}, {role_id}, {role_mention}on_member_boostБуст сервераФильтр: —Данные: {boost_count}, {boost_level}on_nick_changeСмена никаФильтр: —Данные: {old_nick}, {new_nick}on_voice_state_updateВход/выход в голосовойФильтр: —Данные: {user.*}Ограничения защищают бота: одна тяжёлая или ошибочная команда не должна тормозить его на всех серверах. Когда команда упирается в предел, она не ломается — лишнее просто не выполняется, а эмулятор в редакторе показывает, что сработало.
Команда и сообщение
Выполнение
{#if}, {#for}…Циклы
forwhileИнтернет и API
Действия в Discord
send_to и dmПамять
servervar_*)store_*)msg_*) × символов в значенииФайлы в каналах
Картинки
Автоматизация
Нейросеть (Silent AI и /нейросеть)
Изменить уже существующую запись можно всегда. Лимит памяти мешает только создать новую: она не сохранится, а эмулятор в редакторе покажет предупреждение.
Частые вопросы
Почему в карточке появилась лишняя цифра?
Функция записи (store_inc, list_push, lb_add…) напечатала свой результат. Оберните её в {void(…)}.
Почему {set} не работает в кнопке?
Действие кнопки выполняется отдельно и не видит set() основной панели. Вычислите значение внутри {#action}, а то, что нужно помнить между нажатиями, храните в памяти (store_*).
Почему условие всегда ложное или всегда истинное?
Если в условии есть арифметика, оберните её в calc: {#if {calc({a} - {b})} > 10}. Значения функций и переменных пишутся в своих фигурных скобках.
Почему у игрока 0, хотя я написал store_get('hp', 100)?
Второй аргумент store_get только показывается и не записывается. Стартовые значения записывайте один раз, при первом запуске (см. урок «Память»).
Как сделать команду только для админов?
В настройках команды выберите «Только для ролей» или проверяйте внутри: {#if {has_perm('administrator')} == true}.
Кнопку нажимают чужие люди
Добавьте only_author в строку кнопки, а если кнопка для конкретного игрока — only={args.1}.
Бот не выдаёт роль
Роль бота должна быть выше выдаваемой роли. Роли с правами модерации выдавать через команды нельзя.
Картинка из API всё время одна и та же
Поставьте ttl = 0 четвёртым аргументом http_get_json: ответ кэшируется на 3 минуты.
Как перенести команды на другой сервер?
«Экспорт» и «Импорт» во вкладке команд, ссылка «Поделиться» в редакторе или галерея.
Сломал команду. Как вернуть?
Кнопка истории версий в редакторе: каждое сохранение можно восстановить.
Вы дочитали до конца
Осталось открыть редактор и собрать что-то своё. Если что-то не получается, в редакторе есть Silent AI: опишите, что хотите сделать.
К моим серверам