Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> | |
| <title>Knife Hit Pro</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;700;900&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #ff4757; | |
| --bg-dark: #0f172a; | |
| --surface: rgba(255, 255, 255, 0.05); | |
| --border: rgba(255, 255, 255, 0.1); | |
| } | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background-color: var(--bg-dark); | |
| font-family: 'Outfit', sans-serif; | |
| touch-action: none; | |
| user-select: none; | |
| color: white; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| background: radial-gradient(circle at 50% 30%, #1e293b 0%, #0f172a 100%); | |
| } | |
| canvas { display: block; } | |
| .glass { | |
| background: var(--surface); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid var(--border); | |
| border-radius: 24px; | |
| } | |
| .screen { | |
| position: absolute; | |
| inset: 0; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| background: rgba(15, 23, 42, 0.95); | |
| } | |
| .btn-main { | |
| background: var(--primary); | |
| color: white; | |
| padding: 16px 48px; | |
| border-radius: 16px; | |
| font-weight: 900; | |
| font-size: 1.25rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| box-shadow: 0 8px 0 #b33939; | |
| transition: all 0.1s; | |
| margin: 10px; | |
| width: 240px; | |
| border: none; | |
| cursor: pointer; | |
| } | |
| .btn-main:active { transform: translateY(4px); box-shadow: 0 4px 0 #b33939; } | |
| .diff-tag { | |
| padding: 8px 20px; | |
| border-radius: 12px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| border: 2px solid transparent; | |
| background: rgba(255,255,255,0.05); | |
| } | |
| .diff-tag.active { border-color: var(--primary); background: rgba(255, 71, 87, 0.2); } | |
| .hidden { display: none ; } | |
| @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } | |
| .logo-anim { animation: pulse 2s infinite ease-in-out; } | |
| #pause-btn { | |
| position: absolute; | |
| top: 24px; | |
| right: 24px; | |
| z-index: 60; | |
| cursor: pointer; | |
| padding: 12px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <div id="hud" class="absolute top-0 left-0 w-full p-6 flex justify-between items-start z-50 hidden"> | |
| <div class="flex gap-4"> | |
| <div class="glass p-3 px-5"> | |
| <p class="text-[10px] uppercase font-bold opacity-50">Score</p> | |
| <p id="score-val" class="text-3xl font-black">0</p> | |
| </div> | |
| <div class="glass p-3 px-5 flex items-center gap-2"> | |
| <span class="text-xl">🍎</span> | |
| <p id="fruit-val" class="text-2xl font-black text-red-400">0</p> | |
| </div> | |
| </div> | |
| <div id="pause-btn" class="glass"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg> | |
| </div> | |
| <div class="glass p-3 px-5 text-right mr-16"> | |
| <p class="text-[10px] uppercase font-bold opacity-50">Level</p> | |
| <p id="level-val" class="text-xl font-black">1</p> | |
| </div> | |
| </div> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- Main Menu --> | |
| <div id="home-screen" class="screen"> | |
| <div class="logo-anim mb-4"> | |
| <svg width="80" height="80" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#451a03" stroke="#78350f" stroke-width="4"/><path d="M50 20 L50 80 M20 50 L80 50" stroke="rgba(255,255,255,0.1)" stroke-width="2"/></svg> | |
| </div> | |
| <h1 class="text-6xl font-black italic mb-2">KNIFE<span class="text-red-500">HIT</span></h1> | |
| <div class="flex gap-2 my-4 glass p-2"> | |
| <div class="diff-tag active" data-diff="easy">EASY</div> | |
| <div class="diff-tag" data-diff="medium">MED</div> | |
| <div class="diff-tag" data-diff="hard">HARD</div> | |
| </div> | |
| <button id="play-btn" class="btn-main">START GAME</button> | |
| <button id="resume-saved-btn" class="btn-main hidden" style="opacity: 0.8; font-size: 0.9rem;">RESUME SAVED</button> | |
| </div> | |
| <!-- Pause Menu --> | |
| <div id="pause-screen" class="screen hidden"> | |
| <h2 class="text-5xl font-black mb-8">PAUSED</h2> | |
| <button id="resume-btn" class="btn-main">RESUME</button> | |
| <button id="save-exit-btn" class="btn-main" style="background: #475569; box-shadow: 0 8px 0 #1e293b;">SAVE & EXIT</button> | |
| </div> | |
| <!-- Game Over --> | |
| <div id="game-over-screen" class="screen hidden"> | |
| <div class="glass p-10 flex flex-col items-center"> | |
| <h2 class="text-4xl font-black mb-2 text-red-500">CRASHED!</h2> | |
| <p id="final-score" class="text-xl opacity-60 mb-8">Score: 0</p> | |
| <button id="restart-btn" class="btn-main">RETRY</button> | |
| <button onclick="location.reload()" class="btn-main opacity-50 text-sm">MENU</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const SVG_KNIFE = `data:image/svg+xml;base64,${btoa('<svg xmlns="http://www.w3.org/2000/svg" width="40" height="120" viewBox="0 0 40 120"><path d="M20 5 L32 90 L8 90 Z" fill="#e2e8f0"/><path d="M15 90 H25 V115 Q25 120 20 120 Q15 120 15 115 Z" fill="#ff4757"/><rect x="15" y="95" width="10" height="4" fill="#b33939"/></svg>')}`; | |
| const KNIFE_TIP_OFFSET = 5; | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const knifeImg = new Image(); knifeImg.src = SVG_KNIFE; | |
| const DIFFICULTY = { | |
| easy: { speed: 0.03, count: 7, fruitProb: 0.4 }, | |
| medium: { speed: 0.05, count: 10, fruitProb: 0.6 }, | |
| hard: { speed: 0.07, count: 13, fruitProb: 0.8 } | |
| }; | |
| let state = { | |
| isPlaying: false, | |
| isPaused: false, | |
| score: 0, | |
| fruits: 0, | |
| level: 1, | |
| difficulty: 'easy', | |
| rotation: 0, | |
| rotationSpeed: 0.03, | |
| knivesInLog: [], | |
| fruitsInLog: [], | |
| activeKnife: { x: 0, y: 0, status: 'idle' }, | |
| remKnives: 0, | |
| shake: 0, | |
| particles: [] | |
| }; | |
| function init() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| state.activeKnife.x = canvas.width / 2; | |
| state.activeKnife.y = canvas.height - 160; | |
| // Check for saved data | |
| const saved = localStorage.getItem('knifeHitSave'); | |
| if (saved) document.getElementById('resume-saved-btn').classList.remove('hidden'); | |
| } | |
| function createSplash(x, y) { | |
| for(let i=0; i<12; i++) { | |
| state.particles.push({ | |
| x, y, | |
| vx: (Math.random() - 0.5) * 12, | |
| vy: (Math.random() - 0.5) * 12, | |
| life: 1.0 | |
| }); | |
| } | |
| } | |
| function draw() { | |
| if (!state.isPlaying || state.isPaused) return; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| const cx = canvas.width / 2; | |
| const cy = canvas.height * 0.35; | |
| const r = 90; | |
| // Particles | |
| state.particles.forEach((p, idx) => { | |
| ctx.globalAlpha = p.life; | |
| ctx.fillStyle = '#ef4444'; | |
| ctx.beginPath(); ctx.arc(p.x, p.y, 4, 0, Math.PI*2); ctx.fill(); | |
| p.x += p.vx; p.y += p.vy; p.life -= 0.04; | |
| if(p.life <= 0) state.particles.splice(idx, 1); | |
| }); | |
| ctx.globalAlpha = 1; | |
| // Inventory | |
| for(let i=0; i < state.remKnives; i++) { | |
| ctx.drawImage(knifeImg, 40, (canvas.height - 100) - (i * 15), 15, 45); | |
| } | |
| ctx.save(); | |
| ctx.translate(cx + (Math.random()*state.shake), cy + (Math.random()*state.shake)); | |
| ctx.rotate(state.rotation); | |
| ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI*2); | |
| ctx.fillStyle = '#451a03'; ctx.fill(); | |
| ctx.strokeStyle = '#78350f'; ctx.lineWidth = 4; ctx.stroke(); | |
| state.fruitsInLog.forEach(f => { | |
| if(!f.active) return; | |
| ctx.save(); | |
| ctx.rotate(f.angle); | |
| ctx.font = "32px serif"; | |
| ctx.textAlign = "center"; | |
| ctx.fillText("🍎", 0, r + 25); | |
| ctx.restore(); | |
| }); | |
| state.knivesInLog.forEach(k => { | |
| ctx.save(); | |
| ctx.rotate(k.angle); | |
| ctx.drawImage(knifeImg, -15, r - 5, 30, 90); | |
| ctx.restore(); | |
| }); | |
| ctx.restore(); | |
| if (state.activeKnife.status !== 'empty') { | |
| ctx.drawImage(knifeImg, cx - 20, state.activeKnife.y, 40, 120); | |
| } | |
| if (state.activeKnife.status === 'flying') { | |
| state.activeKnife.y -= 70; | |
| const knifeTipY = state.activeKnife.y + KNIFE_TIP_OFFSET; | |
| if (knifeTipY <= cy + r) { | |
| handleImpact(cx, cy, r); | |
| } | |
| } | |
| state.rotation += state.rotationSpeed; | |
| if (state.shake > 0) state.shake *= 0.8; | |
| requestAnimationFrame(draw); | |
| } | |
| function handleImpact(cx, cy, r) { | |
| // APPLYING YOUR NAILED IT FIX: 0.1 | |
| const impactAngle = (Math.PI / 0.1) - state.rotation; | |
| state.fruitsInLog.forEach(f => { | |
| if(f.active && Math.abs(AngleDiff(f.angle, impactAngle)) < 0.28) { | |
| f.active = false; | |
| state.fruits++; | |
| document.getElementById('fruit-val').textContent = state.fruits; | |
| createSplash(cx, cy + r); | |
| } | |
| }); | |
| let crashed = false; | |
| for (let k of state.knivesInLog) { | |
| if (Math.abs(AngleDiff(k.angle, impactAngle)) < 0.28) { crashed = true; break; } | |
| } | |
| if (crashed) { | |
| state.isPlaying = false; | |
| document.getElementById('final-score').textContent = `Score: ${state.score}`; | |
| document.getElementById('game-over-screen').classList.remove('hidden'); | |
| localStorage.removeItem('knifeHitSave'); // Delete save on death | |
| } else { | |
| state.knivesInLog.push({ angle: impactAngle }); | |
| state.score++; | |
| state.remKnives--; | |
| state.shake = 15; | |
| document.getElementById('score-val').textContent = state.score; | |
| if (state.remKnives <= 0) { | |
| state.activeKnife.status = 'empty'; | |
| setTimeout(nextLevel, 400); | |
| } else { | |
| state.activeKnife.y = canvas.height - 160; | |
| state.activeKnife.status = 'idle'; | |
| } | |
| } | |
| } | |
| function AngleDiff(a1, a2) { | |
| let diff = (a1 - a2 + Math.PI) % (Math.PI * 2) - Math.PI; | |
| return diff < -Math.PI ? diff + Math.PI * 2 : diff; | |
| } | |
| function nextLevel() { | |
| if (!state.isPlaying) return; | |
| state.level++; | |
| document.getElementById('level-val').textContent = state.level; | |
| state.knivesInLog = []; | |
| state.fruitsInLog = []; | |
| if(Math.random() < DIFFICULTY[state.difficulty].fruitProb) { | |
| state.fruitsInLog.push({ angle: Math.random()*Math.PI*2, active: true }); | |
| } | |
| state.remKnives = DIFFICULTY[state.difficulty].count + Math.floor(state.level/2); | |
| state.rotationSpeed = DIFFICULTY[state.difficulty].speed + (state.level * 0.002); | |
| if(state.level > 2 && Math.random() > 0.4) state.rotationSpeed *= -1; | |
| state.activeKnife.y = canvas.height - 160; | |
| state.activeKnife.status = 'idle'; | |
| } | |
| function togglePause() { | |
| state.isPaused = !state.isPaused; | |
| if (state.isPaused) { | |
| document.getElementById('pause-screen').classList.remove('hidden'); | |
| } else { | |
| document.getElementById('pause-screen').classList.add('hidden'); | |
| requestAnimationFrame(draw); | |
| } | |
| } | |
| function saveAndExit() { | |
| const saveData = { | |
| score: state.score, | |
| level: state.level, | |
| fruits: state.fruits, | |
| difficulty: state.difficulty | |
| }; | |
| localStorage.setItem('knifeHitSave', JSON.stringify(saveData)); | |
| location.reload(); | |
| } | |
| function startGame(resumeData = null) { | |
| if (resumeData) { | |
| state.score = resumeData.score; | |
| state.level = resumeData.level; | |
| state.fruits = resumeData.fruits; | |
| state.difficulty = resumeData.difficulty; | |
| } else { | |
| state.score = 0; state.level = 1; state.fruits = 0; | |
| } | |
| state.knivesInLog = []; | |
| state.fruitsInLog = []; | |
| state.remKnives = DIFFICULTY[state.difficulty].count + Math.floor(state.level/2); | |
| state.rotationSpeed = DIFFICULTY[state.difficulty].speed + (state.level * 0.002); | |
| state.isPlaying = true; | |
| state.isPaused = false; | |
| state.activeKnife.status = 'idle'; | |
| state.activeKnife.y = canvas.height - 160; | |
| document.getElementById('score-val').textContent = state.score; | |
| document.getElementById('fruit-val').textContent = state.fruits; | |
| document.getElementById('level-val').textContent = state.level; | |
| document.getElementById('home-screen').classList.add('hidden'); | |
| document.getElementById('pause-screen').classList.add('hidden'); | |
| document.getElementById('game-over-screen').classList.add('hidden'); | |
| document.getElementById('hud').classList.remove('hidden'); | |
| requestAnimationFrame(draw); | |
| } | |
| // UI Events | |
| window.addEventListener('pointerdown', (e) => { | |
| if (e.target.closest('button') || e.target.closest('.diff-tag') || e.target.closest('#pause-btn')) return; | |
| if (state.isPlaying && !state.isPaused && state.activeKnife.status === 'idle') { | |
| state.activeKnife.status = 'flying'; | |
| } | |
| }); | |
| document.getElementById('pause-btn').onclick = togglePause; | |
| document.getElementById('resume-btn').onclick = togglePause; | |
| document.getElementById('save-exit-btn').onclick = saveAndExit; | |
| document.getElementById('play-btn').onclick = () => startGame(); | |
| document.getElementById('restart-btn').onclick = () => startGame(); | |
| document.getElementById('resume-saved-btn').onclick = () => { | |
| const saved = JSON.parse(localStorage.getItem('knifeHitSave')); | |
| startGame(saved); | |
| }; | |
| document.querySelectorAll('.diff-tag').forEach(tag => { | |
| tag.onclick = () => { | |
| document.querySelectorAll('.diff-tag').forEach(t => t.classList.remove('active')); | |
| tag.classList.add('active'); | |
| state.difficulty = tag.dataset.diff; | |
| }; | |
| }); | |
| window.onresize = init; | |
| init(); | |
| </script> | |
| </body> | |
| </html> |