UNPKG

hytopia

Version:

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

635 lines (553 loc) 16.2 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=Creepster&display=swap" rel="stylesheet"> <!-- UI--> <div class="boss-health-container"> <div class="boss-name"></div> <div class="boss-health-bar"> <div class="boss-health-fill"></div> </div> </div> <div class="crosshair"></div> <div class="vignette"></div> <div class="wave-announcement">Wave <span class="wave-number"></span></div> <div class="game-info"> <div class="timer">Time: 00:00</div> <div class="wave">Wave: 1</div> </div> <div class="hud"> <div class="weapon-info"> <div class="weapon-text"> <div class="weapon-name">Pistol</div> <div class="weapon-ammo">Clip: 7/7</div> </div> <img src="" alt="Current Weapon" class="weapon-image"> </div> <div class="info-container"> <div class="money-display"> <span class="money-symbol">$</span> <span class="money-amount">0</span> </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/100</div> </div> </div> </div> <template id="downed-player-template"> <div class="downed-player"> <div class="revive-text">Downed Player!</div> <div class="revive-prompt">Hold "E" to revive, stay close!</div> <div class="revive-progress"> <div class="revive-progress-fill"></div> </div> <div class="label-caret"></div> </div> </template> <template id="purchase-label-template"> <div class="purchase-label"> <div class="label-name"></div> <div class="label-cost"></div> <div class="label-prompt">Press "E" to purchase</div> <div class="label-caret"></div> </div> </template> <template id="weapon-roulette-template"> <div class="weapon-roulette"> <img class="roulette-image" src="{{CDN_ASSETS_URL}}/icons/pistol.png" alt="Weapon"> <div class="roulette-name"></div> <div class="roulette-prompt">Press "E" to equip</div> </div> </template> <!-- UI Scripts--> <script> const CDN_ASSETS_URL = '{{CDN_ASSETS_URL}}'; let timerInterval; hytopia.registerSceneUITemplate('downed-player', (id, onState) => { const template = document.getElementById('downed-player-template'); const clone = template.content.cloneNode(true); const progressFill = clone.querySelector('.revive-progress-fill'); onState(state => { progressFill.style.width = `${state.progress}%`; }); return clone; }); // register purchase label template hytopia.registerSceneUITemplate('purchase-label', (id, onState) => { const template = document.getElementById('purchase-label-template'); const clone = template.content.cloneNode(true); const labelName = clone.querySelector('.label-name'); const labelCost = clone.querySelector('.label-cost'); onState(state => { labelName.textContent = state.name; labelCost.textContent = `$${state.cost}`; }); return clone; }); // register weapon roulette template hytopia.registerSceneUITemplate('weapon-roulette', (id, onState) => { const template = document.getElementById('weapon-roulette-template'); const clone = template.content.cloneNode(true); const rouletteImage = clone.querySelector('.roulette-image'); const rouletteName = clone.querySelector('.roulette-name'); const roulettePrompt = clone.querySelector('.roulette-prompt'); let rouletteInterval; // Hide prompt initially roulettePrompt.style.display = 'none'; onState(state => { const { selectedWeaponId, possibleWeapons } = state; let currentIndex = 0; // Clear any existing interval if (rouletteInterval) { clearInterval(rouletteInterval); } // Start rapid weapon cycling rouletteInterval = setInterval(() => { const weapon = possibleWeapons[currentIndex]; rouletteImage.src = `${CDN_ASSETS_URL}/${weapon.iconUri}`; console.log(rouletteImage.src); rouletteName.textContent = weapon.name; currentIndex = (currentIndex + 1) % possibleWeapons.length; }, 100); // After 2 seconds, stop on selected weapon and show prompt setTimeout(() => { clearInterval(rouletteInterval); const selectedWeapon = possibleWeapons.find(w => w.id === selectedWeaponId); if (selectedWeapon) { rouletteImage.src = `${CDN_ASSETS_URL}/${selectedWeapon.iconUri}`; rouletteName.textContent = selectedWeapon.name; roulettePrompt.style.display = 'block'; } }, 3000); }); return clone; }); // handle game data ui updates hytopia.onData(data => { const { type } = data; if (!type) { return console.warn('No type received for data', data); } if (type === 'start') { document.querySelector('.game-info').style.display = 'block'; document.querySelector('.game-info .timer').textContent = 'Time: 00:00'; document.querySelector('.game-info .wave').textContent = 'Wave: 1'; // Clear previous timer if (timerInterval) { clearInterval(timerInterval); } // Start game timer const startTime = Date.now(); timerInterval = setInterval(() => { const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000); const minutes = Math.floor(elapsedSeconds / 60).toString().padStart(2, '0'); const seconds = (elapsedSeconds % 60).toString().padStart(2, '0'); document.querySelector('.game-info .timer').textContent = `Time: ${minutes}:${seconds}`; }, 1000); } if (type === 'wave') { const { wave } = data; document.querySelector('.game-info .wave').textContent = `Wave: ${wave}`; showWaveAnnouncement(wave); } if (type === 'weapon') { const { name, iconImageUri } = data; document.querySelector('.weapon-name').textContent = name; document.querySelector('.weapon-image').src = `${CDN_ASSETS_URL}/${iconImageUri}`; } if (type === 'ammo') { const { ammo, maxAmmo } = data; document.querySelector('.weapon-ammo').textContent = `CLIP: ${ammo}/${maxAmmo}`; } if (type === 'health') { const { health, maxHealth } = data; const percentage = (health / maxHealth) * 100; document.querySelector('.health-bar-fill').style.width = `${percentage}%`; document.querySelector('.health-text').textContent = `${health}/${maxHealth}`; // Update vignette intensity based on health percentage const vignette = document.querySelector('.vignette'); const vignetteOpacity = Math.max(0, (100 - percentage) / 100); vignette.style.opacity = vignetteOpacity; } if (type === 'reload') { document.querySelector('.weapon-ammo').textContent = 'RELOADING...'; } if (type === 'money') { const { money } = data; const moneyAmount = Math.floor(money); const moneyDisplay = document.querySelector('.money-display'); const moneyAmountEl = document.querySelector('.money-amount'); moneyAmountEl.textContent = moneyAmount; // Adjust width based on number of digits (min 2 digits, max 8 digits) const digits = Math.max(2, Math.min(8, moneyAmount.toString().length)); moneyDisplay.style.width = `${10 + (digits * 10)}px`; } if (type === 'boss') { const { name, healthPercent, show } = data; const bossContainer = document.querySelector('.boss-health-container'); const bossName = document.querySelector('.boss-name'); const bossHealthFill = document.querySelector('.boss-health-fill'); console.log(data); if (show === true) { bossContainer.style.display = 'block'; } else if (show === false) { bossContainer.style.display = 'none'; bossHealthFill.style.width = '100%'; } if (name) { bossName.textContent = name; } if (healthPercent) { bossHealthFill.style.width = `${healthPercent}%`; } } }); function showWaveAnnouncement(waveNumber) { const announcement = document.querySelector('.wave-announcement'); const numberSpan = announcement.querySelector('.wave-number'); numberSpan.textContent = waveNumber; announcement.classList.add('show'); setTimeout(() => { announcement.classList.remove('show'); }, 3000); } </script> <!-- UI Styles --> <style> .boss-health-container { display: none; position: fixed; top: 20px; left: 50%; transform: translateX(-50%); width: 40%; text-align: center; } .boss-name { color: #ffffff; font-family: 'Arial', sans-serif; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); margin-bottom: 5px; text-transform: uppercase; } .boss-health-bar { width: 100%; height: 10px; background: rgba(0, 0, 0, 0.5); border-radius: 3px; overflow: hidden; box-shadow: 0 0 10px rgba(255, 0, 0, 0.3); } .boss-health-fill { width: 100%; height: 100%; background: linear-gradient(to right, #800000, #ff0000); transition: width 0.3s ease; } .game-info { display: none; position: fixed; top: 20px; right: 20px; } .game-info div { font-family: 'Arial', sans-serif; font-size: 1em; font-weight: bold; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); color: #ffffff; text-align: right; margin-bottom: 5px; } .hud { position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; z-index: 99; gap: 10px; font-family: 'Arial', sans-serif; text-transform: uppercase; color: #ffffff; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } .vignette { position: fixed; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; background: radial-gradient(circle, transparent 30%, rgba(255, 0, 0, 0.6)); opacity: 0; transition: opacity 0.3s ease; z-index: 9; } .info-container { display: flex; align-items: center; gap: 10px; } .money-display { height: 20px; background: rgba(0, 0, 0, 0.5); border-radius: 3px; padding: 0 10px; display: flex; align-items: center; font-weight: bold; font-size: 0.9em; width: 30px; /* Initial width for 2 digits */ transition: width 0.3s ease; white-space: nowrap; } .money-symbol { color: #44ff44; margin-right: 4px; } .health-bar { width: 200px; 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: 16px; width: 16px; 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; } .weapon-info { display: flex; align-items: center; justify-content: flex-end; gap: 10px; } .weapon-text { text-align: right; } .weapon-name { font-size: 1em; font-weight: bold; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } .weapon-ammo { font-size: 0.8em; opacity: 0.9; text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); } .weapon-image { background-color: rgba(0, 0, 0, 0.5); border-radius: 3px; width: 50px; height: 50px; object-fit: contain; } .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%); } .purchase-label { background-color: rgba(0, 0, 0, 0.9); padding: 12px 20px; border-radius: 4px; text-align: center; color: #ffffff; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); font-family: 'Arial', sans-serif; text-transform: uppercase; position: relative; 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-cost { font-size: 16px; margin-bottom: 6px; color: #44ff44; } .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.7); } .weapon-roulette { background-color: rgba(0, 0, 0, 0.9); padding: 20px; border-radius: 4px; text-align: center; position: relative; margin: 0 auto; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2); color: #ffffff; font-family: 'Arial', sans-serif; text-transform: uppercase; } .roulette-image { width: 100px; height: 100px; object-fit: contain; background-color: rgba(255, 255, 255, 0.1); border-radius: 3px; padding: 10px; margin-bottom: 10px; } .roulette-name { font-size: 16px; font-weight: bold; margin-bottom: 8px; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); } .roulette-prompt { font-size: 12px; opacity: 0.8; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); } .downed-player { background-color: rgba(0, 0, 0, 0.9); padding: 12px 20px; border-radius: 4px; text-align: center; color: #ffffff; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); font-family: 'Arial', sans-serif; text-transform: uppercase; width: 220px; box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2); position: relative; } .revive-text { font-size: 16px; margin-bottom: 8px; font-weight: bold; color: #ff4444; } .revive-prompt { font-size: 14px; opacity: 0.8; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); margin-bottom: 8px; } .revive-progress { width: 100%; height: 6px; background: rgba(255, 255, 255, 0.2); border-radius: 3px; overflow: hidden; margin-bottom: 8px; } .revive-progress-fill { width: 0%; height: 100%; background: linear-gradient(to right, #44ff44, #88ff88); transition: width 0.3s ease; } .wave-announcement { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); font-family: "Creepster", serif; font-weight: 400; font-style: normal; font-size: 72px; color: #ff0000; text-shadow: 0 0 20px rgba(255, 0, 0, 0.8); opacity: 0; transition: transform 0.5s ease, opacity 0.5s ease; pointer-events: none; z-index: 1000; } .wave-announcement.show { transform: translate(-50%, -50%) scale(1); opacity: 1; animation: pulseAndFade 3s ease-out forwards; } @keyframes pulseAndFade { 0% { transform: translate(-50%, -50%) scale(0); opacity: 0; } 20% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; } 40% { transform: translate(-50%, -50%) scale(1); opacity: 1; } 80% { transform: translate(-50%, -50%) scale(1); opacity: 1; } 100% { transform: translate(-50%, -50%) scale(0.8); opacity: 0; } } </style>