Files
HTML-Game/clicker.html
T
2026-06-16 14:33:07 +03:00

997 lines
35 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Кликер-Магнат</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #eee;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.game-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
max-width: 1200px;
width: 100%;
}
.clicker-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
padding: 40px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.stats {
text-align: center;
margin-bottom: 30px;
}
.score {
font-size: 48px;
font-weight: bold;
color: #ffd700;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
margin-bottom: 10px;
}
.per-click {
font-size: 18px;
color: #aaa;
}
.per-second {
font-size: 16px;
color: #888;
margin-top: 5px;
}
.click-button {
width: 200px;
height: 200px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
cursor: pointer;
font-size: 24px;
color: white;
font-weight: bold;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
transition: all 0.1s ease;
display: block;
margin: 0 auto;
}
.click-button:hover {
transform: scale(1.05);
box-shadow: 0 15px 40px rgba(102, 126, 234, 0.6);
}
.click-button:active {
transform: scale(0.95);
}
.shop-section {
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
padding: 30px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-height: 80vh;
overflow-y: auto;
}
.shop-title {
font-size: 32px;
margin-bottom: 20px;
text-align: center;
color: #ffd700;
}
.upgrade-item {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.upgrade-item:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 215, 0, 0.3);
}
.upgrade-item.cant-afford {
opacity: 0.5;
}
.upgrade-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.upgrade-name {
font-size: 20px;
font-weight: bold;
color: #fff;
}
.upgrade-level {
font-size: 14px;
color: #aaa;
background: rgba(255, 255, 255, 0.1);
padding: 5px 10px;
border-radius: 10px;
}
.upgrade-description {
font-size: 14px;
color: #ccc;
margin-bottom: 15px;
}
.upgrade-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.upgrade-cost {
font-size: 18px;
color: #ffd700;
font-weight: bold;
}
.buy-button {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border: none;
padding: 10px 25px;
border-radius: 10px;
color: white;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
font-size: 16px;
}
.buy-button:hover:not(:disabled) {
transform: scale(1.05);
box-shadow: 0 5px 15px rgba(245, 87, 108, 0.4);
}
.buy-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.reset-button {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
border: none;
padding: 15px 30px;
border-radius: 15px;
color: white;
font-weight: bold;
cursor: pointer;
font-size: 16px;
margin-top: 20px;
width: 100%;
transition: all 0.3s ease;
}
.reset-button:hover {
transform: scale(1.02);
box-shadow: 0 5px 20px rgba(250, 112, 154, 0.4);
}
.click-animation {
position: absolute;
pointer-events: none;
font-size: 24px;
font-weight: bold;
color: #ffd700;
animation: float-up 1s ease-out forwards;
}
@keyframes float-up {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-100px);
}
}
@media (max-width: 768px) {
.game-container {
grid-template-columns: 1fr;
}
}
/* Скроллбар */
.shop-section::-webkit-scrollbar {
width: 10px;
}
.shop-section::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
.shop-section::-webkit-scrollbar-thumb {
background: rgba(255, 215, 0, 0.3);
border-radius: 10px;
}
.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); }
}
</style>
</head>
<body>
<div class="save-indicator" id="saveIndicator">
<div class="spinner"></div>
<span id="saveIndicatorText">Сохранение...</span>
</div>
<div class="game-container">
<div class="clicker-section">
<div class="stats">
<div class="score" id="score">0</div>
<div class="per-click">За клик: <span id="perClick">1</span></div>
<div class="per-second">В секунду: <span id="perSecond">0</span></div>
</div>
<button class="click-button" id="clickButton">
КЛИК!
</button>
</div>
<div class="shop-section">
<h2 class="shop-title">🛒 Магазин</h2>
<div id="upgradesContainer"></div>
<button class="reset-button" id="resetButton">🔄 Сбросить прогресс</button>
</div>
</div>
<script>
// Игровое состояние
let gameState = {
score: 0,
totalClicks: 0,
upgrades: {
clickPower: { level: 0, baseCost: 10, cost: 10, power: 1 },
autoClicker: { level: 0, baseCost: 50, cost: 50, power: 1 },
superClick: { level: 0, baseCost: 500, cost: 500, power: 10 },
megaAuto: { level: 0, baseCost: 2000, cost: 2000, power: 10 },
ultraClick: { level: 0, baseCost: 10000, cost: 10000, power: 100 },
hyperAuto: { level: 0, baseCost: 50000, cost: 50000, power: 100 }
}
};
// 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 = `
<div class="permission-modal-content">
<div class="permission-modal-header">
<span class="material-icons-round">security</span>
<h3>Запрос разрешения</h3>
</div>
<div class="permission-modal-body">
<p>Игра <strong>"${this.gameSlug}"</strong> хочет:</p>
<div class="permission-operation">
<span class="material-icons-round">${this.getOperationIcon(operation)}</span>
<strong>${this.getOperationName(operation)}</strong>
</div>
${description ? `<p class="permission-description">${description}</p>` : ''}
</div>
<div class="permission-modal-footer">
<label class="permission-checkbox">
<input type="checkbox" id="dontAskAgain">
<span>Не спрашивать больше для этой операции</span>
</label>
<div class="permission-buttons">
<button class="btn btn-danger" id="denyAllBtn">
<span class="material-icons-round">block</span>
Запретить всё
</button>
<button class="btn btn-secondary" id="denyBtn">
<span class="material-icons-round">close</span>
Запретить
</button>
<button class="btn btn-success" id="allowBtn">
<span class="material-icons-round">check</span>
Разрешить
</button>
<button class="btn btn-primary" id="allowAllBtn">
<span class="material-icons-round">done_all</span>
Разрешить всё
</button>
</div>
</div>
</div>
`;
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 = `
<style>
.permission-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
backdrop-filter: blur(5px);
}
.permission-modal-content {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 16px;
padding: 30px;
max-width: 500px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
animation: modalSlideIn 0.3s ease;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.permission-modal-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
}
.permission-modal-header .material-icons-round {
font-size: 32px;
color: #ffd700;
}
.permission-modal-header h3 {
margin: 0;
color: #fff;
font-size: 24px;
}
.permission-modal-body {
margin-bottom: 25px;
color: #ccc;
}
.permission-modal-body p {
margin: 10px 0;
line-height: 1.6;
}
.permission-operation {
background: rgba(255, 215, 0, 0.1);
border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 8px;
padding: 15px;
margin: 15px 0;
display: flex;
align-items: center;
gap: 10px;
}
.permission-operation .material-icons-round {
color: #ffd700;
font-size: 24px;
}
.permission-operation strong {
color: #ffd700;
font-size: 18px;
}
.permission-description {
background: rgba(255, 255, 255, 0.05);
padding: 10px;
border-radius: 6px;
font-style: italic;
}
.permission-modal-footer {
border-top: 2px solid rgba(255, 255, 255, 0.1);
padding-top: 20px;
}
.permission-checkbox {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
cursor: pointer;
color: #aaa;
}
.permission-checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.permission-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s ease;
}
.btn .material-icons-round {
font-size: 20px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
color: white;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
.btn:active {
transform: translateY(0);
}
@media (max-width: 600px) {
.permission-buttons {
grid-template-columns: 1fr;
}
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', modalStyles);
// Инициализация API
const gameAPI = new GameAPI('clicker-magnat');
// Описания улучшений
const upgradeDescriptions = {
clickPower: 'Увеличивает доход за клик на 1',
autoClicker: 'Автоматически добавляет 1 в секунду',
superClick: 'Увеличивает доход за клик на 10',
megaAuto: 'Автоматически добавляет 10 в секунду',
ultraClick: 'Увеличивает доход за клик на 100',
hyperAuto: 'Автоматически добавляет 100 в секунду'
};
const upgradeNames = {
clickPower: '💪 Сила клика',
autoClicker: '🤖 Автокликер',
superClick: '⚡ Супер клик',
megaAuto: '🚀 Мега автокликер',
ultraClick: '🔥 Ультра клик',
hyperAuto: '💎 Гипер автокликер'
};
// Показ индикатора сохранения
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));
}
}
}
}
// Сохранение игры через 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));
}
}
// Подсчет дохода за клик
function getClickPower() {
let power = 1;
power += gameState.upgrades.clickPower.level * gameState.upgrades.clickPower.power;
power += gameState.upgrades.superClick.level * gameState.upgrades.superClick.power;
power += gameState.upgrades.ultraClick.level * gameState.upgrades.ultraClick.power;
return power;
}
// Подсчет дохода в секунду
function getPerSecond() {
let perSecond = 0;
perSecond += gameState.upgrades.autoClicker.level * gameState.upgrades.autoClicker.power;
perSecond += gameState.upgrades.megaAuto.level * gameState.upgrades.megaAuto.power;
perSecond += gameState.upgrades.hyperAuto.level * gameState.upgrades.hyperAuto.power;
return perSecond;
}
// Обновление UI
function updateUI() {
document.getElementById('score').textContent = Math.floor(gameState.score).toLocaleString();
document.getElementById('perClick').textContent = getClickPower().toLocaleString();
document.getElementById('perSecond').textContent = getPerSecond().toLocaleString();
// Обновление магазина
const container = document.getElementById('upgradesContainer');
container.innerHTML = '';
for (let key in gameState.upgrades) {
const upgrade = gameState.upgrades[key];
const canAfford = gameState.score >= upgrade.cost;
const item = document.createElement('div');
item.className = `upgrade-item ${canAfford ? '' : 'cant-afford'}`;
item.innerHTML = `
<div class="upgrade-header">
<div class="upgrade-name">${upgradeNames[key]}</div>
<div class="upgrade-level">Ур. ${upgrade.level}</div>
</div>
<div class="upgrade-description">${upgradeDescriptions[key]}</div>
<div class="upgrade-footer">
<div class="upgrade-cost">💰 ${upgrade.cost.toLocaleString()}</div>
<button class="buy-button" ${canAfford ? '' : 'disabled'}>
Купить
</button>
</div>
`;
const buyButton = item.querySelector('.buy-button');
buyButton.addEventListener('click', () => buyUpgrade(key));
container.appendChild(item);
}
}
// Покупка улучшения
function buyUpgrade(upgradeKey) {
const upgrade = gameState.upgrades[upgradeKey];
if (gameState.score >= upgrade.cost) {
gameState.score -= upgrade.cost;
upgrade.level++;
upgrade.cost = Math.floor(upgrade.baseCost * Math.pow(1.15, upgrade.level));
updateUI();
saveGame();
}
}
// Клик по кнопке
function handleClick(event) {
const power = getClickPower();
gameState.score += power;
gameState.totalClicks++;
// Анимация
const animation = document.createElement('div');
animation.className = 'click-animation';
animation.textContent = `+${power}`;
animation.style.left = `${event.clientX}px`;
animation.style.top = `${event.clientY}px`;
document.body.appendChild(animation);
setTimeout(() => animation.remove(), 1000);
updateUI();
}
// Автокликер (каждую секунду)
function autoClick() {
const perSecond = getPerSecond();
if (perSecond > 0) {
gameState.score += perSecond / 10;
updateUI();
}
}
// Сброс игры
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();
}
}
// Инициализация
(async () => {
await loadGame();
updateUI();
document.getElementById('clickButton').addEventListener('click', handleClick);
document.getElementById('resetButton').addEventListener('click', resetGame);
// Автокликер каждые 100мс
setInterval(autoClick, 100);
// Автосохранение каждые 30 секунд
setInterval(saveGame, 30000);
})();
</script>
</body>
</html>