TikTok Raid: Concept
Концепт интерактивного TikTok стрима для привлечения игроков на Rust сервер.
1. Summary
Goal: Зрители TikTok LIVE совместно разрушают дом в Rust через лайки, выбирают сторону атаки через комментарии. При победе — промокод на экране.
User Value: Интерактивный опыт → интерес → переход на сервер за наградой.
2. Голосование: как это работает
Механика
[Голосование началось - 15 сек]
Напиши в чат:
1 - Атака СЛЕВА
2 - Атака СПРАВА
3 - Атака СВЕРХУ
Голосов: 1️⃣ 234 | 2️⃣ 156 | 3️⃣ 89
[Таймер: 12...11...10...]
Реализация в Bridge Service
class VotingSystem {
votes = { 1: 0, 2: 0, 3: 0 };
voters = new Set(); // Один голос на юзера
isActive = false;
startVoting(durationSec = 15) {
this.votes = { 1: 0, 2: 0, 3: 0 };
this.voters.clear();
this.isActive = true;
// Отправить на Rust: показать UI голосования
sendToRust({ action: 'VOTE_START', duration: durationSec });
setTimeout(() => this.endVoting(), durationSec * 1000);
}
processComment(username, text) {
if (!this.isActive) return;
if (this.voters.has(username)) return; // Уже голосовал
const vote = parseInt(text.trim());
if ([1, 2, 3].includes(vote)) {
this.votes[vote]++;
this.voters.add(username);
// Обновить счётчики на Rust
sendToRust({ action: 'VOTE_UPDATE', votes: this.votes });
}
}
endVoting() {
this.isActive = false;
// Найти победителя
const winner = Object.entries(this.votes)
.sort((a, b) => b[1] - a[1])[0][0];
sendToRust({
action: 'VOTE_END',
winner: parseInt(winner),
votes: this.votes
});
}
}
На стороне Rust плагина
private int _currentAttackSide = 1; // 1=left, 2=right, 3=top
private void OnVoteEnd(int winner, Dictionary<int, int> votes)
{
_currentAttackSide = winner;
// Показать результат
ShowFloatingText($"Победила сторона {winner}! ({votes[winner]} голосов)");
// Переместить "прицел" на эту сторону
UpdateAttackDirection(winner);
}
private void SpawnExplosive(string type)
{
var position = GetAttackPosition(_currentAttackSide);
var rotation = GetAttackRotation(_currentAttackSide);
// Спавн взрывчатки летящей в дом с выбранной стороны
var explosive = CreateExplosive(type, position, rotation);
explosive.Spawn();
}
3. Полная механика
Цикл игры
┌─────────────────────────────────────────────────────────────┐
│ РАУНД │
├─────────────────────────────────────────────────────────────┤
│ │
│ [0:00-0:15] Голосование за сторону атаки │
│ 💬 "1" "2" "3" в чат │
│ │
│ [0:15-1:00] Фаза атаки │
│ ❤️ Лайки = накопление взрывчатки │
│ 💬 Любой текст = починка дома │
│ Взрывчатка летит с выбранной стороны │
│ │
│ [1:00] Новое голосование → повтор │
│ │
│ [HP <= 0] 🎉 ПОБЕДА → Промокод → Респавн дома │
│ │
└─────────────────────────────────────────────────────────────┘
Система накопления (все параллельно)
1 лайк = +1 ко ВСЕМ счётчикам одновременно
Propane Tank: 1,000 лайков → ~50 урона
Satchel: 3,000 лайков → ~90 урона
Rocket: 5,000 лайков → ~140 урона
C4: 10,000 лайков → ~275 урона
MLRS: 50,000 лайков → ~500 урона (спец. эффект)
Что происходит при 10,000 лайках
Propane: 10,000/1,000 = 10 штук выброшено (по мере накопления)
Satchel: 10,000/3,000 = 3 штуки
Rocket: 10,000/5,000 = 2 штуки
C4: 10,000/10,000 = 1 штука
Общий урон: 10×50 + 3×90 + 2×140 + 1×275 = 500 + 270 + 280 + 275 = 1,325
Баланс дома
Стена (одна сторона): ~800 HP
Шкаф (TC): ~2,000 HP
Чтобы пробить стену: ~6,000 лайков (без починки)
Чтобы сломать шкаф: ~15,000 лайков (без починки)
С учётом починки: ~25,000-40,000 лайков за раунд
Пул домов: баланс по онлайну
Сложность дома выбирается автоматически на основе текущего онлайна стрима.
Принцип: Чем больше зрителей → тем прочнее дом → раунд длится примерно одинаково.
| Онлайн | Дом | HP | Время раунда |
|---|---|---|---|
| < 200 | 🏚️ Хижина | 1,000 | ~5-8 мин |
| 200-500 | 🏠 Дом | 2,000 | ~5-8 мин |
| 500-1000 | 🏡 Укреплённый дом | 4,000 | ~5-8 мин |
| 1000-3000 | 🏰 База | 8,000 | ~5-8 мин |
| 3000+ | 🏯 Крепость | 15,000 | ~5-8 мин |
Конфиг плагина:
{
"houses": [
{ "name": "Хижина", "minOnline": 0, "maxOnline": 199, "hp": 1000 },
{ "name": "Дом", "minOnline": 200, "maxOnline": 499, "hp": 2000 },
{ "name": "Укреплённый дом", "minOnline": 500, "maxOnline": 999, "hp": 4000 },
{ "name": "База", "minOnline": 1000, "maxOnline": 2999, "hp": 8000 },
{ "name": "Крепость", "minOnline": 3000, "maxOnline": 999999, "hp": 15000 }
]
}
Логика выбора:
- При старте раунда Bridge отправляет текущий онлайн
- Плагин выбирает дом из пула по диапазону
- Название дома показывается в Header: "Зарейди Хижину и получи кейс"
Почему не голосование:
- Проще реализация
- Автоматический баланс без участия зрителей
- Голосование за сложность — фича для будущего
4. CUI Design: Полный макет
Общий вид экрана
┌────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 🎁 Зарейди "Дом Шершень" и получи кейс с реальными скинами │ │
│ │ [КЕЙС] │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ HP: 1,847 / 2,000 │ │
│ │ ██████████████████████░░░░░░░░░░░░░░░░ │ │
│ │ [зелёный ←――――――――――→ красный] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ │
│ [ИГРОВАЯ СЦЕНА] │
│ 🏠 Дом │
│ │
│ │
│ ┌──────────────────────┐ │
│ │ 💣 ВЗРЫВЧАТКА │ │
│ │ │ │
│ │ 🛢️ Propane 47/100 │ │
│ │ [████████░░░░░░░░░] │ │
│ │ │ │
│ │ 💼 Satchel 234/1000│ │
│ │ [██░░░░░░░░░░░░░░░] │ │
│ │ │ │
│ │ 🚀 Rocket 89/3000 │ │
│ │ [░░░░░░░░░░░░░░░░░] │ │
│ │ │ │
│ │ 💣 C4 12/5000 │ │
│ │ [░░░░░░░░░░░░░░░░░] │ │
│ │ │ │
│ │ ❤️ ЛАЙК = УРОН │ │
│ └──────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
4.1 Header: Призыв к действию
┌─────────────────────────────────────────────────────────────────┐
│ │
│ 🎁 Зарейди "Дом Шершень" и получи кейс с реальными скинами │
│ │
│ ┌─────────┐ │
│ │ ┌───┐ │ ← Иконка кейса │
│ │ │ ? │ │ в рамке │
│ │ └───┘ │ │
│ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| Элемент | Описание |
|---|---|
| Текст призыва | Крупный, читаемый шрифт. Белый на полупрозрачном тёмном фоне |
| "Дом Шершень" | Название дома, можно менять в конфиге плагина |
| Иконка кейса | Изображение кейса goLoot в аккуратной рамке |
| Рамка | Тонкая, светлая, без лишних украшений — заметно, но не вычурно |
Позиция: Верхняя часть экрана, по центру
Anchor: AnchorMin="0.25 0.88", AnchorMax="0.75 0.98"
4.2 HP Bar: Здоровье дома
┌─────────────────────────────────────────────────────────────┐
│ │
│ HP: 1,847 / 2,000 │
│ │
│ ██████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ [――――― зелёный ―――――][―――――――― красный ――――――――] │
│ │
└─────────────────────────────────────────────────────────────┘
| Элемент | Описание |
|---|---|
| Числа | текущий HP / максимум HP — крупный шрифт под баром |
| Полоса здоровья | Заполняется справа налево по мере урона |
| Цвет заполненной части | Зелёный #33CC33 |
| Цвет пустой части | Красный #CC3333 (показывает "потерянное" здоровье) |
| Фон бара | Тёмный полупрозрачный rgba(0,0,0,0.6) |
Логика цвета:
100% HP → полностью зелёный
50% HP → половина зелёная, половина красная
0% HP → полностью красный (дом разрушен)
Позиция: Под призывом, по центру
Anchor: AnchorMin="0.3 0.78", AnchorMax="0.7 0.87"
4.3 Explosive Counters: Счётчики взрывчатки
┌──────────────────────────┐
│ 💣 ВЗРЫВЧАТКА │
│ │
│ 🛢️ Propane 47/100 │
│ [████████░░░░░░░░░░░░░] │
│ │
│ 💼 Satchel 234/1000 │
│ [██░░░░░░░░░░░░░░░░░░░] │
│ │
│ 🚀 Rocket 89/3000 │
│ [░░░░░░░░░░░░░░░░░░░░░] │
│ │
│ 💣 C4 12/5000 │
│ [░░░░░░░░░░░░░░░░░░░░░] │
│ │
│ ❤️ ЛАЙК = УРОН │
└──────────────────────────┘
| Элемент | Описание |
|---|---|
| Заголовок | "💣 ВЗРЫВЧАТКА" — название секции |
| Иконка | Emoji или sprite типа взрывчатки |
| Название | Propane, Satchel, Rocket, C4 |
| Счётчик | текущие лайки / порог срабатывания |
| Прогресс-бар | Заполняется по мере накопления лайков |
| Подсказка | "❤️ ЛАЙК = УРОН" — инструкция для зрителей |
Цвета прогресс-баров:
| Тип | Цвет заполнения | HEX |
|---|---|---|
| Propane | Жёлтый | #FFCC00 |
| Satchel | Оранжевый | #FF9933 |
| Rocket | Красный | #FF3333 |
| C4 | Тёмно-красный | #CC0000 |
Позиция: Левая часть экрана, вертикально
Anchor: AnchorMin="0.01 0.25", AnchorMax="0.18 0.65"
4.4 Синхронизация и обновление
Best Practice для плавности:
| Компонент | Интервал обновления | Причина |
|---|---|---|
| HP Bar | 300ms | Плавное изменение, не дёргается |
| Счётчики взрывчатки | 300ms | Синхронно с HP |
| Голосование | 300ms | Real-time ощущение |
| Призыв (header) | Статичный | Не меняется во время игры |
Batching:
- Все данные приходят пакетом каждые 300ms
- CUI обновляется одним вызовом, не по элементам
- Избегаем "мерцания" при частых обновлениях
Анимации:
- HP Bar: плавный transition (если поддерживается)
- Счётчики: мгновенное обновление (числа не анимируем)
- При срабатывании взрывчатки: короткая вспышка на баре
4.5 Компактная версия (альтернатива)
Для менее загруженного экрана:
┌────────────────────────────────────────────────────────────┐
│ 🎁 Зарейди дом — получи кейс! HP: 1,847/2,000 │
│ ███████████░░░░░░░░ │
├────────────────────────────────────────────────────────────┤
│ │
│ [ИГРОВАЯ СЦЕНА] │
│ │
├────────────────────────────────────────────────────────────┤
│ 🛢️ 47/100 💼 234/1K 🚀 89/3K 💣 12/5K ❤️=урон │
└────────────────────────────────────────────────────────────┘
Когда использовать: Если основной вариант занимает слишком много места или отвлекает от геймплея.
4.6 Victory Screen: Экран победы
Появляется когда HP дома достигает 0.
┌────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ 🎉 ПОЗДРАВЛЯЕМ! ВЫ ЗАРЕЙДИЛИ ДОМ! 🎉 │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ │ │ ┌────┐ │ Чтобы забрать кейс с реальным скином, │ │
│ │ │ │ 🎁 │ │ заходите на сервер и вводите команду: │ │
│ │ │ └────┘ │ │ │
│ │ └──────────┘ │ │
│ │ [КЕЙС] ┌─────────────────────────┐ │ │
│ │ │ /promo XXXXX │ │ │
│ │ └─────────────────────────┘ │ │
│ │ │ │
│ │ ✨ Кейс содержит ГАРАНТИРОВАННУЮ награду! │ │
│ │ │ │
│ ├────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌──────────────┐ IP сервера: │ │
│ │ │ │ ┌─────────────────────────┐ │ │
│ │ │ [ЛОГО/ │ │ play.server.com:28015 │ │ │
│ │ │ БАННЕР] │ └─────────────────────────┘ │ │
│ │ │ │ │ │
│ │ └──────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ Новый раунд через: 15 сек │
│ │
└────────────────────────────────────────────────────────────────────────┘
Элементы Victory Screen
| Элемент | Описание |
|---|---|
| Заголовок | "🎉 ПОЗДРАВЛЯЕМ! ВЫ ЗАРЕЙДИЛИ ДОМ! 🎉" — крупный, праздничный |
| Иконка кейса | Изображение награды в рамке слева |
| Инструкция | "Чтобы забрать кейс... заходите на сервер и вводите команду" |
| Промокод | /promo XXXXX — в отдельной рамке, крупный шрифт, легко читается |
| Гарантия | "✨ Кейс содержит ГАРАНТИРОВАННУЮ награду!" — важный selling point |
| Лого/баннер сервера | Изображение слева внизу (128x128 или 256x128) |
| IP сервера | В рамке, рядом с логотипом, легко скопировать визуально |
| Таймер | "Новый раунд через: 15 сек" — информирует о перезапуске |
Стилизация
| Компонент | Стиль |
|---|---|
| Фон окна | Тёмный полупрозрачный rgba(0,0,0,0.85) |
| Рамка промокода | Яркая, контрастная (золотая или зелёная) |
| Рамка IP | Нейтральная, серая |
| Текст промокода | Крупный, моноширинный шрифт для читаемости |
| Иконка кейса | С небольшим glow-эффектом (если поддерживается) |
Позиционирование
Victory Screen: AnchorMin="0.15 0.15", AnchorMax="0.85 0.85"
Занимает центральную часть экрана, перекрывает игровую сцену.
Тайминги
| Событие | Время |
|---|---|
| HP = 0 | Victory Screen появляется |
| +0.5 сек | Звук победы (fireworks/celebration) |
| +60 сек | Victory Screen скрывается |
| +60 сек | Дом респавнится, счётчики обнуляются |
| +61 сек | Новый раунд начинается |
Почему 60 секунд:
- Даёт время зрителям прочитать промокод и IP
- Позволяет сделать скриншот
- Создаёт "момент победы" для аудитории
Генерация промокода
- Генерируется автоматически при победе
- Формат: 5-6 символов (буквы + цифры), например
RAID7X - Одноразовый: активируется только один раз
- Срок действия: 24-48 часов (конфигурируется)
5. Особые события
MLRS (50,000 лайков)
Это должен быть момент. Когда накопилось:
private void LaunchMLRS()
{
// Затемнение экрана
ShowDramaticUI("🎯 MLRS INCOMING!");
// Звук сирены
PlaySound("assets/prefabs/npc/mlrs/effects/launch.prefab");
// Задержка 3 сек для драмы
timer.Once(3f, () => {
// Спавн нескольких ракет MLRS
for (int i = 0; i < 12; i++)
{
timer.Once(i * 0.2f, () => {
SpawnMLRSRocket(GetRandomPositionAroundHouse());
});
}
});
}
6. Защита (комментарии)
Механика
Любой комментарий (кроме "1", "2", "3" во время голосования) = починка
Базовая починка: +5 HP
Бонус за длину:
1-5 символов: +5 HP
6-15 символов: +10 HP
16-30 символов: +20 HP
31+ символов: +30 HP
Rate limit
1 комментарий в 2 секунды на юзера
Иначе спам-боты сломают баланс
Отображение
Когда кто-то чинит:
🔧 @username: +15 HP
Floating text над домом:
"+15 🔧" (зелёный, fade out)
7. Резюме механики
| Действие | Что делает | Как часто |
|---|---|---|
| ❤️ Лайк | +1 ко всем счётчикам взрывчатки | Всегда |
| 💬 "1"/"2"/"3" | Голос за сторону | Во время голосования |
| 💬 Любой текст | Починка дома | Вне голосования |
| Фаза | Длительность | Что происходит |
|---|---|---|
| Голосование | 15 сек | Выбор стороны атаки |
| Атака | 45 сек | Лайки → урон, комменты → починка |
| Повтор | — | Каждую минуту новое голосование |
8. Архитектура (высокоуровневая)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ TikTok LIVE │ │ Bridge Service │ │ Rust Server │
│ │ │ (Node.js/TS) │ │ │
│ Лайки │────▶│ Aggregate │◀═══▶│ GoLootTikTok │
│ Комментарии │ WS │ Rate limit │ WS │ plugin │
│ │ │ Voting logic │ │ │
└─────────────────┘ │ Batching │ └─────────────────┘
└─────────────────┘ │
│ ▼
│ ┌─────────────────┐
│ │ Игровая сцена │
▼ │ - Spawn C4 │
┌─────────────────┐ │ - Apply damage │
│ Signing Server │ │ - Show CUI │
│ (Euler Stream) │ │ - Promo code │
└─────────────────┘ │ - QR code │
└─────────────────┘
9. Технические зависимости
| Компонент | Технология | Описание |
|---|---|---|
| TikTok Events | TikTok-Live-Connector | Node.js библиотека |
| Signing Server | Euler Stream | Обязательно в 2026 — без него TikTok блокирует соединение через 5-10 мин |
| Bridge Service | Node.js + TypeScript | Агрегация, rate limit, голосование, batching |
| Rust Plugin | Oxide/uMod C# | Игровая логика, CUI, WebSocket server |
| Коммуникация | WebSocket | Одно постоянное соединение Bridge ↔ Plugin (не HTTP!) |
10. Signing Server: бесплатно vs платно
Варианты
| Вариант | Стоимость | Стабильность | Для кого |
|---|---|---|---|
| Bundled Signer | Бесплатно | Лимиты, может отвалиться при хайпе | Тесты, первые стримы |
| Euler Stream | ~$10-20/мес | Без лимитов, 24/7 | Продакшн, большие стримы |
| Self-hosted | Бесплатно | Зависит от тебя | Только если умеешь в реверс-инжиниринг |
Bundled Signer (бесплатный)
Встроен в tiktok-live-connector из коробки. Работает так:
// Без дополнительных настроек — используется бесплатный signer
const connection = new TikTokLiveConnection('@username');
Плюсы: Бесплатно, работает сразу.
Минусы:
- Жёсткие лимиты на количество подключений
- При хайпе (5000+ зрителей) может отвалиться
- Частые реконнекты → бан IP
- Не для 24/7 стриминга
Premium Signer (платный)
const connection = new TikTokLiveConnection('@username', {
signProviderHost: 'https://api.eulerstream.com',
signProviderApiKey: 'YOUR_API_KEY' // ~$10-20/мес
});
Когда нужен:
- Стрим не должен падать в критический момент (5000 лайков/сек)
- Режим 24/7
- Продакшн с реальными пользователями
Рекомендация
[Старт] → Бесплатный Bundled Signer
↓
[Тесты работают, первые стримы ОК]
↓
[Видишь "Connection lost" при хайпе?]
↓
[Да] → Переключаешься на Euler Stream (1 строка кода)
[Нет] → Продолжаешь бесплатно
11. Технические улучшения (review 2026)
А. WebSocket вместо HTTP
Проблема: При 2000 лайков/сек HTTP спамит сервер тысячами запросов → лаги.
Решение: Одно WebSocket соединение между Bridge и Plugin.
// Bridge: одно соединение
const ws = new WebSocket('ws://RUST_SERVER:28888');
// Отправка пакетов
ws.send(JSON.stringify({ likes: 450, votes: { 1: 12, 2: 5 } }));
Б. Batching (агрегация)
Не отправлять каждый лайк отдельно!
// Накапливаем 300-500ms, отправляем пакетом
class EventAggregator {
private likeBuffer = 0;
private voteBuffer: Record<number, number> = {};
private repairBuffer = 0;
addLike(count: number) {
this.likeBuffer += count;
}
addVote(side: number) {
this.voteBuffer[side] = (this.voteBuffer[side] || 0) + 1;
}
addRepair(hp: number) {
this.repairBuffer += hp;
}
flush(): GameUpdate {
const update = {
likes: this.likeBuffer,
votes: { ...this.voteBuffer },
repair: this.repairBuffer,
timestamp: Date.now()
};
// Reset
this.likeBuffer = 0;
this.voteBuffer = {};
this.repairBuffer = 0;
return update;
}
}
// Flush каждые 300ms
setInterval(() => {
const update = aggregator.flush();
if (update.likes > 0 || update.repair > 0) {
ws.send(JSON.stringify(update));
}
}, 300);
В. QR-код для промокода
Современный подход — QR на экране вместо текста:
// В Rust плагине
private void ShowPromoCodeWithQR(string promoCode)
{
// URL для QR: goloot.gg/redeem?code=XXXXX
var qrUrl = $"https://goloot.gg/redeem?code={promoCode}";
// Генерация QR через ImageLibrary или внешний сервис
var qrImageUrl = $"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={Uri.EscapeDataString(qrUrl)}";
// CUI с QR + текстовым кодом
ShowVictoryUI(promoCode, qrImageUrl);
}
12. Обновлённый VotingSystem (TypeScript)
import WebSocket from 'ws';
interface GameUpdate {
action: 'GAME_UPDATE' | 'VOTE_START' | 'VOTE_END';
likes?: number;
votes?: Record<number, number>;
repair?: number;
winner?: number;
duration?: number;
}
class TikTokRaidBridge {
private ws: WebSocket;
private aggregator = new EventAggregator();
private voting = new VotingSystem();
constructor(rustServerUrl: string) {
this.ws = new WebSocket(rustServerUrl);
// Flush каждые 300ms
setInterval(() => this.flushUpdates(), 300);
// Цикл голосования каждую минуту
setInterval(() => this.voting.startVoting(15), 60_000);
}
onTikTokLike(count: number) {
this.aggregator.addLike(count);
}
onTikTokComment(username: string, text: string) {
// Во время голосования — голос
if (this.voting.isActive) {
const vote = parseInt(text.trim());
if ([1, 2, 3].includes(vote)) {
this.voting.addVote(username, vote);
return;
}
}
// Иначе — починка
const repairHp = this.calculateRepair(text);
this.aggregator.addRepair(repairHp);
}
private calculateRepair(text: string): number {
const len = text.length;
if (len <= 5) return 5;
if (len <= 15) return 10;
if (len <= 30) return 20;
return 30;
}
private flushUpdates() {
const update = this.aggregator.flush();
// Добавляем текущие голоса если идёт голосование
if (this.voting.isActive) {
update.votes = this.voting.getVotes();
}
if (update.likes > 0 || update.repair > 0 || Object.keys(update.votes || {}).length > 0) {
this.send({ action: 'GAME_UPDATE', ...update });
}
}
private send(data: GameUpdate) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
}
class VotingSystem {
isActive = false;
private votes: Record<number, number> = { 1: 0, 2: 0, 3: 0 };
private voters = new Set<string>();
private bridge: TikTokRaidBridge;
startVoting(durationSec: number) {
this.votes = { 1: 0, 2: 0, 3: 0 };
this.voters.clear();
this.isActive = true;
this.bridge.send({ action: 'VOTE_START', duration: durationSec });
setTimeout(() => this.endVoting(), durationSec * 1000);
}
addVote(username: string, side: number) {
if (this.voters.has(username)) return;
this.voters.add(username);
this.votes[side]++;
}
getVotes() {
return { ...this.votes };
}
private endVoting() {
this.isActive = false;
const winner = Object.entries(this.votes)
.sort((a, b) => b[1] - a[1])[0][0];
this.bridge.send({
action: 'VOTE_END',
winner: parseInt(winner),
votes: this.votes
});
}
}
13. Порядок разработки: Plugin First
Почему Plugin First, а не Bridge First?
| Подход | Проблема |
|---|---|
| Bridge First | Пока нет плагина — некуда слать данные. Тестируешь "в пустоту" |
| Plugin First | Плагин тестируется локально через консоль. Видишь результат сразу |
Plugin First выигрывает потому что:
- Самая сложная часть делается первой (если застрянешь — узнаешь рано)
- Не нужен TikTok для тестирования
- Консольные команды эмулируют любые события
- Видишь взрывы, CUI, урон — реальный фидбек
Этапы разработки
┌─────────────────────────────────────────────────────────────────┐
│ ЭТАП 1: Plugin + Console Commands (3-5 дней) │
│ ───────────────────────────────────────────── │
│ • Консольные команды: tiktok.like, tiktok.vote, tiktok.repair │
│ • Спавн взрывчатки, урон, респавн дома │
│ • CUI: HP, счётчики, промокод │
│ • Тест: Rust Server + консоль (без TikTok!) │
├─────────────────────────────────────────────────────────────────┤
│ ЭТАП 2: Plugin + WebSocket Server (1-2 дня) │
│ ───────────────────────────────────────────── │
│ • Добавить WebSocket listener в плагин │
│ • Тест: wscat/Postman шлёт JSON → плагин реагирует │
├─────────────────────────────────────────────────────────────────┤
│ ЭТАП 3: Bridge Service (2-3 дня) │
│ ───────────────────────────────────────────── │
│ • TikTok-Live-Connector + агрегация + голосование │
│ • Тест: подключиться к чужому стриму → данные идут в плагин │
├─────────────────────────────────────────────────────────────────┤
│ ЭТАП 4: Интеграция (1-2 дня) │
│ ───────────────────────────────────────────── │
│ • Bridge слушает ТВОЙ стрим │
│ • Полный цикл: лайк → взрыв → промокод │
├─────────────────────────────────────────────────────────────────┤
│ ЭТАП 5: Продакшн стрим (1 день) │
│ ───────────────────────────────────────────── │
│ • Тест на 100-200 зрителей (если возможно) │
│ • Или сразу на 1000+ (если всё протестировано) │
└─────────────────────────────────────────────────────────────────┘
Этап 1: Консольные команды для тестирования
// GoLootTikTok.cs — команды для разработки
[ConsoleCommand("tiktok.like")]
void CmdLike(ConsoleSystem.Arg arg)
{
int count = arg.GetInt(0, 100);
AddLikes(count);
Puts($"[TikTok] Added {count} likes. Total progress updated.");
}
[ConsoleCommand("tiktok.vote")]
void CmdVote(ConsoleSystem.Arg arg)
{
int side = arg.GetInt(0, 1); // 1=left, 2=right, 3=top
SetAttackSide(side);
Puts($"[TikTok] Attack side set to {side}");
}
[ConsoleCommand("tiktok.repair")]
void CmdRepair(ConsoleSystem.Arg arg)
{
int hp = arg.GetInt(0, 50);
RepairHouse(hp);
Puts($"[TikTok] House repaired +{hp} HP");
}
[ConsoleCommand("tiktok.reset")]
void CmdReset(ConsoleSystem.Arg arg)
{
ResetGame();
Puts("[TikTok] Game reset. House respawned.");
}
[ConsoleCommand("tiktok.status")]
void CmdStatus(ConsoleSystem.Arg arg)
{
Puts($"[TikTok] House HP: {_houseHp}/{_houseMaxHp}");
Puts($"[TikTok] Attack side: {_currentAttackSide}");
Puts($"[TikTok] Propane: {_propaneProgress}/1000");
Puts($"[TikTok] Satchel: {_satchelProgress}/3000");
// ...
}
Как тестировать:
# В RCON консоли или F1 в игре
tiktok.like 500 # Добавить 500 лайков
tiktok.vote 2 # Атака справа
tiktok.repair 100 # Починить на 100 HP
tiktok.status # Посмотреть состояние
tiktok.reset # Начать заново
Сетап оборудования
Для разработки (1 PC):
[Твой PC]
├── Rust Server (localhost, 4-6 GB RAM)
├── Rust Client (minimum settings для тестов)
├── Bridge Service (Node.js, ~100 MB RAM)
└── VSCode / IDE
Для стрима (рекомендуется 2 устройства):
[PC #1 - Стример]
├── Rust Client (high settings)
├── OBS → TikTok LIVE
[PC #2 / VPS - Сервер]
├── Rust Server + Plugin
├── Bridge Service
Почему 2 устройства лучше:
- Стабильный FPS на стриме (OBS не конкурирует с сервером)
- Сервер не лагает от encoding
- VPS = $10-20/мес, но стабильность
Оценка времени
| Этап | Дни | Нужен TikTok? |
|---|---|---|
| 1. Plugin + Console | 3-5 | Нет |
| 2. Plugin + WebSocket | 1-2 | Нет |
| 3. Bridge Service | 2-3 | Да (чужой стрим) |
| 4. Интеграция | 1-2 | Да (свой стрим) |
| 5. Продакшн | 1 | Да |
| Итого | 8-13 | — |
14. Multi-Stream Architecture
Проблема: один стрим → ограниченный охват
Один TikTok аккаунт = одна аудитория. Для масштабирования нужно транслировать на несколько аккаунтов одновременно.
Проблема голосования при мультистриме
Стрим A: зрители голосуют за ЛЕВО (победило)
Стрим B: зрители голосуют за ПРАВО (победило)
→ Один Rust сервер, одна игровая сцена
→ Невозможно показать разные результаты разным стримам
→ Победитель ОДИН, но на одном стриме — "неправильный" результат
Решение: объединённое голосование
Все стримы = одна аудитория. Голоса суммируются:
Стрим A: ЛЕВО 200, ПРАВО 100, ВЕРХ 50
Стрим B: ЛЕВО 50, ПРАВО 300, ВЕРХ 100
Стрим C: ЛЕВО 100, ПРАВО 50, ВЕРХ 200
─────────────────────────────────────────
ИТОГО: ЛЕВО 350, ПРАВО 450, ВЕРХ 350
Победитель: ПРАВО (450)
Почему это работает:
- Каждый голос влияет на результат (честность)
- Масштабируется на любое количество стримов
- В потоке чата зрители не замечают, что голоса суммируются
- CUI показывает общую статистику — выглядит как "большая аудитория"
Архитектура с агрегацией
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ TikTok A │ │ TikTok B │ │ TikTok C │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Bridge A │ │ Bridge B │ │ Bridge C │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────┬────┴─────────────────┘
▼
┌─────────────────┐
│ Aggregator │ ← Центральный сервис
│ │
│ Суммирует: │
│ - Лайки │
│ - Голоса │
│ - Починку │
└────────┬────────┘
│
▼ WebSocket
┌─────────────────┐
│ Rust Plugin │
│ (GoLootTikTok) │
└─────────────────┘
Логика Aggregator
| Событие | Обработка |
|---|---|
| Лайки | Суммируются со всех стримов |
| Голоса | Суммируются, один голос на юзера глобально |
| Починка | Суммируется, rate limit на юзера глобально |
Важно: Aggregator отслеживает voters глобально, чтобы один юзер не мог проголосовать на нескольких стримах.
Сложность реализации
| Компонент | Дополнительное время |
|---|---|
| Aggregator сервис | +1 день |
| Multi-bridge подключение | +0.5 дня |
| CUI с мета-информацией | +0.5 дня |
| Итого | +2 дня |
15. CUI: Голосование
Визуализация
┌──────────────────────────────────────────────────────────┐
│ 🗳️ ГОЛОСОВАНИЕ: 12 сек │
├──────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ СЛЕВА ████████████████░░░░░░░░░░ 35% (350) │
│ │
│ 2️⃣ СПРАВА ████████████████████████░░ 45% (450) │
│ │
│ 3️⃣ СВЕРХУ ████████████████████░░░░░░ 20% (200) │
│ │
│ Напиши 1, 2 или 3 в чат! │
│ │
│ 👥 Всего голосов: 1,000 (3 стрима) │
└──────────────────────────────────────────────────────────┘
Элементы CUI
| Элемент | Назначение |
|---|---|
| Таймер | Обратный отсчёт 15→0 сек |
| Прогресс-бары | Визуально показывают лидера |
| Проценты | Быстрое понимание расклада |
| Абсолютные числа | Масштаб голосования |
| "N стримов" | Объясняет большое количество голосов |
Цветовая схема
| Вариант | Цвет | HEX |
|---|---|---|
| 1️⃣ СЛЕВА | Синий | #3399FF |
| 2️⃣ СПРАВА | Оранжевый | #FF6633 |
| 3️⃣ СВЕРХУ | Зелёный | #33CC33 |
Состояния CUI
1. Голосование активно
- Показывает таймер, бары, инструкцию
- Обновляется каждые 300ms
2. Голосование завершено
- Показывает победителя 3 сек
- Анимация выделения победившего варианта
- Затем скрывается
3. Фаза атаки
- CUI голосования скрыт
- Видны только счётчики взрывчатки и HP
Позиционирование
Экран:
┌────────────────────────────────────────┐
│ │
│ [Voting CUI] [HP Bar] │ ← Верхняя часть
│ │
│ │
│ [Игровая сцена] │
│ │
│ │
│ [Explosive Counters] │ ← Нижняя часть
│ │
└────────────────────────────────────────┘
Voting CUI: AnchorMin="0.02 0.7", AnchorMax="0.4 0.95"
HP Bar: AnchorMin="0.6 0.85", AnchorMax="0.98 0.95"
Counters: AnchorMin="0.02 0.02", AnchorMax="0.4 0.25"
16. Multi-Stream: Техническая реализация
OBS + Multi-RTMP Plugin
Один OBS отправляет поток на несколько TikTok аккаунтов:
[OBS]
└── Сцена: Rust gameplay
├── Output 1 → TikTok Account A (RTMP)
├── Output 2 → TikTok Account B (RTMP)
└── Output 3 → TikTok Account C (RTMP)
Требования к железу
| Количество потоков | GPU | RAM | CPU |
|---|---|---|---|
| 1 поток | GTX 1660+ | 16 GB | 6 ядер |
| 2-3 потока | RTX 3060+ | 32 GB | 8 ядер |
| 4+ потоков | RTX 4070+ | 32 GB | 8+ ядер |
Ключевой момент: Кодирование через NVENC (GPU), не через CPU.
Получение Stream Keys
В 2026 году TikTok выдаёт RTMP ключи через:
| Способ | Требования | Время |
|---|---|---|
| TikTok Live Studio | 1000+ фолловеров, 18+ лет | После достижения |
| Creator Portal | Верификация | 1-2 недели |
| Агентства | Контракт | Быстро, но условия |
Альтернативы OBS
| Инструмент | Плюсы | Минусы |
|---|---|---|
| OBS + Multi-RTMP | Бесплатно, гибко | Нужен мощный PC |
| Restream.io | Облако, не грузит PC | Платно, задержка |
| StreamYard | Простой UI | Ограничения бесплатного плана |
Рекомендация
MVP (1 стрим):
→ OBS стандартный, один аккаунт
Масштабирование (2-3 стрима):
→ OBS + Multi-RTMP plugin
→ RTX 3060+ для NVENC
→ Aggregator сервис
Большой масштаб (4+ стримов):
→ Несколько PC или VPS
→ Или Restream.io (облако)
17. TODO: Следующие шаги
- Этап 1: Plugin с консольными командами
- Структура плагина (state, config)
- Спавн/урон взрывчатки
- CUI: HP бар, счётчики
- Победа → промокод
- Респавн дома
- Этап 2: WebSocket server в плагине
- Этап 3: Bridge Service (TypeScript)
- Этап 4: Интеграция + тесты
- Этап 5: Продакшн стрим
- Этап 6: Multi-stream (после MVP)
- Aggregator сервис
- Несколько Bridge инстансов
- CUI с мета-информацией о стримах