//import player1ImgSrc from '../assets/images/player1.png'; //import player2ImgSrc from '../assets/images/player2.png'; //import player3ImgSrc from '../assets/images/player3.png'; //import player4ImgSrc from '../assets/images/player4.png'; const player1ImgSrc = '/assets/images/player1.png'; const player2ImgSrc = '/assets/images/player2.png'; const player3ImgSrc = '/assets/images/player3.png'; const player4ImgSrc = '/assets/images/player4.png'; const scoreEl = document.getElementById('score'); const orbitCountEl = document.getElementById('orbitCount'); const killsCountEl = document.getElementById('killsCount'); const coordsEl = document.getElementById('coords'); const directionEl = document.getElementById('direction'); const hitMessageEl = document.getElementById('hitMessage'); const topScoreListEl = document.getElementById('topScoreList'); const topKillListEl = document.getElementById('topKillList'); const joystickEl = document.getElementById('joystick'); const stickEl = document.getElementById('stick'); const boostBtnEl = document.getElementById('boostBtn'); const hintEl = document.getElementById('hint'); const playerSkins = []; const skinSrcs = [player1ImgSrc, player2ImgSrc, player3ImgSrc, player4ImgSrc]; skinSrcs.forEach((src) => { const img = new Image(); img.onload = () => { img.loaded = true; }; img.src = src; playerSkins.push(img); }); const MAP_WIDTH = 3000; const MAP_HEIGHT = 3000; let canvas; let ctx; let socketApi; let inputVector = { x: 0, y: 0 }; let mouse = { x: 0, y: 0 }; let useKeyboard = false; let useJoystick = false; let gameStarted = false; let isBoosting = false; let hitTimer = 0; let activePointerId = null; let boostPointerId = null; let particles = []; let flashOpacity = 0; let lastCamX = 0; let lastCamY = 0; let isTouchDevice = false; // Client-side interpolation: stores smoothed display positions for each entity const renderPositions = new Map(); // id -> { x, y } const LERP_ALPHA = 0.25; // how quickly display catches up to server position (0-1) const controls = { w: false, a: false, s: false, d: false, ArrowUp: false, ArrowLeft: false, ArrowDown: false, ArrowRight: false, }; function updateInputVectorFromKeyboard() { const x = (controls.d || controls.ArrowRight ? 1 : 0) - (controls.a || controls.ArrowLeft ? 1 : 0); const y = (controls.s || controls.ArrowDown ? 1 : 0) - (controls.w || controls.ArrowUp ? 1 : 0); const length = Math.hypot(x, y); if (length > 0) { inputVector.x = x / length; inputVector.y = y / length; } else { inputVector.x = 0; inputVector.y = 0; } } window.addEventListener('keydown', (e) => { if (e.code === 'Space' || e.code === 'ShiftLeft') { isBoosting = true; e.preventDefault(); return; } const key = e.key.toLowerCase(); if (key in controls) { controls[key] = true; useKeyboard = true; updateInputVectorFromKeyboard(); e.preventDefault(); } }); window.addEventListener('keyup', (e) => { if (e.code === 'Space' || e.code === 'ShiftLeft') { isBoosting = false; e.preventDefault(); return; } const key = e.key.toLowerCase(); if (key in controls) { controls[key] = false; updateInputVectorFromKeyboard(); e.preventDefault(); } }); // ── Touch device detection & mobile controls visibility ── function showMobileControls() { if (joystickEl) joystickEl.style.display = 'flex'; if (boostBtnEl) boostBtnEl.style.display = 'flex'; if (hintEl) hintEl.style.display = 'none'; } function detectTouchDevice() { if (isTouchDevice) return; isTouchDevice = true; showMobileControls(); } // Show on first touch (handles touch laptops / hybrid devices gracefully) window.addEventListener('touchstart', detectTouchDevice, { once: true }); // Also check immediately using pointer media query if (window.matchMedia && window.matchMedia('(hover: none) and (pointer: coarse)').matches) { isTouchDevice = true; // Defer until DOM is ready (called at module init time) requestAnimationFrame(showMobileControls); } function resetStick() { if (!stickEl) return; stickEl.style.transform = 'translate(-50%, -50%)'; } function setJoystickDirection(dx, dy) { const maxRadius = 45; const distance = Math.min(Math.hypot(dx, dy), maxRadius); const angle = Math.atan2(dy, dx); const x = Math.cos(angle) * distance; const y = Math.sin(angle) * distance; if (stickEl) { stickEl.style.transform = `translate(calc(-50% + ${x}px), calc(-50% + ${y}px))`; } const norm = distance > 8 ? 1 : 0; inputVector.x = norm * Math.cos(angle); inputVector.y = norm * Math.sin(angle); } function onJoystickPointerDown(e) { if (!joystickEl) return; useJoystick = true; activePointerId = e.pointerId; joystickEl.setPointerCapture(activePointerId); const rect = joystickEl.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; setJoystickDirection(e.clientX - centerX, e.clientY - centerY); e.preventDefault(); } function onJoystickPointerMove(e) { if (!useJoystick || e.pointerId !== activePointerId) return; const rect = joystickEl.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; setJoystickDirection(e.clientX - centerX, e.clientY - centerY); e.preventDefault(); } function onJoystickPointerUp(e) { if (e.pointerId !== activePointerId) return; useJoystick = false; activePointerId = null; inputVector.x = 0; inputVector.y = 0; resetStick(); e.preventDefault(); } if (joystickEl) { joystickEl.addEventListener('pointerdown', onJoystickPointerDown); joystickEl.addEventListener('pointermove', onJoystickPointerMove); joystickEl.addEventListener('pointerup', onJoystickPointerUp); joystickEl.addEventListener('pointercancel', onJoystickPointerUp); joystickEl.addEventListener('lostpointercapture', onJoystickPointerUp); } // ── Boost button (mobile) ── function onBoostDown(e) { isBoosting = true; boostPointerId = e.pointerId; if (boostBtnEl) { boostBtnEl.classList.add('active'); boostBtnEl.setPointerCapture(boostPointerId); } e.preventDefault(); } function onBoostUp(e) { if (e.pointerId !== boostPointerId) return; isBoosting = false; boostPointerId = null; if (boostBtnEl) boostBtnEl.classList.remove('active'); e.preventDefault(); } if (boostBtnEl) { boostBtnEl.addEventListener('pointerdown', onBoostDown); boostBtnEl.addEventListener('pointerup', onBoostUp); boostBtnEl.addEventListener('pointercancel', onBoostUp); boostBtnEl.addEventListener('lostpointercapture', onBoostUp); } function drawPlayer(ctx, x, y, radius, name, angle, skinId, isProtected = false, t = 0) { ctx.save(); if (isProtected) { ctx.save(); ctx.beginPath(); ctx.arc(x, y, radius + 8 + Math.sin(t / 220) * 2, 0, Math.PI * 2); ctx.strokeStyle = `rgba(68, 220, 255, ${0.45 + Math.sin(t / 240) * 0.15})`; ctx.lineWidth = 2.5; ctx.stroke(); ctx.restore(); } // Draw name above the circle if (name) { ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.fillText(name, x, y - radius - 6); } // Get matching skin image const skin = (skinId >= 0 && skinId < playerSkins.length) ? playerSkins[skinId] : null; if (skin && skin.loaded) { // Draw rotated image ctx.save(); ctx.translate(x, y); // Align standard direction (East) to asset direction (North) ctx.rotate(angle + Math.PI / 2); ctx.drawImage(skin, -radius, -radius, radius * 2, radius * 2); ctx.restore(); } else { // Fallback main circle ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fillStyle = '#f2e6d8'; ctx.fill(); ctx.lineWidth = 3; ctx.strokeStyle = '#6b8f71'; ctx.stroke(); // Fallback first letter inside the circle if (name) { const letter = name.charAt(0).toUpperCase(); ctx.fillStyle = '#2b241f'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(letter, x, y + 1); // small offset for visual alignment } } ctx.restore(); } function drawMoon(ctx, x, y, r, rotation) { ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); const grad = ctx.createRadialGradient(-r * 0.3, -r * 0.3, r * 0.1, 0, 0, r); grad.addColorStop(0, '#a8ffb0'); grad.addColorStop(1, '#1f9e3d'); ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.fillStyle = grad; ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = '#0e5c22'; ctx.stroke(); ctx.restore(); } function drawStrongShuriken(ctx, x, y, r, rotation) { ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); ctx.shadowBlur = 10; ctx.shadowColor = '#ff0000'; // Red glow ctx.beginPath(); for (let i = 0; i < 4; i++) { const angle = (i * Math.PI) / 2; const midAngle = angle + Math.PI / 4; const px = Math.cos(angle) * r; const py = Math.sin(angle) * r; const mx = Math.cos(midAngle) * (r * 0.35); const my = Math.sin(midAngle) * (r * 0.35); if (i === 0) { ctx.moveTo(px, py); } else { ctx.lineTo(px, py); } ctx.lineTo(mx, my); } ctx.closePath(); ctx.fillStyle = '#ff0000'; ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = '#ff6666'; ctx.stroke(); ctx.beginPath(); ctx.arc(0, 0, r * 0.25, 0, Math.PI * 2); ctx.fillStyle = '#2b241f'; ctx.shadowBlur = 0; ctx.fill(); ctx.lineWidth = 1; ctx.strokeStyle = '#ff6666'; ctx.stroke(); ctx.restore(); } function drawShuriken(ctx, x, y, r, rotation) { ctx.save(); ctx.translate(x, y); ctx.rotate(rotation); // Set neon glow effect ctx.shadowBlur = 8; ctx.shadowColor = '#00f0ff'; // Neon Cyan // Draw 4-pointed star ctx.beginPath(); for (let i = 0; i < 4; i++) { const angle = (i * Math.PI) / 2; const midAngle = angle + Math.PI / 4; const px = Math.cos(angle) * r; const py = Math.sin(angle) * r; const mx = Math.cos(midAngle) * (r * 0.35); const my = Math.sin(midAngle) * (r * 0.35); if (i === 0) { ctx.moveTo(px, py); } else { ctx.lineTo(px, py); } ctx.lineTo(mx, my); } ctx.closePath(); // Gradient fill for metallic look const grad = ctx.createLinearGradient(-r, -r, r, r); grad.addColorStop(0, '#e0f7fa'); grad.addColorStop(0.5, '#00b8d4'); grad.addColorStop(1, '#006064'); ctx.fillStyle = grad; ctx.fill(); // Sharp edge stroke ctx.lineWidth = 1.5; ctx.strokeStyle = '#00ffff'; ctx.stroke(); // Draw center hole ctx.beginPath(); ctx.arc(0, 0, r * 0.25, 0, Math.PI * 2); ctx.fillStyle = '#2b241f'; // Match map background color ctx.shadowBlur = 0; // No glow in the hole ctx.fill(); ctx.lineWidth = 1; ctx.strokeStyle = '#00b8d4'; ctx.stroke(); ctx.restore(); } function drawGrid(ctx, camX, camY) { ctx.strokeStyle = 'rgba(255,255,255,0.03)'; ctx.lineWidth = 1; const grid = 60; const offsetX = camX % grid; const offsetY = camY % grid; for (let x = -offsetX; x < canvas.width; x += grid) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); } for (let y = -offsetY; y < canvas.height; y += grid) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } } function drawMapBounds(ctx, camX, camY) { ctx.save(); ctx.strokeStyle = 'rgba(255, 150, 0, 0.8)'; ctx.lineWidth = 4; ctx.setLineDash([16, 12]); ctx.strokeRect(-camX, -camY, MAP_WIDTH, MAP_HEIGHT); ctx.restore(); } function getCardinalDirection(angle) { const deg = ((angle * 180) / Math.PI + 360) % 360; if (deg >= 45 && deg < 135) return 'S'; if (deg >= 135 && deg < 225) return 'W'; if (deg >= 225 && deg < 315) return 'N'; return 'E'; } function escapeHtml(value) { return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function updateLeaderboard(state) { if (!topScoreListEl || !topKillListEl) return; const players = [...state.players].sort((a, b) => b.score - a.score); const topScorePlayers = players.slice(0, 3); const topKillPlayers = [...players].sort((a, b) => (b.kills || 0) - (a.kills || 0)).slice(0, 3); topScoreListEl.innerHTML = topScorePlayers.length ? topScorePlayers.map((player, index) => `