operation-table / index.html
web3district's picture
before the app asked to access microphone and now not please fix - Follow Up Deployment
cbc9ad8 verified
<!DOCTYPE html>
<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>