Spaces:
Sleeping
Sleeping
| //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, '"') | |
| .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) => ` | |
| <div class="leaderboard-row"> | |
| <span>${index + 1}. ${escapeHtml(player.name || 'Player')}</span> | |
| <b>${player.score}</b> | |
| </div> | |
| `).join('') | |
| : '<div class="leaderboard-row"><span>Chưa có dữ liệu</span><b>0</b></div>'; | |
| topKillListEl.innerHTML = topKillPlayers.length | |
| ? topKillPlayers.map((player, index) => ` | |
| <div class="leaderboard-row"> | |
| <span>${index + 1}. ${escapeHtml(player.name || 'Player')}</span> | |
| <b>${player.kills || 0}</b> | |
| </div> | |
| `).join('') | |
| : '<div class="leaderboard-row"><span>Chưa có dữ liệu</span><b>0</b></div>'; | |
| } | |
| function updateHud(state, myId) { | |
| const me = state.players.find(p => p.id === myId); | |
| if (!me) { | |
| scoreEl.textContent = '0'; | |
| orbitCountEl.textContent = '0'; | |
| if (killsCountEl) killsCountEl.textContent = '0'; | |
| coordsEl.textContent = '--'; | |
| directionEl.textContent = '--'; | |
| return; | |
| } | |
| scoreEl.textContent = me.score; | |
| orbitCountEl.textContent = me.orbits.length; | |
| if (killsCountEl) killsCountEl.textContent = me.kills || 0; | |
| coordsEl.textContent = `${Math.round(me.x)}, ${Math.round(me.y)}`; | |
| directionEl.textContent = getCardinalDirection(me.angle); | |
| const staminaBarEl = document.getElementById('staminaBar'); | |
| if (staminaBarEl) { | |
| staminaBarEl.style.width = `${Math.round(me.stamina)}%`; | |
| } | |
| if (state.lastHit && state.lastHit.targetId === myId) { | |
| hitMessageEl.textContent = `Bị ${state.lastHit.attackerName} đánh trúng! Điểm: ${me.score} - Vệ tinh: ${me.orbits.length}`; | |
| hitTimer = 120; | |
| } | |
| updateLeaderboard(state); | |
| } | |
| function updateHitMessage() { | |
| if (hitTimer > 0) { | |
| hitTimer -= 1; | |
| } | |
| if (hitTimer <= 0) { | |
| hitMessageEl.textContent = ''; | |
| } | |
| } | |
| function sendInput() { | |
| const myId = socketApi.getMyId(); | |
| const state = socketApi.getState(); | |
| if (!myId || state.players.length === 0) return; | |
| const me = state.players.find(p => p.id === myId); | |
| if (!me) return; | |
| let targetX = me.x; | |
| let targetY = me.y; | |
| if (useKeyboard || useJoystick) { | |
| const speedRange = 180; | |
| targetX += inputVector.x * speedRange; | |
| targetY += inputVector.y * speedRange; | |
| } | |
| targetX = Math.min(Math.max(targetX, 0), MAP_WIDTH); | |
| targetY = Math.min(Math.max(targetY, 0), MAP_HEIGHT); | |
| socketApi.emitInput({ x: targetX, y: targetY, boost: isBoosting }); | |
| } | |
| function drawScene(t) { | |
| const state = socketApi.getState(); | |
| const myId = socketApi.getMyId(); | |
| const me = state.players.find(p => p.id === myId); | |
| let camX = lastCamX; | |
| let camY = lastCamY; | |
| if (me) { | |
| const targetCamX = me.x - canvas.width / 2; | |
| const targetCamY = me.y - canvas.height / 2; | |
| // Smooth camera toward player position | |
| lastCamX += (targetCamX - lastCamX) * LERP_ALPHA; | |
| lastCamY += (targetCamY - lastCamY) * LERP_ALPHA; | |
| camX = lastCamX; | |
| camY = lastCamY; | |
| } else if (!gameStarted) { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.save(); | |
| ctx.fillStyle = '#2b241f'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| drawGrid(ctx, 0, 0); | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = '18px Arial'; | |
| ctx.fillText('Đang kết nối tới server...', 24, 42); | |
| ctx.restore(); | |
| return; | |
| } | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| drawGrid(ctx, camX, camY); | |
| drawMapBounds(ctx, camX, camY); | |
| for (const pickup of state.pickups) { | |
| const bob = Math.sin(t / 400 + pickup.id.charCodeAt(0)) * 4; | |
| if (pickup.type === 'normal') { | |
| drawShuriken(ctx, pickup.x - camX, pickup.y - camY + bob, pickup.radius, t / 1200); | |
| } else { | |
| drawStrongShuriken(ctx, pickup.x - camX, pickup.y - camY + bob, pickup.radius, t / 1200); | |
| } | |
| } | |
| // Interpolate and render all players | |
| // Also clean up entries for players no longer in state | |
| const activeIds = new Set(state.players.map(p => p.id)); | |
| for (const id of renderPositions.keys()) { | |
| if (!activeIds.has(id)) renderPositions.delete(id); | |
| } | |
| for (const player of state.players) { | |
| // Get or initialize smooth display position | |
| if (!renderPositions.has(player.id)) { | |
| renderPositions.set(player.id, { x: player.x, y: player.y }); | |
| } | |
| const rp = renderPositions.get(player.id); | |
| // Lerp display position toward server position | |
| rp.x += (player.x - rp.x) * LERP_ALPHA; | |
| rp.y += (player.y - rp.y) * LERP_ALPHA; | |
| const rx = rp.x - camX; | |
| const ry = rp.y - camY; | |
| drawPlayer(ctx, rx, ry, player.radius, player.name, player.angle, player.skinId, player.isProtected, t); | |
| const total = player.orbits.length; | |
| player.orbits.forEach((orbit, index) => { | |
| const angleOffset = (index / total) * Math.PI * 2; | |
| const angle = angleOffset + state.orbitClock; | |
| const radius = 45 + orbit.ring * 18; | |
| const ox = rp.x + Math.cos(angle) * radius - camX; | |
| const oy = rp.y + Math.sin(angle) * radius - camY; | |
| if (orbit.type === 'strong') { | |
| drawStrongShuriken(ctx, ox, oy, 11, angle * 1.5); | |
| } else { | |
| drawShuriken(ctx, ox, oy, 11, angle * 1.5); | |
| } | |
| }); | |
| } | |
| // Update & Draw Particles | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| const p = particles[i]; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.alpha -= p.decay; | |
| if (p.alpha <= 0) { | |
| particles.splice(i, 1); | |
| continue; | |
| } | |
| ctx.save(); | |
| ctx.globalAlpha = p.alpha; | |
| ctx.shadowBlur = 6; | |
| ctx.shadowColor = p.color; | |
| ctx.fillStyle = p.color; | |
| ctx.beginPath(); | |
| ctx.arc(p.x - camX, p.y - camY, p.radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| // Draw Screen Flash | |
| if (flashOpacity > 0) { | |
| ctx.save(); | |
| ctx.fillStyle = `rgba(255, 0, 0, ${flashOpacity})`; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| flashOpacity = Math.max(0, flashOpacity - 0.02); | |
| ctx.restore(); | |
| } | |
| updateHud(state, myId); | |
| updateHitMessage(); | |
| } | |
| function loop(t) { | |
| drawScene(t); | |
| requestAnimationFrame(loop); | |
| } | |
| export function startGame({ canvas: gameCanvas, ctx: gameCtx, socketApi: api }) { | |
| canvas = gameCanvas; | |
| ctx = gameCtx; | |
| socketApi = api; | |
| gameStarted = false; | |
| particles = []; | |
| flashOpacity = 0; | |
| mouse.x = canvas.width / 2; | |
| mouse.y = canvas.height / 2; | |
| setInterval(() => { | |
| if (gameStarted) sendInput(); | |
| }, 50); | |
| requestAnimationFrame(loop); | |
| } | |
| export function setGameStarted(value) { | |
| gameStarted = value; | |
| } | |
| export function triggerDeathEffect(x, y, skinId) { | |
| flashOpacity = 0.8; | |
| particles = []; | |
| const skinColors = ['#00ffff', '#ff4500', '#e040fb', '#00ff00']; | |
| const color = skinColors[skinId] || '#ffffff'; | |
| for (let i = 0; i < 40; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = 2 + Math.random() * 8; | |
| particles.push({ | |
| x: x, | |
| y: y, | |
| vx: Math.cos(angle) * speed, | |
| vy: Math.sin(angle) * speed, | |
| radius: 2 + Math.random() * 5, | |
| alpha: 1.0, | |
| decay: 0.015 + Math.random() * 0.015, | |
| color: color | |
| }); | |
| } | |
| } | |