UNPKG

hytopia

Version:

The HYTOPIA SDK makes it easy for developers to create massively multiplayer games using JavaScript or TypeScript.

1,339 lines (1,142 loc) 34.9 kB
<!-- Fonts--> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"> <!-- UI--> <div class="crosshair"></div> <div class="damage-indicator-container"> <div class="damage-indicator top"></div> <div class="damage-indicator right"></div> <div class="damage-indicator bottom"></div> <div class="damage-indicator left"></div> </div> <div class="hit-damage-container"></div> <img src="{{CDN_ASSETS_URL}}/ui/images/scope.png" class="scope-overlay"> <!-- Game Start Announcement --> <div class="game-start-announcement">DEATHMATCH!</div> <!-- Winner Announcement --> <div class="winner-announcement"></div> <!-- Leaderboard --> <div class="leaderboard"> <div class="leaderboard-title">Deathmatch</div> <div class="leaderboard-timer">0:00</div> <div class="leaderboard-players-count">Players: 0</div> <div class="leaderboard-header"> <div class="header-name">Player</div> <div class="header-kills">Kills</div> </div> <div class="leaderboard-players"> <div class="leaderboard-player no-kills"> <div class="player-name">Waiting for players...</div> <div class="player-kills"></div> </div> </div> </div> <div class="hud"> <div class="info-container"> <div class="ammo-indicator" style="display: none;"> <img src="{{CDN_ASSETS_URL}}/icons/ammo.png" alt="Ammo Icon" class="ammo-icon"> <span class="current-ammo">30</span> <span class="ammo-divider">/</span> <span class="total-ammo">30</span> </div> <div class="shield-bar"> <div class="shield-bar-fill"></div> <img src="{{CDN_ASSETS_URL}}/icons/shield.png" alt="Shield Icon" class="shield-icon"> <div class="shield-text">0</div> </div> <div class="health-bar"> <div class="health-bar-fill"></div> <img src="{{CDN_ASSETS_URL}}/icons/heart.png" alt="Health Icon" class="health-icon"> <div class="health-text">100</div> </div> </div> </div> <div class="mobile-buttons-container"> <div id="mobile-reload-button" class="mobile-button"> <img src="{{CDN_ASSETS_URL}}/icons/mobile-reload.png" /> </div> <div id="mobile-interact-button" class="mobile-button">E</div> <div id="mobile-jump-button" class="mobile-button"> <img src="{{CDN_ASSETS_URL}}/icons/mobile-jump.png" /> </div> <div id="mobile-attack-button" class="mobile-button"> <img src="{{CDN_ASSETS_URL}}/icons/mobile-shoot.png" /> </div> <div id="mobile-place-block-button" class="mobile-button"> <img src="{{CDN_ASSETS_URL}}/icons/mobile-place-block.png" /> </div> </div> <div class="materials-counter"> <img src="{{CDN_ASSETS_URL}}/icons/block.png" alt="Materials Icon" class="materials-icon"> <span class="materials-amount">0</span> </div> <div class="inventory-hud"> <div class="inventory-slot inventory-active-slot" data-slot="0"> <div class="slot-number">F</div> <img class="slot-icon" style="display: none;"> <div class="slot-quantity" style="display: none;"></div> <div class="slot-name" style="display: none;"></div> </div> <div class="inventory-slot" data-slot="1"> <div class="slot-number">1</div> <img class="slot-icon" style="display: none;"> <div class="slot-quantity" style="display: none;"></div> <div class="slot-name" style="display: none;"></div> </div> <div class="inventory-slot" data-slot="2"> <div class="slot-number">2</div> <img class="slot-icon" style="display: none;"> <div class="slot-quantity" style="display: none;"></div> <div class="slot-name" style="display: none;"></div> </div> <div class="inventory-slot" data-slot="3"> <div class="slot-number">3</div> <img class="slot-icon" style="display: none;"> <div class="slot-quantity" style="display: none;"></div> <div class="slot-name" style="display: none;"></div> </div> <div class="inventory-slot" data-slot="4"> <div class="slot-number">4</div> <img class="slot-icon" style="display: none;"> <div class="slot-quantity" style="display: none;"></div> <div class="slot-name" style="display: none;"></div> </div> <div class="inventory-slot" data-slot="5"> <div class="slot-number">5</div> <img class="slot-icon" style="display: none;"> <div class="slot-quantity" style="display: none;"></div> <div class="slot-name" style="display: none;"></div> </div> </div> <!-- Scene UI Templates --> <template id="item-label-template"> <div class="item-label"> <div class="label-quantity"></div> <div class="label-name"></div> <div class="label-prompt">"E" to Pickup</div> <div class="label-caret"></div> </div> </template> <template id="chest-label-template"> <div class="item-label"> <div class="label-name"></div> <div class="label-prompt">"E" to Open</div> <div class="label-caret"></div> </div> </template> <!-- UI Scripts--> <script> const CDN_ASSETS_URL = '{{CDN_ASSETS_URL}}'; let leaderboardKillCounts = {}; let gameEndTime = 0; let timerInterval; function updateLeaderboard() { const leaderboardPlayers = document.querySelector('.leaderboard-players'); leaderboardPlayers.innerHTML = ''; // Check if game has started if (gameEndTime === 0) { // Show "Waiting for players..." message if game hasn't started const waitingElement = document.createElement('div'); waitingElement.className = 'leaderboard-player no-kills'; waitingElement.innerHTML = ` <div class="player-name">Waiting for players...</div> <div class="player-kills"></div> `; leaderboardPlayers.appendChild(waitingElement); return; } // Get sorted players by kill count const sortedPlayers = Object.entries(leaderboardKillCounts) .sort((a, b) => b[1] - a[1]); if (sortedPlayers.length === 0 || sortedPlayers.every(player => player[1] === 0)) { // Show "No kills yet" message if no players have kills const noKillsElement = document.createElement('div'); noKillsElement.className = 'leaderboard-player no-kills'; noKillsElement.innerHTML = ` <div class="player-name">No kills yet</div> <div class="player-kills"></div> `; leaderboardPlayers.appendChild(noKillsElement); return; } // Create player elements for the leaderboard sortedPlayers.forEach((player, index) => { const [username, killCount] = player; // Skip players with 0 kills if (killCount === 0) return; const playerElement = document.createElement('div'); playerElement.className = 'leaderboard-player'; let rankIcon = ''; if (index === 0) { rankIcon = `<img src="${CDN_ASSETS_URL}/icons/crown-gold.png" class="rank-icon">`; } else if (index === 1) { rankIcon = `<img src="${CDN_ASSETS_URL}/icons/crown-silver.png" class="rank-icon">`; } else if (index === 2) { rankIcon = `<img src="${CDN_ASSETS_URL}/icons/crown-bronze.png" class="rank-icon">`; } playerElement.innerHTML = ` <div class="player-name"> ${rankIcon} ${username} </div> <div class="player-kills">${killCount}</div> `; leaderboardPlayers.appendChild(playerElement); }); } function updateTimer() { const now = Date.now(); const timeRemaining = Math.max(0, gameEndTime - now); // Format time as mm:ss const minutes = Math.floor(timeRemaining / 60000); const seconds = Math.floor((timeRemaining % 60000) / 1000); const formattedTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; const timerElement = document.querySelector('.leaderboard-timer'); timerElement.textContent = formattedTime; // Stop the timer when it reaches zero if (timeRemaining <= 0) { clearInterval(timerInterval); } } function showGameStartAnnouncement() { const announcement = document.querySelector('.game-start-announcement'); announcement.classList.add('active'); // Remove the active class after animation completes setTimeout(() => { announcement.classList.remove('active'); }, 3000); } function showWinnerAnnouncement(username) { const announcement = document.querySelector('.winner-announcement'); announcement.textContent = `${username} wins!`; announcement.classList.add('active'); // Remove the active class after animation completes setTimeout(() => { announcement.classList.remove('active'); }, 5000); } hytopia.registerSceneUITemplate('item-label', (id, onState) => { const template = document.getElementById('item-label-template'); const clone = template.content.cloneNode(true); const labelName = clone.querySelector('.label-name'); const labelQuantity = clone.querySelector('.label-quantity'); onState(state => { if (state.name) { labelName.textContent = state.name; } if (state.quantity !== undefined) { labelQuantity.textContent = state.quantity !== -1 ? state.quantity : '∞'; } }); return clone; }); hytopia.registerSceneUITemplate('chest-label', (id, onState) => { const template = document.getElementById('chest-label-template'); const clone = template.content.cloneNode(true); const labelName = clone.querySelector('.label-name'); onState(state => { if (state.name) { labelName.textContent = state.name; } }); return clone; }); hytopia.onData(data => { const { type } = data; if (!type) { return console.warn('No type received for data', data); } if (type === 'game-start') { showGameStartAnnouncement(); } if (type === 'announce-winner') { const { username } = data; showWinnerAnnouncement(username); } if (type === 'ammo-indicator') { const { ammo, totalAmmo, show, reloading } = data; if (show !== undefined) { const ammoIndicator = document.querySelector('.ammo-indicator'); ammoIndicator.style.display = show ? 'block' : 'none'; } if (reloading !== undefined) { const currentAmmo = document.querySelector('.current-ammo'); currentAmmo.textContent = reloading ? '...' : ammo; } if (ammo !== undefined) { const currentAmmo = document.querySelector('.current-ammo'); currentAmmo.textContent = ammo; } if (totalAmmo !== undefined) { const totalAmmoElement = document.querySelector('.total-ammo'); totalAmmoElement.textContent = totalAmmo; } } if (type === 'damage-indicator') { const { direction } = data; // Make sure we have a valid direction object if (!direction || typeof direction.x === 'undefined' || typeof direction.z === 'undefined') { console.error('Invalid direction object:', direction); return; } // Hide all indicators first document.querySelectorAll('.damage-indicator').forEach(el => { el.classList.remove('active'); }); // Determine which indicator to show based on the dominant direction const absX = Math.abs(direction.x); const absZ = Math.abs(direction.z); let indicatorClass; if (absX > absZ) { indicatorClass = direction.x > 0 ? '.damage-indicator.right' : '.damage-indicator.left'; } else { indicatorClass = direction.z > 0 ? '.damage-indicator.top' : '.damage-indicator.bottom'; } // Show and animate the indicator const indicator = document.querySelector(indicatorClass); if (indicator) { indicator.classList.add('active'); indicator.style.animation = 'none'; indicator.offsetHeight; // Force reflow indicator.style.animation = 'fadeOut 1s forwards'; } } if (type === 'show-damage') { const { damage } = data; // Create a new damage number element const damageNumber = document.createElement('div'); damageNumber.className = 'hit-damage-number'; damageNumber.textContent = damage; // Add some randomness to position const randomX = Math.random() * 60 - 30; // -30 to 30px const randomY = Math.random() * 40 - 20; // -20 to 20px damageNumber.style.transform = `translate(${randomX}px, ${randomY}px)`; // Add to container const container = document.querySelector('.hit-damage-container'); container.appendChild(damageNumber); // Remove after animation completes setTimeout(() => { damageNumber.remove(); }, 500); } if (type === 'health') { const { health, maxHealth } = data; const healthText = document.querySelector('.health-text'); const healthBarFill = document.querySelector('.health-bar-fill'); healthText.textContent = health; healthBarFill.style.width = `${(health / maxHealth) * 100}%`; } if (type === 'inventory') { const { inventory } = data; inventory.forEach((item, i) => { const slot = document.querySelector(`.inventory-slot[data-slot="${i}"]`); const icon = slot.querySelector('.slot-icon'); const name = slot.querySelector('.slot-name'); const quantity = slot.querySelector('.slot-quantity'); icon.style.display = item ? 'block' : 'none'; name.style.display = item ? 'block' : 'none'; quantity.style.display = item ? 'block' : 'none'; if (!item) { return; } icon.src = `${CDN_ASSETS_URL}/${item.iconImageUri}`; name.textContent = item.name; quantity.textContent = item.quantity !== -1 ? item.quantity : '∞'; }); } if (type === 'inventory-active-slot') { const { index } = data; // Remove active slot class from all slots document.querySelectorAll('.inventory-slot').forEach(slot => { slot.classList.remove('inventory-active-slot'); }); // Add active slot class to selected slot const activeSlot = document.querySelector(`.inventory-slot[data-slot="${index}"]`); if (activeSlot) { activeSlot.classList.add('inventory-active-slot'); } } if (type === 'inventory-quantity-update') { const { index, quantity } = data; const slot = document.querySelector(`.inventory-slot[data-slot="${index}"]`); const quantityElement = slot.querySelector('.slot-quantity'); quantityElement.textContent = quantity; } if (type === 'leaderboard-sync') { const { killCounts } = data; leaderboardKillCounts = killCounts; updateLeaderboard(); } if (type === 'leaderboard-update') { const { username, killCount } = data; leaderboardKillCounts[username] = killCount; updateLeaderboard(); } if (type === 'players-count') { const { count } = data; const playersCount = document.querySelector('.leaderboard-players-count'); playersCount.textContent = `Players: ${count}`; } if (type === 'materials') { const { materials } = data; const materialsAmount = document.querySelector('.materials-amount'); const currentMaterials = parseInt(materialsAmount.textContent); const difference = materials - currentMaterials; if (difference !== 0) { const materialsCounter = document.querySelector('.materials-counter'); const floatingNumber = document.createElement('div'); floatingNumber.className = 'floating-number'; floatingNumber.textContent = difference > 0 ? `+${difference}` : difference; materialsCounter.appendChild(floatingNumber); // Remove the element after animation completes setTimeout(() => { floatingNumber.remove(); }, 1000); } materialsAmount.textContent = materials; } if (type === 'shield') { const { shield, maxShield } = data; const shieldText = document.querySelector('.shield-text'); const shieldBarFill = document.querySelector('.shield-bar-fill'); shieldText.textContent = shield; shieldBarFill.style.width = `${(shield / maxShield) * 100}%`; } if (type === 'scope-zoom') { const { zoom } = data; const scopeOverlay = document.querySelector('.scope-overlay'); if (zoom === 1) { scopeOverlay.classList.remove('active'); } else { scopeOverlay.classList.add('active'); } } if (type === 'timer-sync') { const { startedAt, endsAt } = data; // Clear any existing timer interval if (timerInterval) { clearInterval(timerInterval); } // Set the end time gameEndTime = endsAt; // Update timer immediately updateTimer(); // Set up interval to update timer every second timerInterval = setInterval(updateTimer, 1000); } }); // Mobile UI Controls const inventorySlots = document.querySelectorAll('.inventory-slot'); inventorySlots.forEach((slot, index) => { slot.addEventListener('click', () => { hytopia.sendData({ type: 'inventory-select', index: index, }) }); }); const mobileInteractButton = document.getElementById('mobile-interact-button'); mobileInteractButton.addEventListener('touchstart', () => { mobileInteractButton.classList.add('active'); hytopia.pressInput('e', true); }); mobileInteractButton.addEventListener('touchend', () => { mobileInteractButton.classList.remove('active'); hytopia.pressInput('e', false); }); const mobileReloadButton = document.getElementById('mobile-reload-button'); mobileReloadButton.addEventListener('touchstart', () => { mobileReloadButton.classList.add('active'); hytopia.pressInput('r', true); }); mobileReloadButton.addEventListener('touchend', () => { mobileReloadButton.classList.remove('active'); hytopia.pressInput('r', false); }); const mobileJumpButton = document.getElementById('mobile-jump-button'); mobileJumpButton.addEventListener('touchstart', () => { mobileJumpButton.classList.add('active'); hytopia.pressInput(' ', true); }); mobileJumpButton.addEventListener('touchend', () => { mobileJumpButton.classList.remove('active'); hytopia.pressInput(' ', false); }); const mobileAttackButton = document.getElementById('mobile-attack-button'); mobileAttackButton.addEventListener('touchstart', () => { mobileAttackButton.classList.add('active'); hytopia.pressInput('ml', true); }, { passive: true }); mobileAttackButton.addEventListener('touchend', () => { mobileAttackButton.classList.remove('active'); hytopia.pressInput('ml', false); }, { passive: true }); const mobilePlaceBlockButton = document.getElementById('mobile-place-block-button'); mobilePlaceBlockButton.addEventListener('touchstart', () => { mobilePlaceBlockButton.classList.add('active'); hytopia.pressInput('mr', true); }); mobilePlaceBlockButton.addEventListener('touchend', () => { mobilePlaceBlockButton.classList.remove('active'); hytopia.pressInput('mr', false); }); </script> <!-- UI Styles --> <style> .crosshair { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20px; height: 20px; pointer-events: none; opacity: 0.7; } .crosshair::before, .crosshair::after { content: ''; position: absolute; background-color: rgba(255, 255, 255, 0.8); } .crosshair::before { width: 2px; height: 100%; left: 50%; transform: translateX(-50%); } .crosshair::after { width: 100%; height: 2px; top: 50%; transform: translateY(-50%); } .game-start-announcement { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); font-family: 'Inter', sans-serif; font-size: 60px; font-weight: bold; color: #ff3333; text-shadow: 0 0 10px rgba(0, 0, 0, 0.7), 0 0 20px rgba(0, 0, 0, 0.5); text-transform: uppercase; letter-spacing: 2px; opacity: 0; z-index: 1000; pointer-events: none; text-align: center; } .game-start-announcement.active { animation: announcementFade 5s ease-in-out forwards; } .winner-announcement { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); font-family: 'Inter', sans-serif; font-size: 80px; font-weight: bold; color: #ffd700; text-shadow: 0 0 15px rgba(0, 0, 0, 0.8), 0 0 30px rgba(255, 215, 0, 0.6); text-transform: uppercase; letter-spacing: 3px; opacity: 0; z-index: 1000; pointer-events: none; text-align: center; } .winner-announcement.active { animation: winnerFade 5s ease-in-out forwards; } @keyframes announcementFade { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); } 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } } @keyframes winnerFade { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); } 40% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); } 60% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); } 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } } .hit-damage-container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; z-index: 100; width: 100px; height: 100px; display: flex; justify-content: center; align-items: center; } .hit-damage-number { color: white; font-family: 'Inter', sans-serif; font-size: 18px; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); animation: damagePopup 0.5s ease-out forwards; position: absolute; } @keyframes damagePopup { 0% { opacity: 0; transform: scale(0.5) translateY(0); } 20% { opacity: 1; transform: scale(1.2) translateY(0); } 100% { opacity: 0; transform: scale(1) translateY(-50px); } } .materials-counter { position: fixed; bottom: 120px; right: 20px; background: rgba(0, 0, 0, 0.5); padding: 8px 15px; border-radius: 5px; display: flex; align-items: center; gap: 10px; font-family: 'Inter', sans-serif; color: #ffffff; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); border: 2px solid rgba(255, 255, 255, 0.3); box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } .floating-number { position: absolute; color: #ffffff; font-weight: bold; font-size: 16px; pointer-events: none; animation: floatUp 1s ease-out forwards; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } @keyframes floatUp { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-50px); } } .materials-icon { width: 20px; height: 20px; object-fit: contain; } .materials-amount { font-size: 16px; font-weight: bold; } .hud { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; z-index: 99; gap: 10px; font-family: 'Inter', sans-serif; text-transform: uppercase; color: #ffffff; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } .ammo-indicator { text-align: center; font-size: 18px; font-weight: bold; margin-bottom: 5px; background: rgba(0, 0, 0, 0.5); padding: 5px 15px; border-radius: 3px; box-shadow: 0 0 10px rgba(255, 255, 255, 0.1); display: flex; align-items: center; justify-content: center; user-select: none; gap: 10px; } .ammo-icon { width: 18px; height: 18px; object-fit: contain; position: relative; top: 2px; } .ammo-divider { margin: 0 5px; opacity: 0.7; } .inventory-hud { position: fixed; bottom: 20px; right: 20px; display: flex; gap: 10px; z-index: 999; } .inventory-slot { width: 60px; height: 60px; background: rgba(0, 0, 0, 0.5); border-radius: 5px; border: 2px solid rgba(255, 255, 255, 0.3); display: flex; align-items: center; justify-content: center; position: relative; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); } .inventory-active-slot { border-color: rgba(255, 255, 255, 0.6); } .slot-number { position: absolute; top: -20px; left: 50%; transform: translateX(-50%); font-family: 'Inter', sans-serif; font-size: 14px; font-weight: bold; color: rgba(255, 255, 255, 0.8); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); } .slot-icon { max-width: 70%; max-height: 70%; object-fit: contain; } .slot-quantity { position: absolute; bottom: 2px; left: 2px; font-family: 'Inter', sans-serif; font-size: 12px; font-weight: bold; color: rgba(255, 255, 255, 0.9); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.5); padding: 1px 4px; border-radius: 3px; } .slot-name { position: absolute; bottom: -18px; left: 50%; transform: translateX(-50%); font-family: 'Inter', sans-serif; font-size: 10px; font-weight: bold; color: rgba(255, 255, 255, 0.8); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); white-space: nowrap; } .info-container { display: flex; align-items: center; gap: 10px; flex-direction: column; } .shield-bar { width: 250px; height: 20px; background: rgba(0, 0, 0, 0.5); border-radius: 3px; overflow: hidden; box-shadow: 0 0 10px rgba(0, 0, 255, 0.3); position: relative; } .shield-bar-fill { width: 0%; height: 100%; background: linear-gradient(to right, #0000ff, #3333ff); transition: width 0.3s ease; } .shield-icon { position: absolute; left: 5px; top: 50%; transform: translateY(-50%); height: 12px; width: 12px; z-index: 1; } .shield-text { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-size: 0.8em; font-weight: bold; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); z-index: 1; } .health-bar { width: 250px; height: 20px; background: rgba(0, 0, 0, 0.5); border-radius: 3px; overflow: hidden; box-shadow: 0 0 10px rgba(255, 0, 0, 0.3); position: relative; } .health-bar-fill { width: 100%; height: 100%; background: linear-gradient(to right, #ff0000, #ff3333); transition: width 0.3s ease; } .health-icon { position: absolute; left: 5px; top: 50%; transform: translateY(-50%); height: 12px; width: 12px; z-index: 1; } .health-text { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); font-size: 0.8em; font-weight: bold; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); z-index: 1; } .item-label { background-color: rgba(0, 0, 0, 0.6); padding: 12px 20px; border-radius: 4px; text-align: center; color: #ffffff; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); font-family: 'Inter', sans-serif; text-transform: uppercase; position: relative; max-width: 220px; margin: 0 auto; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2); } .label-name { font-size: 20px; margin-bottom: 6px; font-weight: bold; } .label-quantity { position: absolute; top: -12px; right: -10px; background: #ffb700; color: black; font-weight: bold; padding: 2px 4px; font-size: 14px; text-transform: none; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); text-shadow: none; border: 1px solid #ffffff; } .label-prompt { font-size: 14px; opacity: 0.8; } .label-caret { position: absolute; bottom: -8px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 8px solid rgba(0, 0, 0, 0.6); } .scope-overlay { position: fixed; top: 0; left: 0; pointer-events: none; width: 100%; height: 100%; z-index: 99; object-fit: cover; opacity: 0; transition: opacity 0.3s ease; } .scope-overlay.active { opacity: 1; } #chat-window { width: 25% !important; } .damage-indicator-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 5; } .damage-indicator { position: absolute; opacity: 0; background: linear-gradient(to center, rgba(255,0,0,0.7), rgba(255,0,0,0)); } .damage-indicator.active { opacity: 1; } /* Top damage indicator */ .damage-indicator.top { top: 0; left: 0; width: 100%; height: 150px; background: linear-gradient(to bottom, rgba(255,0,0,0.7), rgba(255,0,0,0)); } /* Right damage indicator */ .damage-indicator.right { top: 0; right: 0; width: 150px; height: 100%; background: linear-gradient(to left, rgba(255,0,0,0.7), rgba(255,0,0,0)); } /* Bottom damage indicator */ .damage-indicator.bottom { bottom: 0; left: 0; width: 100%; height: 150px; background: linear-gradient(to top, rgba(255,0,0,0.7), rgba(255,0,0,0)); } /* Left damage indicator */ .damage-indicator.left { top: 0; left: 0; width: 150px; height: 100%; background: linear-gradient(to right, rgba(255,0,0,0.7), rgba(255,0,0,0)); } @keyframes fadeOut { 0% { opacity: 1; } 100% { opacity: 0; } } /* Leaderboard Styles */ .leaderboard { position: fixed; top: 20px; right: 20px; background: rgba(0, 0, 0, 0.7); border-radius: 5px; padding: 10px; width: 15%; max-width: 200px; max-height: 40%; font-family: 'Arial', sans-serif; color: white; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); border: 2px solid rgba(255, 255, 255, 0.2); z-index: 100; } .leaderboard-title { text-align: center; font-size: 18px; font-weight: bold; padding-bottom: 5px; margin-bottom: 5px; border-bottom: 1px solid rgba(255, 255, 255, 0.3); text-transform: uppercase; letter-spacing: 1px; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); } .leaderboard-timer { text-align: center; font-size: 16px; font-weight: bold; margin-bottom: 10px; padding: 3px; background: rgba(255, 255, 255, 0.1); border-radius: 3px; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); } .leaderboard-header { display: flex; justify-content: space-between; padding: 3px 8px; font-size: 12px; font-weight: bold; text-transform: uppercase; color: rgba(255, 255, 255, 0.7); border-bottom: 1px solid rgba(255, 255, 255, 0.2); margin-bottom: 5px; } .leaderboard-players { display: flex; flex-direction: column; gap: 5px; } .leaderboard-player { display: flex; justify-content: space-between; padding: 5px 8px; background: rgba(255, 255, 255, 0.1); border-radius: 3px; transition: background 0.2s ease; } .leaderboard-player:hover { background: rgba(255, 255, 255, 0.2); } .leaderboard-players-count { font-size: 14px; font-weight: bold; color: rgba(255, 255, 255, 0.7); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); text-transform: uppercase; text-align: center; margin-bottom: 10px; padding: 3px; background: rgba(255, 255, 255, 0.1); border-radius: 3px; } .rank-icon { width: 16px; height: 16px; margin-right: 5px; position: relative; top: 1px; } .player-name { font-weight: bold; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); white-space: nowrap; overflow: hidden; max-width: 160px; text-overflow: ellipsis; } .mobile-buttons-container { display: none; } /* Mobile UI */ body.mobile .hud { left: 50%; transform: translateX(-50%); bottom: 35px; right: auto; } body.mobile .inventory-hud { bottom: 30px; } body.mobile .inventory-slot { width: 45px; height: 45px; } body.mobile .slot-number { font-size: 12px; top: -16px; } body.mobile .slot-name { font-size: 8px; bottom: -14px; } body.mobile .slot-quantity { font-size: 10px; } body.mobile .shield-bar, body.mobile .health-bar { width: 120px; height: 16px; } body.mobile .shield-text, body.mobile .health-text { font-size: 0.7em; } body.mobile .shield-icon, body.mobile .health-icon { width: 10px; height: 10px; } body.mobile .leaderboard-title { font-size: 14px; } body.mobile .leaderboard-timer { font-size: 12px; } body.mobile .leaderboard-players-count { font-size: 12px; } body.mobile .leaderboard-header { font-size: 10px; } body.mobile .leaderboard-player { font-size: 10px; } body.mobile .materials-counter { right: 20px; bottom: 115px; font-size: 12px; } body.mobile .floating-number { font-size: 14px; } body.mobile div > canvas { /* hide three debugger panels */ display: none !important; } body.mobile .mobile-buttons-container { display: flex; gap: 10px; position: fixed; bottom: 113px; right: 118px; user-select: none; } body.mobile .mobile-button { background-color: rgba(0, 0, 0, 0.5); border-radius: 50%; align-items: center; justify-content: center; display: flex; width: 45px; height: 45px; -webkit-tap-highlight-color: transparent; touch-action: manipulation; user-select: none; transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); will-change: transform, background-color; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); font-family: 'Inter', sans-serif; font-size: 14px; font-weight: bold; color: rgba(255, 255, 255, 0.8); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); } body.mobile .mobile-button.active { transform: scale(0.92); background-color: rgba(0, 0, 0, 0.75); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); } body.mobile .mobile-button img { width: 25px; height: 25px; } </style>