diff --git a/clicker.html b/clicker.html index 22c7b36..bb67ac3 100644 --- a/clicker.html +++ b/clicker.html @@ -246,9 +246,69 @@ .shop-section::-webkit-scrollbar-thumb:hover { background: rgba(255, 215, 0, 0.5); } + + /* Индикатор сохранения */ + .save-indicator { + position: fixed; + top: 20px; + right: 20px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + display: none; + align-items: center; + gap: 10px; + z-index: 1000; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .save-indicator.show { + display: flex; + animation: slideIn 0.3s ease; + } + + .save-indicator.success { + border-color: rgba(76, 175, 80, 0.5); + } + + .save-indicator.error { + border-color: rgba(244, 67, 54, 0.5); + } + + @keyframes slideIn { + from { + opacity: 0; + transform: translateX(100px); + } + to { + opacity: 1; + transform: translateX(0); + } + } + + .spinner { + width: 16px; + height: 16px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; + } + + @keyframes spin { + to { transform: rotate(360deg); } + } +
+
+ Сохранение... +
+
@@ -283,6 +343,431 @@ } }; + // API клиент + class GameAPI { + constructor(gameSlug) { + this.gameSlug = gameSlug; + this.tableName = 'game_save'; + } + + getCSRFToken() { + return document.querySelector('[name=csrfmiddlewaretoken]')?.value || + document.cookie.split('; ').find(row => row.startsWith('csrftoken='))?.split('=')[1]; + } + + async requestPermission(operation, description = '') { + return new Promise((resolve) => { + const modal = document.createElement('div'); + modal.className = 'permission-modal'; + modal.innerHTML = ` +
+
+ security +

Запрос разрешения

+
+ +
+

Игра "${this.gameSlug}" хочет:

+
+ ${this.getOperationIcon(operation)} + ${this.getOperationName(operation)} +
+ ${description ? `

${description}

` : ''} +
+ + +
+ `; + + document.body.appendChild(modal); + + const dontAskAgain = modal.querySelector('#dontAskAgain'); + const allowBtn = modal.querySelector('#allowBtn'); + const denyBtn = modal.querySelector('#denyBtn'); + const allowAllBtn = modal.querySelector('#allowAllBtn'); + const denyAllBtn = modal.querySelector('#denyAllBtn'); + + const handleResponse = async (allow, dontAsk, allowAll, denyAll) => { + await this.savePermission(operation, allow, dontAsk, allowAll, denyAll); + modal.remove(); + resolve(allow); + }; + + allowBtn.onclick = () => handleResponse(true, dontAskAgain.checked, false, false); + denyBtn.onclick = () => handleResponse(false, dontAskAgain.checked, false, false); + allowAllBtn.onclick = () => handleResponse(true, true, true, false); + denyAllBtn.onclick = () => handleResponse(false, true, false, true); + + modal.onclick = (e) => { + if (e.target === modal) { + handleResponse(false, false, false, false); + } + }; + }); + } + + async getPermission(operation) { + try { + const response = await fetch('/api/user/permissions/', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': this.getCSRFToken() + } + }); + + if (response.ok) { + const data = await response.json(); + const permissions = data.permissions || {}; + const gamePerms = permissions[this.gameSlug] || {}; + return gamePerms[this.tableName]?.[operation] ?? null; + } + } catch (error) { + console.error('Ошибка загрузки разрешений:', error); + } + return null; + } + + async savePermission(operation, allow, dontAskAgain, allowAll, denyAll) { + try { + await fetch('/api/user/permissions/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': this.getCSRFToken() + }, + body: JSON.stringify({ + game_slug: this.gameSlug, + table_name: this.tableName, + operation: operation, + allow: allow, + dont_ask_again: dontAskAgain, + allow_all: allowAll, + deny_all: denyAll + }) + }); + } catch (error) { + console.error('Ошибка сохранения разрешения:', error); + } + } + + async checkPermission(operation, description = '') { + const cached = await this.getPermission(operation); + if (cached !== null) { + return cached; + } + return await this.requestPermission(operation, description); + } + + async loadGame() { + const allowed = await this.checkPermission('select', 'Загрузить сохранение игры'); + if (!allowed) { + throw new Error('Загрузка запрещена'); + } + + const response = await fetch('/api/game/select/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': this.getCSRFToken() + }, + body: JSON.stringify({ + game_slug: this.gameSlug, + table_name: this.tableName, + conditions: { 'key': 'game_state' } + }) + }); + + if (!response.ok) { + throw new Error('Ошибка загрузки'); + } + + const data = await response.json(); + return data.data?.[0]?.value || null; + } + + async saveGame(state) { + const allowed = await this.checkPermission('insert', 'Сохранить прогресс игры'); + if (!allowed) { + throw new Error('Сохранение запрещено'); + } + + const response = await fetch('/api/game/insert/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': this.getCSRFToken() + }, + body: JSON.stringify({ + game_slug: this.gameSlug, + table_name: this.tableName, + data: { + 'key': 'game_state', + 'value': JSON.stringify(state), + 'updated_at': new Date().toISOString() + } + }) + }); + + if (!response.ok) { + throw new Error('Ошибка сохранения'); + } + + return await response.json(); + } + + async deleteGame() { + const allowed = await this.checkPermission('delete', 'Удалить сохранение игры'); + if (!allowed) { + throw new Error('Удаление запрещено'); + } + + const response = await fetch('/api/game/delete/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': this.getCSRFToken() + }, + body: JSON.stringify({ + game_slug: this.gameSlug, + table_name: this.tableName, + conditions: { 'key': 'game_state' } + }) + }); + + if (!response.ok) { + throw new Error('Ошибка удаления'); + } + + return await response.json(); + } + + getOperationIcon(operation) { + const icons = { + 'select': 'download', + 'insert': 'save', + 'delete': 'delete' + }; + return icons[operation] || 'help'; + } + + getOperationName(operation) { + const names = { + 'select': 'Загрузить данные', + 'insert': 'Сохранить данные', + 'delete': 'Удалить данные' + }; + return names[operation] || operation; + } + } + + // CSS для модального окна + const modalStyles = ` + + `; + document.head.insertAdjacentHTML('beforeend', modalStyles); + + // Инициализация API + const gameAPI = new GameAPI('clicker-magnat'); + // Описания улучшений const upgradeDescriptions = { clickPower: 'Увеличивает доход за клик на 1', @@ -302,23 +787,81 @@ hyperAuto: '💎 Гипер автокликер' }; - // Загрузка сохранения - function loadGame() { - const saved = localStorage.getItem('clickerGame'); - if (saved) { - const savedState = JSON.parse(saved); - gameState = { ...gameState, ...savedState }; - // Восстанавливаем стоимость улучшений - for (let key in gameState.upgrades) { - const upgrade = gameState.upgrades[key]; - upgrade.cost = Math.floor(upgrade.baseCost * Math.pow(1.15, upgrade.level)); + // Показ индикатора сохранения + function showSaveIndicator(text, type = 'loading') { + const indicator = document.getElementById('saveIndicator'); + const indicatorText = document.getElementById('saveIndicatorText'); + + indicator.className = 'save-indicator show'; + if (type === 'success') { + indicator.classList.add('success'); + indicator.querySelector('.spinner').style.display = 'none'; + } else if (type === 'error') { + indicator.classList.add('error'); + indicator.querySelector('.spinner').style.display = 'none'; + } else { + indicator.querySelector('.spinner').style.display = 'block'; + } + + indicatorText.textContent = text; + } + + function hideSaveIndicator() { + const indicator = document.getElementById('saveIndicator'); + indicator.classList.remove('show'); + } + + // Загрузка сохранения через API + async function loadGame() { + try { + showSaveIndicator('Загрузка сохранения...'); + const savedData = await gameAPI.loadGame(); + + if (savedData) { + const savedState = JSON.parse(savedData); + gameState = { ...gameState, ...savedState }; + + // Восстанавливаем стоимость улучшений + for (let key in gameState.upgrades) { + const upgrade = gameState.upgrades[key]; + upgrade.cost = Math.floor(upgrade.baseCost * Math.pow(1.15, upgrade.level)); + } + } + + showSaveIndicator('Сохранение загружено', 'success'); + setTimeout(hideSaveIndicator, 2000); + } catch (error) { + console.error('Ошибка загрузки:', error); + showSaveIndicator('Ошибка загрузки', 'error'); + setTimeout(hideSaveIndicator, 2000); + + // Fallback на localStorage + const saved = localStorage.getItem('clickerGame'); + if (saved) { + const savedState = JSON.parse(saved); + gameState = { ...gameState, ...savedState }; + for (let key in gameState.upgrades) { + const upgrade = gameState.upgrades[key]; + upgrade.cost = Math.floor(upgrade.baseCost * Math.pow(1.15, upgrade.level)); + } } } } - // Сохранение игры - function saveGame() { - localStorage.setItem('clickerGame', JSON.stringify(gameState)); + // Сохранение игры через API + async function saveGame() { + try { + await gameAPI.saveGame(gameState); + showSaveIndicator('Сохранено', 'success'); + setTimeout(hideSaveIndicator, 1500); + } catch (error) { + console.error('Ошибка сохранения:', error); + showSaveIndicator('Ошибка сохранения', 'error'); + setTimeout(hideSaveIndicator, 2000); + + // Fallback на localStorage + localStorage.setItem('clickerGame', JSON.stringify(gameState)); + } } // Подсчет дохода за клик @@ -405,38 +948,50 @@ setTimeout(() => animation.remove(), 1000); updateUI(); - saveGame(); } // Автокликер (каждую секунду) function autoClick() { const perSecond = getPerSecond(); if (perSecond > 0) { - gameState.score += perSecond / 10; // Обновляем 10 раз в секунду для плавности + gameState.score += perSecond / 10; updateUI(); } } // Сброс игры - function resetGame() { + async function resetGame() { if (confirm('Вы уверены, что хотите сбросить весь прогресс?')) { + try { + showSaveIndicator('Удаление сохранения...'); + await gameAPI.deleteGame(); + showSaveIndicator('Прогресс сброшен', 'success'); + setTimeout(hideSaveIndicator, 1500); + } catch (error) { + console.error('Ошибка удаления:', error); + showSaveIndicator('Ошибка удаления', 'error'); + setTimeout(hideSaveIndicator, 2000); + } + localStorage.removeItem('clickerGame'); location.reload(); } } // Инициализация - loadGame(); - updateUI(); + (async () => { + await loadGame(); + updateUI(); - document.getElementById('clickButton').addEventListener('click', handleClick); - document.getElementById('resetButton').addEventListener('click', resetGame); + document.getElementById('clickButton').addEventListener('click', handleClick); + document.getElementById('resetButton').addEventListener('click', resetGame); - // Автокликер каждые 100мс - setInterval(autoClick, 100); + // Автокликер каждые 100мс + setInterval(autoClick, 100); - // Автосохранение каждые 10 секунд - setInterval(saveGame, 10000); + // Автосохранение каждые 30 секунд + setInterval(saveGame, 30000); + })(); \ No newline at end of file