Моя история разработки инкрементальной игры о горнодобывающей промышленности Кузбасса с подробным разбором технической архитектуры, системы безопасности и монетизации.
Игра на 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
