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
HTML
<!-- 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% ;
}
.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 ;
}
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>