Download index.html from ZMaxAIru/ppl: direct link, hf CLI and curl.
- Browser
- Download file 40.2 kB
-
https://huggingface.co/spaces/ZMaxAIru/ppl/resolve/main/index.html
- Command line
-
hf download hf://spaces/ZMaxAIru/ppl/index.html
-
curl -L -o index.html https://huggingface.co/spaces/ZMaxAIru/ppl/resolve/main/index.html
40.2 kB
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Пиксельный майнинг</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-color: #1a1a2e; | |
| --panel-bg: #16213e; | |
| --text-color: #e6e6e6; | |
| --accent-color: #4cc9f0; | |
| --rare-silver: #c0c0c0; | |
| --rare-gold: #ffd700; | |
| --rare-diamond: #b9f2ff; | |
| --pixel-size: 16px; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| .container { | |
| max-width: 100%; | |
| padding: 12px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 8px 12px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| margin-bottom: 12px; | |
| } | |
| .logo { | |
| font-weight: bold; | |
| font-size: 1.2rem; | |
| color: var(--accent-color); | |
| } | |
| .user-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .user-avatar { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| background-color: var(--panel-bg); | |
| } | |
| .mining-section { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 16px; | |
| margin-bottom: 20px; | |
| } | |
| .canvas-container { | |
| position: relative; | |
| margin: 0 auto; | |
| border: 2px solid var(--panel-bg); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| background-color: #111; | |
| } | |
| .pixel-canvas { | |
| display: grid; | |
| grid-template-columns: repeat(var(--cols), var(--pixel-size)); | |
| grid-template-rows: repeat(var(--rows), var(--pixel-size)); | |
| } | |
| .pixel { | |
| background-color: #222; | |
| border: 1px solid #111; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| } | |
| .pixel:hover { | |
| opacity: 0.9; | |
| } | |
| .pixel-mined { | |
| background-color: #444; | |
| } | |
| .pixel-special { | |
| position: relative; | |
| } | |
| .pixel-silver { | |
| background-color: var(--rare-silver); | |
| } | |
| .pixel-gold { | |
| background-color: var(--rare-gold); | |
| } | |
| .pixel-diamond { | |
| background-color: var(--rare-diamond); | |
| } | |
| .mining-info { | |
| background-color: var(--panel-bg); | |
| border-radius: 8px; | |
| padding: 12px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .block-info { | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .progress-container { | |
| background-color: #333; | |
| border-radius: 999px; | |
| height: 8px; | |
| margin-top: 4px; | |
| overflow: hidden; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background-color: var(--accent-color); | |
| border-radius: 999px; | |
| transition: width 0.3s; | |
| } | |
| .mining-button { | |
| background-color: var(--accent-color); | |
| color: #111; | |
| border: none; | |
| border-radius: 8px; | |
| padding: 12px; | |
| font-weight: bold; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: transform 0.2s, background-color 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .mining-button:active { | |
| transform: scale(0.98); | |
| background-color: #3aa8d8; | |
| } | |
| .mining-button i { | |
| font-size: 1.2rem; | |
| } | |
| .stats-section { | |
| background-color: var(--panel-bg); | |
| border-radius: 8px; | |
| padding: 12px; | |
| margin-bottom: 16px; | |
| } | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| margin-top: 12px; | |
| } | |
| .stat-item { | |
| background-color: rgba(0, 0, 0, 0.2); | |
| border-radius: 6px; | |
| padding: 8px; | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| margin-top: 4px; | |
| color: var(--accent-color); | |
| } | |
| .tab-container { | |
| display: flex; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| margin-bottom: 12px; | |
| } | |
| .tab { | |
| padding: 8px 12px; | |
| cursor: pointer; | |
| position: relative; | |
| } | |
| .tab.active { | |
| color: var(--accent-color); | |
| } | |
| .tab.active::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: -1px; | |
| left: 0; | |
| width: 100%; | |
| height: 2px; | |
| background-color: var(--accent-color); | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .leaderboard { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| .leaderboard th { | |
| text-align: left; | |
| padding: 8px 12px; | |
| background-color: rgba(0, 0, 0, 0.2); | |
| } | |
| .leaderboard td { | |
| padding: 8px 12px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .leaderboard tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .medal { | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-right: 6px; | |
| } | |
| .medal-gold { | |
| background-color: var(--rare-gold); | |
| color: #333; | |
| } | |
| .medal-silver { | |
| background-color: var(--rare-silver); | |
| color: #333; | |
| } | |
| .medal-bronze { | |
| background-color: #cd7f32; | |
| color: #333; | |
| } | |
| .history-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 8px 0; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .history-item:last-child { | |
| border-bottom: none; | |
| } | |
| .rare-icon { | |
| font-size: 0.8rem; | |
| margin-left: 4px; | |
| } | |
| .pixel-tooltip { | |
| position: absolute; | |
| top: -30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background-color: rgba(0, 0, 0, 0.8); | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 0.8rem; | |
| white-space: nowrap; | |
| z-index: 10; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| } | |
| .pixel:hover .pixel-tooltip { | |
| opacity: 1; | |
| } | |
| .avatar-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 8px; | |
| margin-top: 12px; | |
| } | |
| .avatar-item { | |
| aspect-ratio: 1/1; | |
| background-color: #333; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| overflow: hidden; | |
| } | |
| .avatar-item img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .avatar-item:active { | |
| transform: scale(0.95); | |
| } | |
| .avatar-item.selected { | |
| border: 2px solid var(--accent-color); | |
| } | |
| @media (min-width: 500px) { | |
| :root { | |
| --pixel-size: 20px; | |
| } | |
| .container { | |
| max-width: 500px; | |
| } | |
| .stats-grid { | |
| grid-template-columns: repeat(4, 1fr); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="logo">PixelMiner</div> | |
| <div class="user-info"> | |
| <span id="user-coins">0 PX</span> | |
| <div class="user-avatar" id="user-avatar"></div> | |
| </div> | |
| </header> | |
| <div class="mining-section"> | |
| <div class="canvas-container"> | |
| <div class="pixel-canvas" id="pixel-canvas"></div> | |
| </div> | |
| <div class="mining-info"> | |
| <div class="block-info"> | |
| <div> | |
| <div>Блок #<span id="current-block">1</span></div> | |
| <div class="progress-container"> | |
| <div class="progress-bar" id="block-progress" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div style="text-align: right;"> | |
| <div>Пикселей: <span id="mined-pixels">0</span>/<span id="total-pixels">0</span></div> | |
| <div>Игроков: <span id="online-players">0</span></div> | |
| </div> | |
| </div> | |
| <button class="mining-button" id="mine-button"> | |
| <i class="fas fa-hammer"></i> | |
| <span>Добыть пиксель</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="tab-container"> | |
| <div class="tab active" data-tab="stats">Статистика</div> | |
| <div class="tab" data-tab="leaderboard">Лидеры</div> | |
| <div class="tab" data-tab="history">История</div> | |
| <div class="tab" data-tab="profile">Профиль</div> | |
| </div> | |
| <div class="tab-content active" id="stats-content"> | |
| <div class="stats-section"> | |
| <div class="stats-grid"> | |
| <div class="stat-item"> | |
| <div>Ваши очки</div> | |
| <div class="stat-value" id="user-score">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Добыто</div> | |
| <div class="stat-value" id="user-mined">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Серебряных</div> | |
| <div class="stat-value" id="user-silver">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Золотых</div> | |
| <div class="stat-value" id="user-gold">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Бриллиантов</div> | |
| <div class="stat-value" id="user-diamond">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Создано блоков</div> | |
| <div class="stat-value" id="user-created">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Место в топе</div> | |
| <div class="stat-value" id="user-rank">-</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="tab-content" id="leaderboard-content"> | |
| <div class="stats-section"> | |
| <table class="leaderboard"> | |
| <thead> | |
| <tr> | |
| <th>#</th> | |
| <th>Имя</th> | |
| <th>Очки</th> | |
| </tr> | |
| </thead> | |
| <tbody id="leaderboard-body"> | |
| <!-- заполняется JS --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="tab-content" id="history-content"> | |
| <div class="stats-section"> | |
| <div id="history-list"> | |
| <!-- заполняется JS --> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="tab-content" id="profile-content"> | |
| <div class="stats-section"> | |
| <div> | |
| <div style="text-align: center; margin-bottom: 12px;"> | |
| <div class="user-avatar" style="width: 80px; height: 80px; margin: 0 auto 8px;" id="profile-avatar"></div> | |
| <div id="profile-name">Имя пользователя</div> | |
| <div style="font-size: 0.9rem; color: var(--accent-color);" id="profile-pix">0 PX</div> | |
| </div> | |
| <div style="margin-top: 16px;">Созданные блоки:</div> | |
| <div class="avatar-grid" id="avatar-grid"> | |
| <!-- заполняется JS --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Инициализация игры | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Конфигурация | |
| const config = { | |
| cols: 16, | |
| rows: 16, | |
| blockSizeIncrease: 2, | |
| initialBlockSize: 16*16, | |
| baseScore: 1, | |
| rareChances: { | |
| silver: 0.1, | |
| gold: 0.03, | |
| diamond: 0.01 | |
| }, | |
| rareScores: { | |
| silver: 5, | |
| gold: 15, | |
| diamond: 50 | |
| }, | |
| updateInterval: 2000, | |
| fakePlayers: 25 | |
| }; | |
| // Состояние игры | |
| const state = { | |
| currentBlock: 1, | |
| canvasSize: config.initialBlockSize, | |
| minedPixels: 0, | |
| onlinePlayers: 0, | |
| playerData: { | |
| score: 0, | |
| mined: 0, | |
| silver: 0, | |
| gold: 0, | |
| diamond: 0, | |
| createdBlocks: 0, | |
| pix: 0, | |
| avatar: '', | |
| name: 'Игрок' | |
| }, | |
| leaderboard: [], | |
| blockHistory: [], | |
| createdBlocks: [] | |
| }; | |
| // DOM элементы | |
| const elements = { | |
| canvas: document.getElementById('pixel-canvas'), | |
| currentBlock: document.getElementById('current-block'), | |
| minedPixels: document.getElementById('mined-pixels'), | |
| totalPixels: document.getElementById('total-pixels'), | |
| onlinePlayers: document.getElementById('online-players'), | |
| mineButton: document.getElementById('mine-button'), | |
| blockProgress: document.getElementById('block-progress'), | |
| userScore: document.getElementById('user-score'), | |
| userMined: document.getElementById('user-mined'), | |
| userSilver: document.getElementById('user-silver'), | |
| userGold: document.getElementById('user-gold'), | |
| userDiamond: document.getElementById('user-diamond'), | |
| userCreated: document.getElementById('user-created'), | |
| userRank: document.getElementById('user-rank'), | |
| userCoins: document.getElementById('user-coins'), | |
| userAvatar: document.getElementById('user-avatar'), | |
| profileAvatar: document.getElementById('profile-avatar'), | |
| profileName: document.getElementById('profile-name'), | |
| profilePix: document.getElementById('profile-pix'), | |
| leaderboardBody: document.getElementById('leaderboard-body'), | |
| historyList: document.getElementById('history-list'), | |
| avatarGrid: document.getElementById('avatar-grid'), | |
| tabs: document.querySelectorAll('.tab'), | |
| tabContents: document.querySelectorAll('.tab-content') | |
| }; | |
| // Симуляция WebSocket соединения | |
| let socket = { | |
| connect: () => { | |
| console.log('Connecting to WebSocket server...'); | |
| // Здесь будет реальное подключение через WebSocket | |
| // Пока используем setTimeout для имитации сетевых событий | |
| // Имитация получения данных от сервера | |
| setInterval(() => { | |
| updateOnlinePlayers(); | |
| updateBlockProgress(); | |
| updateLeaderboard(); | |
| }, config.updateInterval); | |
| // Для демонстрации - имитация других игроков | |
| setInterval(() => { | |
| simulateOtherPlayers(); | |
| }, 1000); | |
| }, | |
| send: (data) => { | |
| console.log('Sending data:', data); | |
| // Здесь будет отправка данных на сервер через WebSocket | |
| } | |
| }; | |
| // Инициализация канваса | |
| function initCanvas() { | |
| elements.canvas.style.setProperty('--cols', config.cols); | |
| elements.canvas.style.setProperty('--rows', config.rows); | |
| elements.totalPixels.textContent = config.cols * config.rows; | |
| // Очищаем канвас | |
| elements.canvas.innerHTML = ''; | |
| // Создаем пиксели | |
| for (let i = 0; i < config.cols * config.rows; i++) { | |
| const pixel = document.createElement('div'); | |
| pixel.className = 'pixel'; | |
| pixel.dataset.index = i; | |
| // Добавляем tooltip | |
| const tooltip = document.createElement('div'); | |
| tooltip.className = 'pixel-tooltip'; | |
| tooltip.textContent = 'Не добыт'; | |
| pixel.appendChild(tooltip); | |
| elements.canvas.appendChild(pixel); | |
| } | |
| } | |
| // Обновление счетчиков | |
| function updateUI() { | |
| elements.currentBlock.textContent = state.currentBlock; | |
| elements.minedPixels.textContent = state.minedPixels; | |
| elements.totalPixels.textContent = config.cols * config.rows; | |
| elements.onlinePlayers.textContent = state.onlinePlayers; | |
| elements.userScore.textContent = state.playerData.score; | |
| elements.userMined.textContent = state.playerData.mined; | |
| elements.userSilver.textContent = state.playerData.silver; | |
| elements.userGold.textContent = state.playerData.gold; | |
| elements.userDiamond.textContent = state.playerData.diamond; | |
| elements.userCreated.textContent = state.playerData.createdBlocks; | |
| elements.userCoins.textContent = state.playerData.pix + ' PX'; | |
| elements.profilePix.textContent = state.playerData.pix + ' PX'; | |
| // Обновление прогресса блока | |
| const progress = (state.minedPixels / (config.cols * config.rows)) * 100; | |
| elements.blockProgress.style.width = progress + '%'; | |
| } | |
| // Добыча пикселя | |
| function minePixel(index = null) { | |
| // Если индекс не указан, выбираем случайный неподобранный пиксель | |
| if (index === null) { | |
| const unminedPixels = Array.from(document.querySelectorAll('.pixel:not(.pixel-mined)')); | |
| if (unminedPixels.length === 0) return false; | |
| const randomIndex = Math.floor(Math.random() * unminedPixels.length); | |
| index = parseInt(unminedPixels[randomIndex].dataset.index); | |
| } | |
| const pixel = elements.canvas.children[index]; | |
| if (!pixel || pixel.classList.contains('pixel-mined')) return false; | |
| // Определяем, является ли пиксель редким | |
| const random = Math.random(); | |
| let pixelType = 'normal'; | |
| let score = config.baseScore; | |
| if (random < config.rareChances.diamond) { | |
| pixelType = 'diamond'; | |
| score = config.rareScores.diamond; | |
| state.playerData.diamond++; | |
| } else if (random < config.rareChances.gold) { | |
| pixelType = 'gold'; | |
| score = config.rareScores.gold; | |
| state.playerData.gold++; | |
| } else if (random < config.rareChances.silver) { | |
| pixelType = 'silver'; | |
| score = config.rareScores.silver; | |
| state.playerData.silver++; | |
| } | |
| // Обновляем состояние | |
| state.minedPixels++; | |
| state.playerData.mined++; | |
| state.playerData.score += score; | |
| state.playerData.pix += score; | |
| // Обновляем пиксель | |
| pixel.classList.add('pixel-mined'); | |
| if (pixelType !== 'normal') { | |
| pixel.classList.add('pixel-special', `pixel-${pixelType}`); | |
| pixel.querySelector('.pixel-tooltip').innerHTML = | |
| pixelType === 'silver' ? 'Серебряный ✨' : | |
| pixelType === 'gold' ? 'Золотой ⭐' : 'Бриллиантовый 💎'; | |
| } else { | |
| pixel.querySelector('.pixel-tooltip').textContent = 'Добыт'; | |
| } | |
| // Проверяем, завершен ли блок | |
| if (state.minedPixels >= config.cols * config.rows) { | |
| completeBlock(); | |
| } | |
| updateUI(); | |
| return true; | |
| } | |
| // Завершение блока | |
| function completeBlock() { | |
| // Находим игрока с максимальным количеством очков | |
| // В реальном приложении это будет определяться сервером | |
| state.playerData.createdBlocks++; | |
| // Добавляем блок в историю | |
| const blockData = { | |
| id: state.currentBlock, | |
| creator: state.playerData.name, | |
| score: state.playerData.score, | |
| pixels: state.minedPixels, | |
| silver: state.playerData.silver, | |
| gold: state.playerData.gold, | |
| diamond: state.playerData.diamond, | |
| completedAt: new Date() | |
| }; | |
| state.blockHistory.unshift(blockData); | |
| state.createdBlocks.push({ | |
| id: state.currentBlock, | |
| // В реальном приложении здесь будет ссылка на изображение холста | |
| image: generateBlockImage() | |
| }); | |
| // Переходим к следующему блоку | |
| state.currentBlock++; | |
| state.minedPixels = 0; | |
| // Увеличиваем размер холста для следующего блока | |
| config.cols += config.blockSizeIncrease; | |
| config.rows += config.blockSizeIncrease; | |
| // Инициализируем новый холст | |
| initCanvas(); | |
| // Показываем уведомление | |
| showBlockCompleteNotification(blockData); | |
| } | |
| // Генерация изображения блока (для демонстрации) | |
| function generateBlockImage() { | |
| // В реальном приложении это будет canvas.toDataURL() | |
| return `https://picsum.photos/200/200?random=${state.currentBlock}`; | |
| } | |
| // Показать уведомление о завершении блока | |
| function showBlockCompleteNotification(blockData) { | |
| const notification = document.createElement('div'); | |
| notification.style.position = 'fixed'; | |
| notification.style.bottom = '20px'; | |
| notification.style.left = '50%'; | |
| notification.style.transform = 'translateX(-50%)'; | |
| notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; | |
| notification.style.color = 'white'; | |
| notification.style.padding = '12px 20px'; | |
| notification.style.borderRadius = '8px'; | |
| notification.style.zIndex = '1000'; | |
| notification.style.display = 'flex'; | |
| notification.style.alignItems = 'center'; | |
| notification.style.gap = '8px'; | |
| notification.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.3)'; | |
| notification.innerHTML = ` | |
| <div style="font-weight: bold; color: var(--accent-color)">Блок #${blockData.id} завершен!</div> | |
| <div style="font-size: 0.9rem;">Создатель: ${blockData.creator}</div> | |
| `; | |
| document.body.appendChild(notification); | |
| setTimeout(() => { | |
| notification.style.transition = 'opacity 0.5s'; | |
| notification.style.opacity = '0'; | |
| setTimeout(() => notification.remove(), 500); | |
| }, 3000); | |
| } | |
| // Обновление списка игроков онлайн | |
| function updateOnlinePlayers() { | |
| // В реальном приложении это будет получаться с сервера | |
| state.onlinePlayers = Math.floor(Math.random() * 50) + config.fakePlayers; | |
| updateUI(); | |
| } | |
| // Обновление прогресса блока | |
| function updateBlockProgress() { | |
| // В реальном приложении это будет получаться с сервера | |
| if (state.minedPixels < config.cols * config.rows) { | |
| state.minedPixels += Math.floor(Math.random() * 5) + 1; | |
| // Ограничиваем максимумом | |
| if (state.minedPixels > config.cols * config.rows) { | |
| state.minedPixels = config.cols * config.rows; | |
| } | |
| updateUI(); | |
| } | |
| } | |
| // Обновление таблицы лидеров | |
| function updateLeaderboard() { | |
| // В реальном приложении это будет получаться с сервера | |
| const fakeData = Array.from({length: 10}, (_, i) => ({ | |
| id: i+1, | |
| name: ['Игрок1', 'Игрок2', 'Игрок3', 'Игрок4', 'Игрок5', 'Игрок6', 'Игрок7', 'Игрок8'][i] || `Игрок${i+1}`, | |
| score: Math.floor(Math.random() * 10000) + 1000, | |
| avatar: `https://picsum.photos/200/200?random=${i+100}` | |
| })); | |
| // Добавляем текущего игрока, если его нет в топе | |
| if (!fakeData.some(p => p.id === 999)) { | |
| fakeData.push({ | |
| id: 999, | |
| name: state.playerData.name, | |
| score: state.playerData.score, | |
| avatar: state.playerData.avatar | |
| }); | |
| } | |
| // Сортируем по очкам | |
| fakeData.sort((a, b) => b.score - a.score); | |
| state.leaderboard = fakeData.slice(0, 10); | |
| // Обновляем UI | |
| elements.leaderboardBody.innerHTML = ''; | |
| state.leaderboard.forEach((player, index) => { | |
| const isCurrentUser = player.id === 999; | |
| const row = document.createElement('tr'); | |
| row.style.background = isCurrentUser ? 'rgba(76, 201, 240, 0.2)' : 'transparent'; | |
| row.innerHTML = ` | |
| <td> | |
| ${index < 3 ? | |
| `<span class="medal ${index === 0 ? 'medal-gold' : index === 1 ? 'medal-silver' : 'medal-bronze'}"> | |
| ${index + 1} | |
| </span>` : | |
| index + 1 | |
| } | |
| </td> | |
| <td> | |
| <div style="display: flex; align-items: center; gap: 8px;"> | |
| <img src="${player.avatar}" style="width: 24px; height: 24px; border-radius: 50%;"> | |
| ${player.name} | |
| </div> | |
| </td> | |
| <td>${player.score.toLocaleString()}</td> | |
| `; | |
| elements.leaderboardBody.appendChild(row); | |
| // Обновляем место текущего игрока | |
| if (isCurrentUser) { | |
| const rank = index + 1; | |
| elements.userRank.textContent = rank; | |
| } | |
| }); | |
| // Если текущего игрока нет в топе, показываем его место | |
| if (!state.leaderboard.some(p => p.id === 999)) { | |
| elements.userRank.textContent = '>10'; | |
| } | |
| } | |
| // Имитация других игроков | |
| function simulateOtherPlayers() { | |
| const unminedPixels = Array.from(document.querySelectorAll('.pixel:not(.pixel-mined)')); | |
| if (unminedPixels.length === 0) return; | |
| const simultaneousMining = Math.floor(Math.random() * 5); | |
| for (let i = 0; i < simultaneousMining; i++) { | |
| if (Math.random() < 0.7) { // 70% шанс, что другой игрок добудет пиксель | |
| const randomIndex = Math.floor(Math.random() * unminedPixels.length); | |
| minePixel(parseInt(unminedPixels[randomIndex].dataset.index)); | |
| // Обновляем список неподобранных пикселей | |
| unminedPixels.splice(randomIndex, 1); | |
| if (unminedPixels.length === 0) break; | |
| } | |
| } | |
| } | |
| // Обновление истории блоков | |
| function updateHistory() { | |
| elements.historyList.innerHTML = ''; | |
| if (state.blockHistory.length === 0) { | |
| elements.historyList.innerHTML = '<div style="text-align: center; padding: 12px; color: #666;">История блоков пуста</div>'; | |
| return; | |
| } | |
| state.blockHistory.forEach(block => { | |
| const item = document.createElement('div'); | |
| item.className = 'history-item'; | |
| item.innerHTML = ` | |
| <div> | |
| <div style="font-weight: bold;">Блок #${block.id}</div> | |
| <div style="font-size: 0.8rem; color: #aaa;">${block.completedAt.toLocaleTimeString()}</div> | |
| </div> | |
| <div style="text-align: right;"> | |
| <div>${block.creator}</div> | |
| <div style="font-size: 0.8rem;"> | |
| <span>${block.score} PX</span> | |
| ${block.silver > 0 ? `<span class="rare-icon">✨${block.silver}</span>` : ''} | |
| ${block.gold > 0 ? `<span class="rare-icon">⭐${block.gold}</span>` : ''} | |
| ${block.diamond > 0 ? `<span class="rare-icon">💎${block.diamond}</span>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| elements.historyList.appendChild(item); | |
| }); | |
| } | |
| // Обновление профиля | |
| function updateProfile() { | |
| elements.profileName.textContent = state.playerData.name; | |
| if (state.playerData.avatar) { | |
| elements.userAvatar.style.backgroundImage = `url(${state.playerData.avatar})`; | |
| elements.userAvatar.style.backgroundSize = 'cover'; | |
| elements.profileAvatar.style.backgroundImage = `url(${state.playerData.avatar})`; | |
| elements.profileAvatar.style.backgroundSize = 'cover'; | |
| } | |
| // Обновляем сетку созданных блоков | |
| elements.avatarGrid.innerHTML = ''; | |
| if (state.createdBlocks.length === 0) { | |
| elements.avatarGrid.innerHTML = '<div style="grid-column: 1 / -1; text-align: center; padding: 12px;">Нет созданных блоков</div>'; | |
| return; | |
| } | |
| state.createdBlocks.forEach(block => { | |
| const item = document.createElement('div'); | |
| item.className = 'avatar-item'; | |
| item.dataset.blockId = block.id; | |
| const img = document.createElement('img'); | |
| img.src = block.image; | |
| img.alt = `Блок ${block.id}`; | |
| item.appendChild(img); | |
| elements.avatarGrid.appendChild(item); | |
| }); | |
| } | |
| // Обработчики событий | |
| function setupEventListeners() { | |
| // Кнопка добычи пикселя | |
| elements.mineButton.addEventListener('click', () => { | |
| minePixel(); | |
| }); | |
| // Переключение вкладок | |
| elements.tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| elements.tabs.forEach(t => t.classList.remove('active')); | |
| tab.classList.add('active'); | |
| const tabName = tab.dataset.tab; | |
| elements.tabContents.forEach(content => { | |
| content.classList.remove('active'); | |
| if (content.id === `${tabName}-content`) { | |
| content.classList.add('active'); | |
| // Обновляем данные при открытии вкладки | |
| if (tabName === 'leaderboard') { | |
| updateLeaderboard(); | |
| } else if (tabName === 'history') { | |
| updateHistory(); | |
| } else if (tabName === 'profile') { | |
| updateProfile(); | |
| } | |
| } | |
| }); | |
| }); | |
| }); | |
| // Клик по пикселю | |
| elements.canvas.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('pixel')) { | |
| const index = parseInt(e.target.dataset.index); | |
| if (!e.target.classList.contains('pixel-mined')) { | |
| if (Math.random() < 0.5) { // 50% шанс успешной добычи при клике | |
| minePixel(index); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Инициализация игры | |
| function initGame() { | |
| // Устанавливаем имя игрока из Telegram WebApp.initData | |
| // В демо-версии используем случайное имя | |
| const randomName = `Игрок${Math.floor(Math.random() * 1000)}`; | |
| state.playerData.name = randomName; | |
| state.playerData.avatar = `https://picsum.photos/200/200?random=${Math.floor(Math.random() * 1000)}`; | |
| initCanvas(); | |
| updateUI(); | |
| setupEventListeners(); | |
| socket.connect(); | |
| // Инициализация демо-данных | |
| setTimeout(() => { | |
| updateLeaderboard(); | |
| updateHistory(); | |
| updateProfile(); | |
| }, 500); | |
| } | |
| // Запуск игры | |
| initGame(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |