Skip to main content

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 }
]
}

Логика выбора:

  1. При старте раунда Bridge отправляет текущий онлайн
  2. Плагин выбирает дом из пула по диапазону
  3. Название дома показывается в 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 Bar300msПлавное изменение, не дёргается
Счётчики взрывчатки300msСинхронно с HP
Голосование300msReal-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 = 0Victory 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 EventsTikTok-Live-ConnectorNode.js библиотека
Signing ServerEuler StreamОбязательно в 2026 — без него TikTok блокирует соединение через 5-10 мин
Bridge ServiceNode.js + TypeScriptАгрегация, rate limit, голосование, batching
Rust PluginOxide/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 + Console3-5Нет
2. Plugin + WebSocket1-2Нет
3. Bridge Service2-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)

Требования к железу

Количество потоковGPURAMCPU
1 потокGTX 1660+16 GB6 ядер
2-3 потокаRTX 3060+32 GB8 ядер
4+ потоковRTX 4070+32 GB8+ ядер

Ключевой момент: Кодирование через NVENC (GPU), не через CPU.

Получение Stream Keys

В 2026 году TikTok выдаёт RTMP ключи через:

СпособТребованияВремя
TikTok Live Studio1000+ фолловеров, 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 с мета-информацией о стримах