Spaces:
Running
Running
before the app asked to access microphone and now not please fix - Follow Up Deployment
cbc9ad8 verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Operation Game Companion</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| success: '#10b981', | |
| warning: '#fcd34d', | |
| danger: '#ef4444', | |
| primary: '#3b82f6', | |
| secondary: '#8b5cf6', | |
| dark: '#111827' | |
| }, | |
| animation: { | |
| 'pulse-fast': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite', | |
| 'buzz': 'buzz 0.1s linear infinite', | |
| 'heartbeat': 'heartbeat 1.5s ease infinite' | |
| }, | |
| keyframes: { | |
| buzz: { | |
| '0%, 100%': { transform: 'translateX(-2px) rotate(-1deg)' }, | |
| '50%': { transform: 'translateX(2px) rotate(1deg)' } | |
| }, | |
| heartbeat: { | |
| '0%, 100%': { transform: 'scale(1)' }, | |
| '50%': { transform: 'scale(1.1)' } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| /* Custom styles */ | |
| body { | |
| background-image: linear-gradient(135deg, #1e3a8a 0%, #0f172a 100%); | |
| min-height: 100vh; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| .game-container { | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); | |
| } | |
| .patient-torso { | |
| background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 350"><rect x="60" y="0" width="80" height="350" rx="10" fill="%23f3e8ff"/></svg>'); | |
| background-position: center; | |
| background-repeat: no-repeat; | |
| background-size: contain; | |
| } | |
| .body-part { | |
| transition: all 0.3s ease; | |
| cursor: pointer; | |
| border: 2px dashed rgba(255, 255, 255, 0.4); | |
| } | |
| .body-part:hover { | |
| transform: scale(1.1); | |
| } | |
| .removed-part { | |
| filter: grayscale(1); | |
| opacity: 0.7; | |
| transform: scale(0.8); | |
| } | |
| .life-bar { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .life-gradient { | |
| background: linear-gradient(to right, #ef4444 0%, #fcd34d 50%, #10b981 100%); | |
| } | |
| .negative-section { | |
| background: #4b5563; | |
| } | |
| @media (max-width: 768px) { | |
| .player-grid { | |
| grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="text-gray-200 py-8 px-4"> | |
| <div class="max-w-6xl mx-auto"> | |
| <!-- Header --> | |
| <header class="text-center mb-10"> | |
| <h1 class="text-4xl md:text-5xl font-bold text-white mb-4"> | |
| <i class="fas fa-heartbeat text-red-500 mr-3"></i> | |
| Operation Game Companion | |
| </h1> | |
| <p class="text-primary-200 max-w-xl mx-auto"> | |
| Extract all body parts without killing the patient! Connect your microphone to detect the buzzer sound. | |
| </p> | |
| </header> | |
| <!-- Main Game Area --> | |
| <div class="game-container overflow-hidden p-6 mb-8"> | |
| <!-- Patient Status --> | |
| <div class="flex flex-col md:flex-row justify-between items-center mb-10 gap-6"> | |
| <!-- Life Bar --> | |
| <div class="w-full md:w-3/5"> | |
| <p class="text-md font-bold mb-2 flex justify-between items-center"> | |
| <span class="flex items-center"><i class="fas fa-heart mr-2"></i> PATIENT LIFE</span> | |
| <span id="life-value">100%</span> | |
| </p> | |
| <div class="life-bar h-8 bg-gray-700 rounded-full overflow-hidden relative"> | |
| <div id="life-fill" class="life-gradient h-full w-full"> | |
| <div class="negative-section absolute top-0 right-0 h-full transition-all duration-300" id="negative-life"></div> | |
| </div> | |
| <div class="absolute inset-0 flex items-center justify-center font-bold" id="life-text">Stable Condition</div> | |
| </div> | |
| </div> | |
| <!-- Buzzer Status --> | |
| <div class="flex items-center space-x-4 bg-gray-800 p-4 rounded-xl"> | |
| <div id="buzzer-status" class="h-3 w-3 rounded-full bg-gray-500 mr-2"></div> | |
| <div> | |
| <p class="text-sm">MICROPHONE</p> | |
| <p id="buzz-count" class="font-bold text-lg"><i class="fas fa-bolt mr-2"></i> 0 Buzzes</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Player Status --> | |
| <div id="player-status" class="player-grid grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"> | |
| <!-- Player cards will be populated here --> | |
| </div> | |
| <!-- Game Controls --> | |
| <div class="flex justify-center gap-4 mt-8"> | |
| <button id="add-player" class="bg-primary-600 hover:bg-primary-700 text-white py-2 px-6 rounded-xl flex items-center"> | |
| <i class="fas fa-user-plus mr-2"></i> Add Player | |
| </button> | |
| <button id="calibrate" class="bg-secondary-600 hover:bg-secondary-700 text-white py-2 px-6 rounded-xl flex items-center"> | |
| <i class="fas fa-sliders-h mr-2"></i> Calibrate | |
| </button> | |
| <button id="buzz-test" class="bg-warning-600 hover:bg-warning-700 text-white py-2 px-6 rounded-xl flex items-center"> | |
| <i class="fas fa-bolt mr-2"></i> Test Buzz | |
| </button> | |
| <button id="new-game" class="bg-success-600 hover:bg-success-700 text-white py-2 px-6 rounded-xl flex items-center"> | |
| <i class="fas fa-play mr-2"></i> New Game | |
| </button> | |
| </div> | |
| <!-- Current Player Turn --> | |
| <div id="current-player" class="inline-block bg-secondary-700 py-2 px-6 rounded-full font-bold text-lg text-center mt-6"> | |
| Add players to start | |
| </div> | |
| </div> | |
| <!-- Instructions --> | |
| <div class="bg-gray-800 rounded-xl p-6 mb-8"> | |
| <h3 class="text-xl font-bold mb-4 text-center">How to Play</h3> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div class="flex items-start"> | |
| <i class="fas fa-user-plus text-2xl text-primary-500 mt-1 mr-3"></i> | |
| <div> | |
| <h4 class="font-bold mb-1">Add Players</h4> | |
| <p>Add players using the "Add Player" button. Players take turns removing body parts.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <i class="fas fa-heart text-2xl text-danger-500 mt-1 mr-3"></i> | |
| <div> | |
| <h4 class="font-bold mb-1">Monitor Patient</h4> | |
| <p>The life bar decreases with each buzz. If it reaches zero or below, the patient dies.</p> | |
| </div> | |
| </div> | |
| <div class="flex items-start"> | |
| <i class="fas fa-trophy text-2xl text-warning-500 mt-1 mr-3"></i> | |
| <div> | |
| <h4 class="font-bold mb-1">Win Conditions</h4> | |
| <p>Player with most parts removed wins if patient survives. With a dead patient, player with least buzzes wins.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Game Over Modal --> | |
| <div id="game-over-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-50 hidden transition-opacity duration-300"> | |
| <div class="bg-gray-800 rounded-2xl max-w-md w-full p-8 text-center relative"> | |
| <button class="absolute top-4 right-4 text-gray-400 hover:text-white"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| <div class="mb-6"> | |
| <i class="fas fa-skull-crossbones text-7xl mb-4"></i> | |
| <h2 id="game-outcome" class="text-3xl font-bold mb-2">PATIENT DECEASED</h2> | |
| <p id="game-summary">The patient could not survive the surgery.</p> | |
| </div> | |
| <div class="bg-gray-700 p-4 rounded-xl mb-6"> | |
| <h3 class="font-bold mb-2 text-lg">FINAL RESULTS</h3> | |
| <div id="final-scores" class="space-y-3"> | |
| <!-- Scores will be populated here --> | |
| </div> | |
| </div> | |
| <button id="play-again" class="bg-slate-600 hover:bg-primary-600 text-white py-3 px-8 rounded-xl font-bold"> | |
| Play Again <i class="fas fa-redo ml-2"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Add Player Modal --> | |
| <div id="player-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-50 hidden"> | |
| <div class="bg-gray-800 rounded-2xl max-w-md w-full p-8"> | |
| <h2 class="text-2xl font-bold mb-6 text-center">Add Player</h2> | |
| <div class="mb-6"> | |
| <label class="block mb-2 font-medium">Player Name</label> | |
| <input type="text" id="player-name" class="w-full bg-gray-700 text-white rounded-lg py-3 px-4" placeholder="Enter player name" value="Player 1" maxlength="20"> | |
| </div> | |
| <div class="mb-6"> | |
| <label class="block mb-2 font-medium">Player Icon</label> | |
| <div id="icon-picker" class="grid grid-cols-6 gap-3"> | |
| <!-- Icons will be populated via JS --> | |
| </div> | |
| </div> | |
| <div class="flex gap-3"> | |
| <button id="cancel-player" class="flex-1 bg-gray-700 hover:bg-gray-600 py-3 rounded-xl font-medium">Cancel</button> | |
| <button id="save-player" class="flex-1 bg-primary-600 hover:bg-primary-700 py-3 rounded-xl font-medium">Add Player</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Calibration Modal --> | |
| <div id="calibration-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center p-4 z-50 hidden"> | |
| <div class="bg-gray-800 rounded-2xl max-w-md w-full p-8"> | |
| <h2 class="text-2xl font-bold mb-6 text-center">Calibrate Buzzer</h2> | |
| <div class="mb-6"> | |
| <p class="mb-4">Press the buzzer from your Operation game 3-5 times to calibrate. The detector will learn the frequency pattern of your specific buzzer.</p> | |
| <div class="bg-gray-700 rounded-lg p-4 mb-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span>Buzz Detections:</span> | |
| <span id="calibration-count">0</span> | |
| </div> | |
| <div class="h-4 bg-gray-600 rounded-full overflow-hidden"> | |
| <div id="calibration-progress" class="h-full bg-success-600" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="text-center"> | |
| <div id="calibration-status" class="inline-block px-3 py-1 rounded-full text-sm text-gray-200 bg-gray-700 mb-4"> | |
| Ready to calibrate | |
| </div> | |
| <div id="sound-level" class="w-full h-2 bg-gray-700 rounded-full overflow-hidden mb-2"> | |
| <div class="h-full bg-primary-600" style="width: 0%"></div> | |
| </div> | |
| <div class="text-xs text-gray-400">Current sound level</div> | |
| </div> | |
| </div> | |
| <div class="flex gap-3"> | |
| <button id="cancel-calibration" class="flex-1 bg-gray-700 hover:bg-gray-600 py-3 rounded-xl font-medium">Cancel</button> | |
| <button id="finish-calibration" class="flex-1 bg-primary-600 hover:bg-primary-700 py-3 rounded-xl font-medium">Finish</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game state variables | |
| let gameState = { | |
| players: [], | |
| currentPlayer: 0, | |
| patientLife: 100, | |
| buzzCount: 0, | |
| extractedParts: [], | |
| gameActive: false, | |
| audioContext: null, | |
| analyser: null, | |
| microphone: null | |
| }; | |
| // Body parts and their initial coordinates | |
| const bodyParts = [ | |
| { id: "wishbone", name: "Wishbone", icon: "fas fa-bone", color: "bg-gray-200", x: 0, y: 0 }, | |
| { id: "heart", name: "Heart", icon: "fas fa-heart", color: "bg-red-500", x: 0, y: -10 }, | |
| { id: "spare-ribs", name: "Spare Ribs", icon: "fas fa-utensil-spoon", color: "bg-white", x: -35, y: 5 }, | |
| { id: "funny-bone", name: "Funny Bone", icon: "fas fa-laugh", color: "bg-white", x: 30, y: 10 }, | |
| { id: "bread-basket", name: "Bread Basket", icon: "fas fa-bread-slice", color: "bg-yellow-200", x: 0, y: 25 }, | |
| { id: "water-on-the-knee", name: "Water on the Knee", icon: "fas fa-tint", color: "bg-blue-300", x: 0, y: 65 }, | |
| { id: "butterflies", name: "Butterflies", icon: "fas fa-bug", color: "bg-yellow-100", x: 0, y: -25 }, | |
| { id: "ankle", name: "Ankle Bone", icon: "fas fa-bone", color: "bg-gray-200", x: 0, y: 85 } | |
| ]; | |
| // Player icons | |
| const playerIcons = [ | |
| 'fa-user', 'fa-user-md', 'fa-user-nurse', 'fa-user-graduate', | |
| 'fa-user-astronaut', 'fa-user-tie', 'fa-user-injured', 'fa-user-cowboy' | |
| ]; | |
| // DOM elements | |
| const lifeValueEl = document.getElementById('life-value'); | |
| const lifeFillEl = document.getElementById('life-fill'); | |
| const negativeLifeEl = document.getElementById('negative-life'); | |
| const lifeTextEl = document.getElementById('life-text'); | |
| const buzzCountEl = document.getElementById('buzz-count'); | |
| const buzzerStatusEl = document.getElementById('buzzer-status'); | |
| const currentPlayerEl = document.getElementById('current-player'); | |
| const playerStatusEl = document.getElementById('player-status'); | |
| const bodyPartsEl = document.getElementById('body-parts'); | |
| const gameOverModal = document.getElementById('game-over-modal'); | |
| const gameOutcomeEl = document.getElementById('game-outcome'); | |
| const gameSummaryEl = document.getElementById('game-summary'); | |
| const finalScoresEl = document.getElementById('final-scores'); | |
| const playerModal = document.getElementById('player-modal'); | |
| const iconPickerEl = document.getElementById('icon-picker'); | |
| // Buttons | |
| document.getElementById('add-player').addEventListener('click', openAddPlayerModal); | |
| document.getElementById('calibrate').addEventListener('click', startCalibration); | |
| document.getElementById('buzz-test').addEventListener('click', simulateBuzz); | |
| document.getElementById('new-game').addEventListener('click', startNewGame); | |
| document.getElementById('play-again').addEventListener('click', startNewGame); | |
| document.getElementById('cancel-player').addEventListener('click', closeAddPlayerModal); | |
| document.getElementById('save-player').addEventListener('click', addNewPlayer); | |
| document.getElementById('cancel-calibration').addEventListener('click', cancelCalibration); | |
| document.getElementById('finish-calibration').addEventListener('click', finishCalibration); | |
| document.querySelector('#game-over-modal button').addEventListener('click', () => gameOverModal.classList.add('hidden')); | |
| // Audio detection settings | |
| let audioSettings = { | |
| minFrequency: 1000, // Default minimum frequency | |
| maxFrequency: 3000, // Default maximum frequency | |
| threshold: 0.7, // Default volume threshold | |
| calibrated: false, | |
| calibrationSamples: [] | |
| }; | |
| // Microphone stream | |
| let microphoneStream = null; | |
| // Initialize the game | |
| function initGame() { | |
| renderBodyParts(); | |
| updatePlayerListDisplay(); | |
| } | |
| // Set up audio listening | |
| async function setupMicrophone() { | |
| try { | |
| if (!gameState.audioContext) { | |
| gameState.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| gameState.analyser = gameState.audioContext.createAnalyser(); | |
| gameState.analyser.fftSize = 2048; | |
| } | |
| if (microphoneStream) { | |
| microphoneStream.getTracks().forEach(track => track.stop()); | |
| } | |
| microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: { | |
| echoCancellation: false, | |
| noiseSuppression: false, | |
| autoGainControl: false | |
| }}); | |
| gameState.microphone = gameState.audioContext.createMediaStreamSource(microphoneStream); | |
| gameState.microphone.connect(gameState.analyser); | |
| buzzerStatusEl.classList.remove('bg-gray-500'); | |
| buzzerStatusEl.classList.add('bg-green-500'); | |
| if (!audioSettings.calibrated) { | |
| startCalibration(); | |
| } else { | |
| startAudioDetection(); | |
| } | |
| } catch (err) { | |
| buzzerStatusEl.classList.remove('bg-gray-500'); | |
| buzzerStatusEl.classList.add('bg-red-500'); | |
| // Add alert message | |
| const alert = document.createElement('div'); | |
| alert.className = 'bg-red-500 text-white p-3 rounded-lg mb-4 text-center'; | |
| alert.innerHTML = ` | |
| <p><strong>Microphone Access Denied</strong></p> | |
| <p class="text-sm">Please allow microphone access in your browser settings to detect buzzes.</p> | |
| `; | |
| // Insert alert after the player status | |
| const playerStatus = document.getElementById('player-status'); | |
| playerStatus.parentNode.insertBefore(alert, playerStatus.nextSibling); | |
| } | |
| } | |
| // Render body parts on the patient | |
| function renderBodyParts() { | |
| bodyPartsEl.innerHTML = ''; | |
| bodyParts.forEach(part => { | |
| // Skip if already removed | |
| if (gameState.extractedParts.includes(part.id)) return; | |
| const partEl = document.createElement('div'); | |
| partEl.className = `body-part transform ${part.color} rounded-lg flex items-center justify-center w-16 h-16 mx-auto`; | |
| partEl.innerHTML = ` | |
| <i class="${part.icon} text-2xl"></i> | |
| `; | |
| partEl.style.transform = `translate(${part.x}px, ${part.y}px)`; | |
| partEl.dataset.id = part.id; | |
| partEl.addEventListener('click', e => { | |
| if (!gameState.gameActive) return; | |
| handlePartRemoval(e.target.closest('.body-part').dataset.id); | |
| }); | |
| bodyPartsEl.appendChild(partEl); | |
| }); | |
| } | |
| // Update player list display | |
| function updatePlayerListDisplay() { | |
| playerStatusEl.innerHTML = ''; | |
| gameState.players.forEach((player, index) => { | |
| const isCurrent = index === gameState.currentPlayer && gameState.gameActive; | |
| const playerEl = document.createElement('div'); | |
| playerEl.className = `bg-gray-800 rounded-xl p-4 border-2 ${isCurrent ? 'border-primary-500 animate-pulse-fast' : 'border-gray-700'}`; | |
| playerEl.innerHTML = ` | |
| <div class="flex justify-between"> | |
| <div class="flex items-center"> | |
| <div class="w-10 h-10 rounded-full ${player.color} flex items-center justify-center text-white text-lg mr-3"> | |
| <i class="fas ${player.icon}"></i> | |
| </div> | |
| <h3 class="font-bold ${isCurrent ? 'text-primary-400' : ''}">${player.name}</h3> | |
| </div> | |
| <div class="text-right"> | |
| <div class="font-bold text-lg">${player.score}</div> | |
| <div class="text-sm text-gray-400">Parts</div> | |
| </div> | |
| </div> | |
| <div class="mt-2 text-sm flex justify-between text-gray-400"> | |
| <div>${player.buzzes} buzzes</div> | |
| <div>${player.damage} damage</div> | |
| </div> | |
| `; | |
| playerStatusEl.appendChild(playerEl); | |
| }); | |
| // Update current player display | |
| if (gameState.players.length > 0 && gameState.gameActive) { | |
| const currentPlayer = gameState.players[gameState.currentPlayer]; | |
| currentPlayerEl.innerHTML = ` | |
| <i class="${currentPlayer.icon} mr-2"></i> | |
| ${currentPlayer.name}'s Turn | |
| `; | |
| } else if (!gameState.gameActive && gameState.players.length > 0) { | |
| currentPlayerEl.textContent = 'Click NEW GAME to start'; | |
| } | |
| } | |
| // Update life bar display | |
| function updateLifeDisplay() { | |
| // Calculate life percentage (can be negative) | |
| lifeValueEl.textContent = `${Math.round(gameState.patientLife)}%`; | |
| // Handle negative life separately | |
| if (gameState.patientLife <= 0) { | |
| const positiveAmount = Math.min(100, Math.abs(gameState.patientLife)); | |
| negativeLifeEl.style.width = `${positiveAmount}%`; | |
| lifeFillEl.style.width = '0'; | |
| // Change life text based on status | |
| if (gameState.patientLife < -50) { | |
| lifeTextEl.textContent = 'Deceased'; | |
| lifeTextEl.classList.add('text-gray-300'); | |
| } else { | |
| lifeTextEl.textContent = 'Critical Condition'; | |
| lifeTextEl.classList.add('text-red-400'); | |
| } | |
| return; | |
| } | |
| // Positive life | |
| lifeFillEl.style.width = `${gameState.patientLife}%`; | |
| negativeLifeEl.style.width = '0'; | |
| // Change text based on life level | |
| if (gameState.patientLife > 70) { | |
| lifeTextEl.textContent = 'Stable Condition'; | |
| lifeTextEl.className = 'absolute inset-0 flex items-center justify-center font-bold text-success-400'; | |
| } else if (gameState.patientLife > 30) { | |
| lifeTextEl.textContent = 'Serious Condition'; | |
| lifeTextEl.className = 'absolute inset-0 flex items-center justify-center font-bold text-warning-400'; | |
| } else { | |
| lifeTextEl.textContent = 'Critical Condition'; | |
| lifeTextEl.className = 'absolute inset-0 flex items-center justify-center font-bold text-red-400 animate-pulse-fast'; | |
| } | |
| } | |
| // Open player modal | |
| function openAddPlayerModal() { | |
| // Generate icon options | |
| iconPickerEl.innerHTML = ''; | |
| playerIcons.forEach((icon, index) => { | |
| const iconEl = document.createElement('div'); | |
| iconEl.className = `icon-option flex justify-center items-center h-8 w-8 rounded-full bg-primary-500 text-white cursor-pointer hover:bg-secondary-500 ${index === 0 ? 'selected' : ''}`; | |
| iconEl.innerHTML = `<i class="fas ${icon}"></i>`; | |
| iconEl.dataset.icon = icon; | |
| iconEl.dataset.color = getColorForIndex(index); | |
| iconEl.addEventListener('click', () => { | |
| document.querySelectorAll('.icon-option').forEach(el => el.classList.remove('border-2', 'border-white')); | |
| iconEl.classList.add('border-2', 'border-white'); | |
| }); | |
| iconPickerEl.appendChild(iconEl); | |
| }); | |
| playerModal.classList.remove('hidden'); | |
| } | |
| // Close player modal | |
| function closeAddPlayerModal() { | |
| playerModal.classList.add('hidden'); | |
| } | |
| // Generate a unique color for player | |
| function getColorForIndex(index) { | |
| const colors = ['bg-rose-600', 'bg-primary-600', 'bg-amber-600', 'bg-emerald-600', 'bg-violet-600', 'bg-pink-600', 'bg-cyan-600', 'bg-lime-600']; | |
| return colors[index % colors.length]; | |
| } | |
| // Add a new player | |
| function addNewPlayer() { | |
| const nameInput = document.getElementById('player-name'); | |
| const playerName = nameInput.value.trim() || 'Player ' + (gameState.players.length + 1); | |
| const selectedIcon = document.querySelector('.icon-option.selected') || | |
| document.querySelector('.icon-option:first-child'); | |
| const icon = selectedIcon.dataset.icon; | |
| const color = selectedIcon.dataset.color; | |
| const player = { | |
| id: gameState.players.length, | |
| name: playerName, | |
| icon: icon, | |
| color: color, | |
| score: 0, | |
| buzzes: 0, | |
| damage: 0 | |
| }; | |
| gameState.players.push(player); | |
| updatePlayerListDisplay(); | |
| closeAddPlayerModal(); | |
| // Reset input for next player | |
| nameInput.value = 'Player ' + (gameState.players.length + 1); | |
| // Auto focus | |
| nameInput.focus(); | |
| } | |
| // Handle buzz detection | |
| function handleBuzzDetected() { | |
| if (!gameState.gameActive) return; | |
| // Patient takes damage | |
| gameState.patientLife -= 10; | |
| gameState.buzzCount++; | |
| // Update player stats | |
| const currentPlayer = gameState.players[gameState.currentPlayer]; | |
| currentPlayer.buzzes++; | |
| currentPlayer.damage += 10; | |
| // Update UI | |
| buzzCountEl.innerHTML = `<i class="fas fa-bolt mr-2 text-yellow-400 animate-buzz"></i> ${gameState.buzzCount} Buzzes`; | |
| updateLifeDisplay(); | |
| updatePlayerListDisplay(); | |
| // Visual feedback | |
| document.body.classList.add('buzzer-active'); | |
| setTimeout(() => { | |
| document.body.classList.remove('buzzer-active'); | |
| checkGameOver(); | |
| }, 1000); | |
| // Switch to next player without part being removed | |
| setTimeout(nextPlayer, 1000); | |
| } | |
| // Handle successful part removal | |
| function handlePartRemoval(partId) { | |
| if (!gameState.gameActive) return; | |
| // Add to removed parts | |
| gameState.extractedParts.push(partId); | |
| // Update current player score | |
| const currentPlayer = gameState.players[gameState.currentPlayer]; | |
| currentPlayer.score++; | |
| // Remove the part visually | |
| const partElements = document.querySelectorAll('.body-part'); | |
| partElements.forEach(part => { | |
| if (part.dataset.id === partId) { | |
| part.classList.add('removed-part'); | |
| setTimeout(() => { | |
| part.remove(); | |
| renderBodyParts(); // Re-render to update positions | |
| }, 300); | |
| } | |
| }); | |
| updatePlayerListDisplay(); | |
| checkGameOver(); | |
| // If game continues, move to next player after delay | |
| setTimeout(() => { | |
| if (gameState.gameActive) { | |
| nextPlayer(); | |
| } | |
| }, 1000); | |
| } | |
| // Move to next player | |
| function nextPlayer() { | |
| gameState.currentPlayer = (gameState.currentPlayer + 1) % gameState.players.length; | |
| updatePlayerListDisplay(); | |
| } | |
| // Check for game over conditions | |
| function checkGameOver() { | |
| const partsRemaining = bodyParts.length - gameState.extractedParts.length; | |
| // Game ends when all parts are removed or patient is dead | |
| if (partsRemaining === 0 || gameState.patientLife <= -50) { | |
| gameState.gameActive = false; | |
| showGameOver(); | |
| } | |
| } | |
| // Show game over screen with results | |
| function showGameOver() { | |
| // Determine outcome | |
| const allPartsRemoved = bodyParts.length === gameState.extractedParts.length; | |
| const patientAlive = gameState.patientLife > 0; | |
| if (allPartsRemoved && patientAlive) { | |
| gameOutcomeEl.textContent = 'SUCCESSFUL OPERATION'; | |
| gameSummaryEl.textContent = 'Congratulations! All parts were removed successfully.'; | |
| gameOutcomeEl.className = 'text-3xl font-bold mb-2 text-success-500'; | |
| } else if (!patientAlive) { | |
| gameOutcomeEl.textContent = 'PATIENT DECEASED'; | |
| gameSummaryEl.textContent = 'The surgery proved too traumatic for the patient.'; | |
| gameOutcomeEl.className = 'text-3xl font-bold mb-2 text-red-500'; | |
| } else { | |
| gameOutcomeEl.textContent = 'GAME OVER'; | |
| gameSummaryEl.textContent = 'The operation was abandoned.'; | |
| gameOutcomeEl.className = 'text-3xl font-bold mb-2 text-warning-500'; | |
| } | |
| // Sort players by score then by least damage | |
| const sortedPlayers = [...gameState.players].sort((a, b) => { | |
| if (b.score !== a.score) return b.score - a.score; | |
| return a.damage - b.damage; | |
| }); | |
| // Build winner text | |
| const topPlayer = sortedPlayers[0]; | |
| let winnerText = `${topPlayer.name} wins by `; | |
| if (allPartsRemoved && patientAlive) { | |
| winnerText += `removing ${topPlayer.score} ${topPlayer.score === 1 ? 'part' : 'parts'}!`; | |
| } else { | |
| winnerText += `causing only ${topPlayer.damage}% damage`; | |
| } | |
| document.getElementById('game-outcome').insertAdjacentHTML('afterend', | |
| `<p class="text-lg text-success-400 font-bold">${winnerText}</p>`); | |
| // Display player results | |
| finalScoresEl.innerHTML = ''; | |
| sortedPlayers.forEach((player, index) => { | |
| const playerScore = document.createElement('div'); | |
| playerScore.className = 'flex justify-between p-2 rounded-lg bg-gray-600'; | |
| playerScore.innerHTML = ` | |
| <div class="flex items-center"> | |
| <span class="w-6 h-6 rounded-full ${player.color} flex items-center justify-center text-white mr-2 text-sm"> | |
| <i class="fas ${player.icon}"></i> | |
| </span> | |
| <span>${player.name}</span> | |
| ${index === 0 ? `<span class="ml-2 px-2 py-1 bg-amber-600 rounded-full text-xs">WINNER</span>` : ''} | |
| </div> | |
| <div class="text-right"> | |
| <div> | |
| <span class="font-bold">${player.score}</span> parts, | |
| <span class="text-rose-400">${player.buzzes}</span> buzzes | |
| </div> | |
| <div class="text-sm">(${player.damage}% damage)</div> | |
| </div> | |
| `; | |
| finalScoresEl.appendChild(playerScore); | |
| }); | |
| gameOverModal.classList.remove('hidden'); | |
| } | |
| // Start a new game | |
| function startNewGame() { | |
| // Reset game state but keep players | |
| gameState.patientLife = 100; | |
| gameState.extractedParts = []; | |
| gameState.currentPlayer = 0; | |
| gameState.buzzCount = 0; | |
| gameState.gameActive = true; | |
| // Reset player stats | |
| gameState.players.forEach(player => { | |
| player.score = 0; | |
| player.buzzes = 0; | |
| player.damage = 0; | |
| }); | |
| // Update UI | |
| buzzCountEl.innerHTML = `<i class="fas fa-bolt mr-2"></i> ${gameState.buzzCount} Buzzes`; | |
| updateLifeDisplay(); | |
| renderBodyParts(); | |
| updatePlayerListDisplay(); | |
| gameOverModal.classList.add('hidden'); | |
| // Setup microphone | |
| setupMicrophone(); | |
| } | |
| // Start calibration mode | |
| function startCalibration() { | |
| audioSettings.calibrationSamples = []; | |
| document.getElementById('calibration-count').textContent = '0'; | |
| document.getElementById('calibration-progress').style.width = '0%'; | |
| document.getElementById('calibration-status').textContent = 'Making initial analysis...'; | |
| document.getElementById('calibration-status').className = 'inline-block px-3 py-1 rounded-full text-sm text-gray-200 bg-gray-700 mb-4'; | |
| calibrationModal.classList.remove('hidden'); | |
| startAudioDetection(true); | |
| } | |
| // Cancel calibration | |
| function cancelCalibration() { | |
| calibrationModal.classList.add('hidden'); | |
| if (gameState.gameActive) { | |
| startAudioDetection(); | |
| } | |
| } | |
| // Finish calibration and calculate thresholds | |
| function finishCalibration() { | |
| if (audioSettings.calibrationSamples.length > 0) { | |
| // Calculate average frequency and volume thresholds | |
| const avgFreq = audioSettings.calibrationSamples.reduce((sum, val) => sum + val.frequency, 0) / audioSettings.calibrationSamples.length; | |
| const avgVol = audioSettings.calibrationSamples.reduce((sum, val) => sum + val.volume, 0) / audioSettings.calibrationSamples.length; | |
| // Set detection thresholds with some buffer | |
| audioSettings.minFrequency = Math.max(0, avgFreq - 200); | |
| audioSettings.maxFrequency = avgFreq + 200; | |
| audioSettings.threshold = avgVol * 0.7; // 70% of average volume | |
| audioSettings.calibrated = true; | |
| document.getElementById('calibration-status').textContent = 'Calibration complete!'; | |
| document.getElementById('calibration-status').className = 'inline-block px-3 py-1 rounded-full text-sm text-white bg-success-600 mb-4'; | |
| setTimeout(() => { | |
| calibrationModal.classList.add('hidden'); | |
| if (gameState.gameActive) { | |
| startAudioDetection(); | |
| } | |
| }, 1000); | |
| } | |
| } | |
| // Start audio detection | |
| function startAudioDetection(isCalibrating = false) { | |
| const bufferLength = gameState.analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| const frequencyData = new Float32Array(bufferLength); | |
| const sampleRate = gameState.audioContext.sampleRate; | |
| const minFreq = isCalibrating ? 0 : audioSettings.minFrequency; | |
| const maxFreq = isCalibrating ? sampleRate/2 : audioSettings.maxFrequency; | |
| const detectSound = () => { | |
| if (!gameState.gameActive && !isCalibrating) return; | |
| gameState.analyser.getByteFrequencyData(dataArray); | |
| gameState.analyser.getFloatFrequencyData(frequencyData); | |
| // Get frequency with peak volume | |
| let maxVolume = -Infinity; | |
| let peakFrequency = 0; | |
| let totalVolume = 0; | |
| for (let i = 0; i < bufferLength; i++) { | |
| const freq = i * sampleRate / bufferLength; | |
| if (freq >= minFreq && freq <= maxFreq) { | |
| if (dataArray[i] > maxVolume) { | |
| maxVolume = dataArray[i]; | |
| peakFrequency = freq; | |
| } | |
| totalVolume += dataArray[i]; | |
| } | |
| } | |
| const avgVolume = totalVolume / bufferLength; | |
| // Update sound level indicator for calibration | |
| if (isCalibrating) { | |
| const soundLevelBar = document.querySelector('#sound-level div'); | |
| const normalizedVolume = Math.min(100, Math.max(0, (avgVolume / 255) * 100)); | |
| soundLevelBar.style.width = `${normalizedVolume}%`; | |
| } | |
| // Detect buzz - during calibration or normal play | |
| if (avgVolume > (isCalibrating ? 30 : audioSettings.threshold * 255)) { | |
| if (isCalibrating) { | |
| // During calibration, record the frequency signature | |
| audioSettings.calibrationSamples.push({ | |
| frequency: peakFrequency, | |
| volume: avgVolume / 255 | |
| }); | |
| const count = audioSettings.calibrationSamples.length; | |
| document.getElementById('calibration-count').textContent = count; | |
| document.getElementById('calibration-progress').style.width = `${Math.min(100, count * 20)}%`; | |
| if (count >= 3) { | |
| document.getElementById('calibration-status').textContent = 'Good samples collected!'; | |
| document.getElementById('calibration-status').className = 'inline-block px-3 py-1 rounded-full text-sm text-white bg-success-600 mb-4'; | |
| } | |
| } else { | |
| // During normal gameplay, trigger buzz effects | |
| handleBuzzDetected(); | |
| buzzerStatusEl.classList.remove('bg-green-500', 'bg-yellow-500'); | |
| buzzerStatusEl.classList.add('bg-red-500', 'animate-buzz'); | |
| setTimeout(() => { | |
| buzzerStatusEl.classList.remove('animate-buzz'); | |
| }, 500); | |
| } | |
| } else { | |
| buzzerStatusEl.classList.remove('bg-red-500', 'animate-buzz'); | |
| if (gameState.gameActive) { | |
| buzzerStatusEl.classList.add('bg-green-500'); | |
| } | |
| } | |
| requestAnimationFrame(detectSound); | |
| }; | |
| detectSound(); | |
| } | |
| // Simulate a buzz for testing | |
| function simulateBuzz() { | |
| handleBuzzDetected(); | |
| } | |
| // Initialize default player | |
| gameState.players.push({ | |
| id: 0, | |
| name: "Surgeon", | |
| icon: "fa-user-md", | |
| color: "bg-primary-600", | |
| score: 0, | |
| buzzes: 0, | |
| damage: 0 | |
| }); | |
| // Initialize game when loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initGame(); | |
| updateLifeDisplay(); | |
| setupMicrophone(); // Request mic access on load | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=web3district/operation-table" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |