Моя история разработки инкрементальной игры о горнодобывающей промышленности Кузбасса с подробным разбором технической архитектуры, системы безопасности и монетизации.
Игра на 80% сделана с помощью вайб кодинга, но это не так просто как звучит.
? Идея проекта
Я родом из Кемеровской области (Кузбасс) - угольной столицы России. Регион известен своими месторождениями: угля, золота, редких металлов. Идея пришла простая: создать современную idle-игру про управление горнодобывающей империей, где все месторождения - реальные объекты региона!
? Ключевые показатели проекта:
Старт разработки: 22 августа 2025
Версия: 1.8.0
Технологии: Impact.js, PHP 7.4+, MySQL 8.0, Telegram WebApp API
Платформы: Telegram Mini Apps, VK Mini Apps, Web
Архитектура: Client-Server с серверной авторитативностью
Концепция игры:
Начинаешь с Антоновского рудника (пгт. Рудничный) (моя родина )
Развиваешься до легендарной шахты Распадская
15 реальных месторождений с историческими данными
Образовательный элемент - игроки узнают об экономике региона
?️ Технический стек
Impact.js, PHP 8+, MySQL 8.0, Telegram WebApp API
Почему Impact.js?
Выбрал Impact.js по нескольким причинам:
Canvas-рендеринг - плавная анимация даже на слабых устройствах
Entity система - удобно для управления игровыми объектами
Встроенная физика - для анимаций рабочих и лифта
Малый вес - быстрая загрузка в Telegram WebApp
⚠️ Проблема: Impact.js устарел и документация скудная. Пришлось изучать практически "методом тыка"
?️ Архитектура проекта
Структура доменов
Проект разделён на два домена:
Домен |
Назначение |
Технологии |
|---|---|---|
|
Игра (Telegram/VK Mini Apps) |
Impact.js + Canvas, PHP API |
Лендинг + Личный кабинет |
PHP + HTML/CSS, REST API |
|
Есть тестовый домен, но его не вижу смысла указывать, там вечная каша ? |
Серверная архитектура
API Gateway: Все игровые операции идут через единую точку входа /api/v1.php. Это скрывает структуру бэкенда и упрощает защиту.
// Клиент отправляет:
POST /api/v1.php
{
"action": "buy_upgrade",
"user_id": kuzbassANJ8112,
"session_token": "kuztokenJjksa119",
"price": 777
}
// Gateway маршрутизирует на handler:
api/***/buy_upgrade.***.php
12 handlers (модульная система):
load.handler.php- загрузка прогресса игрокаsave.handler.php- сохранение прогрессаheartbeat.handler.php- отслеживание времени в игреcheck_session.handler.php- валидация сессииbuy_upgrade.handler.php- покупки и улучшенияspend_kuzbass.handler.php- траты премиум-валютыexchange_currency.handler.php- обмен Кузбиков на монеты...и другие
✅ Преимущества подхода: Легко добавлять новые действия, централизованная обработка ошибок, невозможно узнать структуру API через DevTools.
?️ База данных (17 таблиц)
Полностью server-authoritative подход - все критичные данные хранятся и валидируются на сервере:
Таблица |
Назначение |
|---|---|
|
Профили игроков (Telegram ID, имя, премиум-статус) |
|
Игровой прогресс (JSON + критичные поля) |
|
OAuth токены для входа через бота |
|
Реферальная система с milestone tracking |
|
Логирование попыток читерства |
|
История покупок премиум-валюты |
|
Аудит всех действий пользователей и админов |
|
Кэш рейтинга игроков |
|
Динамические настройки игровой экономики |
|
Справочник месторождений (15 объектов) |
Гибридное хранение прогресса
Разработал систему "JSON + критичные поля":
CREATE TABLE `game_savez_kuzb` (
`user_id` INT PRIMARY KEY,
`save_data` TEXT, -- Полный JSON прогресса
`cash` DOUBLE, -- Дублируется для быстрых запросов
`kuzbass` DOUBLE, -- Премиум-валюта
`income_per_hour` DOUBLE, -- Для рейтинга
`investors_count` INT, -- Для рейтинга
`total_earned` DOUBLE, -- Статистика
`admin_updated` TINYINT -- Флаг админ-правки
);
Зачем дублирование?
✅ Быстрые SQL-запросы для рейтинга (без парсинга JSON)
✅ Индексы на критичных полях
✅ Защита от манипуляций (сервер сверяет JSON с полями)
? Система безопасности (многоуровневая)
1. Server-Authoritative подход
Клиент НИКОГДА не решает сам - только сервер!
// ❌ ПЛОХО (клиент решает):
player.money -= 1000;
saveToServer(player);
// ✅ ХОРОШО (сервер решает):
const response = await buyUpgrade(price);
if (response.success) {
player.money = response.new_cash; // От сервера!
}
2. Session Token система
Каждый игрок получает уникальный токен при входе:
// При авторизации:
$sessionToken = bin2hex(random_bytes(32)); // 64 символа
$db->update('users', ['session_token' => $sessionToken]);
// При КАЖДОМ запросе:
if ($user['session_token'] !== $requestToken) {
die(json_encode(['error' => 'Invalid session']));
}
Защита от IDOR (Insecure Direct Object Reference):
Нельзя загрузить чужой прогресс даже зная user_id
Все эндпоинты валидируют session_token
При входе с нового устройства старая сессия сбрасывается
3. Античит система (двухуровневая)
Клиентская детекция:
Определение открытой консоли DevTools
Обнаружение изменений localStorage/sessionStorage
Детект попыток вызова скрытых функций
Серверная валидация:
Проверка адекватности изменений (нельзя заработать много монет за 1 секунду)
Валидация последовательности операций
Автобан после 5 попыток читерства
Уведомления админам в Telegram
Уведомление-предупрежние пользователю в Telegram что так делать нехорошо
4. Скрытие структуры API
Использую несколько методов obfuscation:
// .htaccess - возврат 404 вместо 403
RewriteRule ^.*\.php$ - [R=404,L]
// Все запросы через Gateway
POST /api/v1.php (action в body)
// Проверка Referer
RewriteCond %{HTTP_REFERER} !^https://([a-z0-9-]+\.)?kuzbass-empire\.ru
? Игровые механики
Idle-геймплей с глубиной
Основной цикл:
Добыча ресурсов на месторождениях
Транспортировка подъёмником
Продажа со склада
Реинвестирование в улучшения
Узкие места (bottleneck механика):
// Реальный доход ограничен вместимостью!
const shaftProduction = ∑(shaft.income); // Добыча шахт
const elevatorCap = elevator.capacity; // Вместимость лифта
const warehouseCap = warehouse.capacity; // Вместимость склада
const realIncome = min(shaftProduction, elevatorCap, warehouseCap);
// Если склад слабый - доход упадёт!
? Логичная игра: Игроки должны балансировать развитие всех элементов, а не только месторождений. Это добавляет стратегическую глубину!
Престиж-система (IPO)
Классическая механика idle-игр с математическим балансом:
// Формула расчёта инвесторов:
const divisor = 44444444444.444;
const power = 0.5; // Корень квадратный
investorsGain = Math.floor(
Math.pow(lifetimeEarning / divisor, power) -
Math.pow(startingLifetime / divisor, power)
);
// Бонус к доходу:
incomeBonus = investors × 0.02; // 2% за инвестора
Все параметры (divisor, power, процент бонуса) - динамические, хранятся в БД и настраиваются через админку.
? OAuth авторизация (свой велосипед)
Telegram Login Widget не подходил, потому что в настройках бота можно указать только один поддомен, то есть нельзя одного бота подключить для авторизации в кабинете на kuzbass-empire.ru и одновременно на поддомене game,kuzbass-empire.ru. Пришлось придумывать выход из ситуации и по итогу разработал свою OAuth-систему через бота:
Схема работы:
Пользователь на сайте или в игре → кликает "Войти через бота"
Генерируется токен (POST /api/auth_generate_token.php)
└─> Сохраняется в auth_tokens (TTL 5 минут)
Открывается бот с deeplink: t.me/bot?start=TOKEN
Бот отправляет токен на сервер + telegram_id пользователя
Сервер связывает токен с telegram_id
Сайт и игра polling (каждые 2 сек): проверяет статус токена
Токен подтверждён → получаем данные пользователя → вход!
Безопасность:
Токен используется только 1 раз
Время жизни 5 минут
Привязка к IP и User-Agent и ещё нескольким параметрам
Функция "Запомнить устройство" через localStorage
? Монетизация (двойная валюта)
Кузбики ?️ (премиум-валюта)
Источники получения:
Стартовый бонус: 100 Кузбиков
Рефералы: 250 Кузбиков за друга (milestone 10K монет)
Покупка: ЮMoney, FreeKassa (курс настраиваемый)
Применение:
Сброс перезарядки навыка менеджера (10 Кузбиков)
Обмен на игровые монеты (курс 1:1000, комиссия 0-10%)
Pending система начислений
Разработал систему отложенного начисления для безопасности:
// 1. Callback от платёжки:
$user['pending_kuzbass_change'] += 5500;
// 2. Игра проверяет каждые 10 сек:
if ($pending > 0) {
$user['kuzbass'] += $pending;
$user['pending_kuzbass_change'] = 0;
// Показываем уведомление в игре
}
Зачем? Защита от race conditions - нельзя потратить Кузбики пока транзакция не подтверждена полностью, а то уже есть такие умники, которые нажмают ускорение перезарядки за кузбики и резко перезагружают страницу, в таком случае списания кузбиков не было и можно было бесплатно перезаряжать сколько угодно ?
? Кроссплатформенность (Telegram + VK)
Telegram Mini Apps
Полная интеграция с Telegram WebApp API:
Авторизация - через
initDataUnsafeТема - автоматическая адаптация под тему устройства
Haptic Feedback - вибрации при действиях
Header/BottomBar - цвета под тему
VK Mini Apps
Адаптация заняла пару часов благодаря модульной архитектуре:
// Определение платформы:
const isVK = window.location.search.includes('vk_');
// VK Bridge инициализация:
if (isVK && window.vkBridge) {
vkBridge.send('VKWebAppInit');
// Авторизация через VK
}
Единый бэкенд для обеих платформ - только фронтенд различается!
?️ Защита от читерства (многослойная)
Проблема
В браузерной игре игрок имеет доступ к:
JavaScript коду (можно вызывать функции)
Памяти (можно менять переменные)
Network (можно повторять запросы)
Решение #1: Серверная валидация
// Проверка адекватности изменений:
$timeDiff = time() - $lastSaveTime;
$maxPossibleEarning = $income_per_hour / 3600 * $timeDiff * 1.5;
if ($cashDiff > $maxPossibleEarning) {
logCheatAttempt($userId, 'impossible_earning');
return error('Suspicious activity');
}
Решение #2: Race Condition защита
// Флаги операций в клиенте:
window._upgradeOperationInProgress = true;
// Дублирование через sendBeacon:
navigator.sendBeacon('/api/v1.php', data); // Надёжная доставка
fetch('/api/v1.php', data); // Получение ответа
Решение #3: Session binding
При каждом сохранении сервер возвращает актуальные данные:
// Ответ сервера:
{
"success": true,
"new_cash": 12345.67, // Актуальный баланс
"new_kuzbass": 150, // Актуальные Кузбики
"server_time": 1699120345
}
// Клиент применяет:
player.money = response.new_cash; // Не своё значение!
⚡ Оптимизация производительности
Автосохранение (умное)
Сохранение каждые 10 секунд, но с защитой:
// Heartbeat отдельно от save:
setInterval(() => {
sendHeartbeat(timePlayed); // Только время
}, 30000); // 30 сек
setInterval(() => {
saveGame(fullData); // Полные данные
}, 10000); // 10 сек
Это снижает нагрузку на БД на 66%!
Бандлинг ресурсов
Вместо 15 отдельных JS файлов - один бандл:
// scripts-bundle.php объединяет:
$files = [
'***/scripts/reset.js',
'и другие'
];
// Отдаём один файл с мгновенным применением изменений при новой загрузке страницы:
/api/scripts-bundle.php?v={timestamp}
Результат: Загрузка игры ускорилась с 3.2s до 0.8s!
Capture Phase для UI
Проблема: При клике на UI игра ставилась на паузу (Impact.js перехватывал события).
Решение:
// ❌ Bubbling phase (было):
element.addEventListener('click', handler, false);
// ✅ Capture phase (стало):
element.addEventListener('click', handler, true);
// Теперь UI перехватывает события ДО игры!
? Модернизация UI (от Canvas к HTML)
Impact.js использует Canvas для попапов. Это выглядит устаревшим и не адаптивно.
Решение: Перехват создания попапов и замена на HTML-шторки!
// Патч spawnEntity:
const originalSpawn = ig.game.spawnEntity;
ig.game.spawnEntity = function(entityClass, x, y, settings) {
if (entityClass.name === 'EntityUpgradeController') {
openUpgradeSheet(); // Наша HTML-шторка!
return { kill: () => {}, _wasReplaced: true };
}
return originalSpawn.call(this, entityClass, x, y, settings);
};
Результат:
✅ Современный дизайн в стиле Web3 приложения
✅ Анимации (slide-up/slide-down)
✅ Адаптация под тему устройства пользователя (светлая/тёмная)
✅ Копирование данных (реферальная ссылка)
✅ Сохранение всего функционала игры
? Платёжные системы (интеграция)
ЮMoney + FreeKassa
Интегрировал обе платёжки с единым интерфейсом:
Возможность |
ЮMoney |
FreeKassa |
|---|---|---|
Банковские карты |
✅ МИР, Visa, MC |
❌ (конские условия) |
Электронные кошельки |
✅ ЮMoney |
❌ (конские условия) |
Криптовалюта |
❌ |
✅ BTC, ETH, USDT, TON |
Подпись callback |
SHA-256 |
MD5 |
Почему именно ЮMoney и FreeKass? Потому что я решил на первых этапах игры принимать платежи как физ.лицо, а это единственный сервисы, которые я знаю с адекватной интеграцией и, главное, возможностью принимать как физ.лицо.
Изначально только фрикасу планировал подключить, но когда магазин был одобрен и я настроил интеграцию, то столкнулся с тем, что минимальный платёж - 1000 рублей и его нельзя настроить ?
Динамическая настройка через админку:
Включение/выключение систем
Логотипы и названия
Бонусы к пополнению (%, настраиваемо)
Минимальные/максимальные суммы
? Аналитика и мониторинг
Логирую ВСЁ через таблицу activity_logz_kuzb:
// Примеры событий:
- register (регистрация)
- login (вход)
- buy_upgrade (покупка)
- kuzbass_topup (пополнение)
- referral_kuzbass_bonus (бонус реферала)
- cheat_attempt (попытка читерства)
- admin_update_user (админ изменил данные)
Админ-панель показывает:
Онлайн игроков в реальном времени
Топ читеров с деталями попыток
История транзакций
Графики активности по дням
Детальная информация о каждом игроке
? Масштабируемость
Динамическая конфигурация
ВСЕ параметры игры настраиваемые:
// Таблица game_settingz_kuzb (24 параметра):
- mine_price_coef: 1.40
- other_price_coef: 1.50
- manager_skill_duration: 120
- max_offline_hours: 24
- referral_kuzbass_bonus: 100
- kuzbass_exchange_rate: 1000
...и другие
Зачем? Можно балансировать экономику БЕЗ обновления кода! Изменения применяются мгновенно для всех игроков.
Поддержка 15 месторождений (расширяемая)
// Патч для поддержки динамического числа шахт:
const actualShaftsCount = window.DEPOSITS_DATA?.length || 15;
// Добавление пустых слотов:
while (sessionData.Mineshaft.length < actualShaftsCount) {
sessionData.Mineshaft.push({
level: 0,
managerExist: false,
...defaultShaftData
});
}
? Проблемы и решения
Проблема #1: Откат баланса после покупок
Причина: Автосохранение срабатывало ДО завершения покупки на сервере.
Решение:
// Синхронизируем баланс ПЕРЕД покупкой:
await saveGameToServer();
await delay(500); // Ждём завершения
const response = await buyUpgrade(price);
// Применяем СЕРВЕРНЫЙ баланс:
player.money = response.new_cash;
Проблема #2: iOS не открывает платёжные формы
Причина: Safari блокирует window.open вне обработчика клика.
Решение:
// Создаём скрытую форму + auto-submit:
const form = document.createElement('form');
form.method = 'POST';
form.action = paymentURL;
form.target = '_blank';
document.body.appendChild(form);
form.submit(); // Работает в Safari!
Проблема #3: Игра ставится на паузу при открытии UI
Решение: Переопределение pauseGame + агрессивный мониторинг каждые 50ms.
? Выводы
За несколько месяцев создал полноценную idle-игру с:
✅ Глубокими игровыми механиками
✅ Многоуровневой системой безопасности
✅ Двумя платформами (Telegram + VK)
✅ Монетизацией через 2 платёжные системы
✅ Реферальной программой
✅ Админ-панелью для управления
✅ Образовательным компонентом (реальные месторождения)
Главный урок: Даже на устаревшем движке (Impact.js) можно создать современную, безопасную и красивую игру, если продумать архитектуру и не жалеть времени на детали.
? Игра доступна:
Telegram: @kuzbass_empire_bot
VK: vk.com/app54256088
Сайт: kuzbass-empire.ru
alex_02
Неплохо, неплохо, но над системой вычисления людей, которые в браузере решили залезть в консоль разработчика, стоит ещё поработать. Мне прилетело предупреждение, что так нельзя делать, когда игра только только открылась в браузере. Мой браузер — Chrome ))