|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Blind Chef's Culinary Journey</title> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://unpkg.com/feather-icons"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
|
|
<style> |
|
|
@import url('https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&display=swap'); |
|
|
body { |
|
|
font-family: 'Atkinson Hyperlegible', sans-serif; |
|
|
} |
|
|
.screen-reader-text { |
|
|
position: absolute; |
|
|
width: 1px; |
|
|
height: 1px; |
|
|
padding: 0; |
|
|
margin: -1px; |
|
|
overflow: hidden; |
|
|
clip: rect(0, 0, 0, 0); |
|
|
white-space: nowrap; |
|
|
border-width: 0; |
|
|
} |
|
|
.audio-cue { |
|
|
position: absolute; |
|
|
opacity: 0; |
|
|
width: 0; |
|
|
height: 0; |
|
|
} |
|
|
.haptic-feedback { |
|
|
transition: transform 0.1s; |
|
|
} |
|
|
.haptic-feedback:active { |
|
|
transform: scale(0.95); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-amber-50 text-gray-900 min-h-screen flex flex-col"> |
|
|
|
|
|
<audio id="welcome-sound" class="audio-cue" src="https://assets.mixkit.co/sfx/preview/mixkit-positive-interface-beep-221.mp3" preload="auto"></audio> |
|
|
<audio id="button-sound" class="audio-cue" src="https://assets.mixkit.co/sfx/preview/mixkit-modern-click-box-check-1120.mp3" preload="auto"></audio> |
|
|
<audio id="error-sound" class="audio-cue" src="https://assets.mixkit.co/sfx/preview/mixkit-warning-alarm-688.mp3" preload="auto"></audio> |
|
|
<audio id="success-sound" class="audio-cue" src="https://assets.mixkit.co/sfx/preview/mixkit-achievement-bell-600.mp3" preload="auto"></audio> |
|
|
|
|
|
|
|
|
<main class="flex-1 container mx-auto px-4 py-8" role="main" aria-labelledby="main-heading"> |
|
|
<div id="vanta-bg" class="fixed inset-0 -z-10 opacity-20"></div> |
|
|
|
|
|
<header class="mb-12 text-center"> |
|
|
<h1 id="main-heading" class="text-4xl md:text-5xl font-bold mb-4 text-amber-800" tabindex="0">Blind Chef's Culinary Journey</h1> |
|
|
<p class="text-lg md:text-xl text-amber-700 max-w-3xl mx-auto" tabindex="0"> |
|
|
Experience the world of culinary arts through sound, touch, and imagination. |
|
|
</p> |
|
|
</header> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto"> |
|
|
|
|
|
<section aria-labelledby="game-section" class="bg-white rounded-xl shadow-lg p-6 md:col-span-2"> |
|
|
<h2 id="game-section" class="text-2xl font-bold mb-6 text-amber-700" tabindex="0">Your Restaurant</h2> |
|
|
|
|
|
<div id="game-container" class="space-y-6"> |
|
|
<div class="bg-amber-100 rounded-lg p-4 border-l-4 border-amber-400"> |
|
|
<h3 class="font-bold mb-2" tabindex="0">Today's Special</h3> |
|
|
<p id="daily-special" class="mb-4" tabindex="0">Loading today's menu...</p> |
|
|
<button id="generate-special" class="haptic-feedback bg-amber-600 hover:bg-amber-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors" aria-label="Generate today's special dish"> |
|
|
<i data-feather="refresh-cw" aria-hidden="true"></i> |
|
|
<span>Generate Special</span> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> |
|
|
<div class="bg-white border border-amber-200 rounded-lg p-4"> |
|
|
<h3 class="font-bold mb-2" tabindex="0">Customers Waiting</h3> |
|
|
<p id="customer-count" class="text-3xl font-bold text-amber-600" tabindex="0">0</p> |
|
|
<button id="serve-customer" class="haptic-feedback mt-2 bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors" aria-label="Serve next customer"> |
|
|
<i data-feather="user-plus" aria-hidden="true"></i> |
|
|
<span>Serve Customer</span> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
<div class="bg-white border border-amber-200 rounded-lg p-4"> |
|
|
<h3 class="font-bold mb-2" tabindex="0">Dishes Prepared</h3> |
|
|
<p id="dish-count" class="text-3xl font-bold text-amber-600" tabindex="0">0</p> |
|
|
<button id="prepare-dish" class="haptic-feedback mt-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors" aria-label="Prepare a dish"> |
|
|
<i data-feather="utensils" aria-hidden="true"></i> |
|
|
<span>Prepare Dish</span> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="bg-amber-50 rounded-lg p-4 border border-amber-200"> |
|
|
<h3 class="font-bold mb-2" tabindex="0">Kitchen Status</h3> |
|
|
<div id="kitchen-status" class="space-y-2" tabindex="0"> |
|
|
<p>Your kitchen is quiet. Start preparing dishes!</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section aria-labelledby="stats-section" class="bg-white rounded-xl shadow-lg p-6"> |
|
|
<h2 id="stats-section" class="text-2xl font-bold mb-6 text-amber-700" tabindex="0">Restaurant Stats</h2> |
|
|
|
|
|
<div class="space-y-6"> |
|
|
<div> |
|
|
<h3 class="font-bold mb-2" tabindex="0">Total Customers Served</h3> |
|
|
<p id="total-customers" class="text-3xl font-bold text-amber-600" tabindex="0">0</p> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<h3 class="font-bold mb-2" tabindex="0">Total Dishes Prepared</h3> |
|
|
<p id="total-dishes" class="text-3xl font-bold text-amber-600" tabindex="0">0</p> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<h3 class="font-bold mb-2" tabindex="0">Popular Dishes</h3> |
|
|
<ol id="popular-dishes" class="list-decimal list-inside space-y-1" tabindex="0"> |
|
|
<li>No dishes prepared yet</li> |
|
|
</ol> |
|
|
</div> |
|
|
|
|
|
<div class="bg-amber-50 rounded-lg p-4 border border-amber-200"> |
|
|
<h3 class="font-bold mb-2" tabindex="0">Save Your Progress</h3> |
|
|
<div class="flex flex-col gap-2"> |
|
|
<button id="save-game" class="haptic-feedback bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors" aria-label="Save game progress"> |
|
|
<i data-feather="save" aria-hidden="true"></i> |
|
|
<span>Save Game</span> |
|
|
</button> |
|
|
<button id="load-game" class="haptic-feedback bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors" aria-label="Load saved game"> |
|
|
<i data-feather="upload" aria-hidden="true"></i> |
|
|
<span>Load Game</span> |
|
|
</button> |
|
|
<button id="reset-game" class="haptic-feedback bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors" aria-label="Reset game data"> |
|
|
<i data-feather="trash-2" aria-hidden="true"></i> |
|
|
<span>Reset Game</span> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="screen-reader-text" aria-live="polite" id="audio-feedback"></div> |
|
|
</main> |
|
|
|
|
|
<footer class="bg-amber-800 text-white py-6 mt-12"> |
|
|
<div class="container mx-auto px-4 text-center"> |
|
|
<p tabindex="0">An accessible restaurant simulation game for everyone</p> |
|
|
<p class="mt-2" tabindex="0">All progress is saved in your browser's local storage</p> |
|
|
</div> |
|
|
</footer> |
|
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r121/three.min.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/vanta@latest/dist/vanta.globe.min.js"></script> |
|
|
<script> |
|
|
|
|
|
VANTA.GLOBE({ |
|
|
el: "#vanta-bg", |
|
|
mouseControls: true, |
|
|
touchControls: true, |
|
|
gyroControls: false, |
|
|
minHeight: 200.00, |
|
|
minWidth: 200.00, |
|
|
scale: 1.00, |
|
|
scaleMobile: 1.00, |
|
|
color: 0xe3a008, |
|
|
backgroundColor: 0xfef3c7, |
|
|
size: 0.80 |
|
|
}); |
|
|
|
|
|
|
|
|
const gameState = { |
|
|
customersWaiting: 0, |
|
|
dishesPrepared: 0, |
|
|
totalCustomers: 0, |
|
|
totalDishes: 0, |
|
|
dailySpecial: "", |
|
|
popularDishes: {}, |
|
|
kitchenLog: [], |
|
|
lastSaved: null |
|
|
}; |
|
|
|
|
|
|
|
|
const dishes = [ |
|
|
"Braille Burgers", "Tactile Tacos", "Audio Alfredo", |
|
|
"Sensory Salad", "Haptic Hotdogs", "Echo Eclairs", |
|
|
"Guide Dog Gyros", "White Cane Waffles", "Sonar Soufflé", |
|
|
"Accessible Apple Pie", "Descriptive Dumplings", |
|
|
"Non-Visual Nachos", "Olfactory Omelette", "Audio-Descriptive Asparagus", |
|
|
"Haptic Hash Browns", "Talking Tiramisu", "Sighted Guide Sandwich", |
|
|
"Braille Biscuits", "Echo Espresso", "Tactile Toast" |
|
|
]; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
feather.replace(); |
|
|
playSound('welcome-sound'); |
|
|
loadGame(); |
|
|
updateUI(); |
|
|
|
|
|
const buttons = [ |
|
|
{id: 'generate-special', func: generateDailySpecial}, |
|
|
{id: 'serve-customer', func: serveCustomer}, |
|
|
{id: 'prepare-dish', func: prepareDish}, |
|
|
{id: 'save-game', func: saveGame}, |
|
|
{id: 'load-game', func: loadGame}, |
|
|
{id: 'reset-game', func: resetGame} |
|
|
]; |
|
|
|
|
|
buttons.forEach(btn => { |
|
|
const button = document.getElementById(btn.id); |
|
|
button.addEventListener('click', () => { |
|
|
playSound('button-sound'); |
|
|
btn.func(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
document.querySelectorAll('button').forEach(button => { |
|
|
button.addEventListener('keydown', (e) => { |
|
|
if (e.key === 'Enter' || e.key === ' ') { |
|
|
e.preventDefault(); |
|
|
button.click(); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
function generateDailySpecial() { |
|
|
const randomIndex = Math.floor(Math.random() * dishes.length); |
|
|
gameState.dailySpecial = dishes[randomIndex]; |
|
|
updateUI(); |
|
|
speakFeedback(`Today's special is ${gameState.dailySpecial}`); |
|
|
} |
|
|
function serveCustomer() { |
|
|
gameState.customersWaiting++; |
|
|
gameState.totalCustomers++; |
|
|
updateUI(); |
|
|
speakFeedback(`Customer arrived. Total waiting: ${gameState.customersWaiting}`); |
|
|
|
|
|
|
|
|
addKitchenLog(`Customer #${gameState.totalCustomers} arrived`); |
|
|
} |
|
|
function prepareDish() { |
|
|
if (gameState.customersWaiting === 0) { |
|
|
playSound('error-sound'); |
|
|
speakFeedback("No customers to serve!"); |
|
|
addKitchenLog("Tried to prepare dish with no customers"); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (!gameState.dailySpecial) { |
|
|
playSound('error-sound'); |
|
|
speakFeedback("Please set today's special first"); |
|
|
return; |
|
|
} |
|
|
|
|
|
gameState.customersWaiting--; |
|
|
gameState.dishesPrepared++; |
|
|
gameState.totalDishes++; |
|
|
|
|
|
|
|
|
if (!gameState.popularDishes[gameState.dailySpecial]) { |
|
|
gameState.popularDishes[gameState.dailySpecial] = 0; |
|
|
} |
|
|
gameState.popularDishes[gameState.dailySpecial]++; |
|
|
|
|
|
|
|
|
addKitchenLog(`Prepared ${gameState.dailySpecial} for customer`); |
|
|
|
|
|
playSound('success-sound'); |
|
|
updateUI(); |
|
|
speakFeedback(`Prepared ${gameState.dailySpecial}. ${gameState.customersWaiting} customers remaining`); |
|
|
} |
|
|
|
|
|
function addKitchenLog(message) { |
|
|
const timestamp = new Date().toLocaleTimeString(); |
|
|
gameState.kitchenLog.unshift(`${timestamp}: ${message}`); |
|
|
|
|
|
|
|
|
if (gameState.kitchenLog.length > 5) { |
|
|
gameState.kitchenLog.pop(); |
|
|
} |
|
|
|
|
|
updateUI(); |
|
|
} |
|
|
function saveGame() { |
|
|
try { |
|
|
localStorage.setItem('blindChefGame', JSON.stringify(gameState)); |
|
|
gameState.lastSaved = new Date(); |
|
|
speakFeedback("Game progress saved successfully"); |
|
|
addKitchenLog("Game saved to browser storage"); |
|
|
updateUI(); |
|
|
playSound('success-sound'); |
|
|
} catch (e) { |
|
|
playSound('error-sound'); |
|
|
speakFeedback("Error saving game: " + e.message); |
|
|
} |
|
|
} |
|
|
function loadGame() { |
|
|
try { |
|
|
const savedGame = localStorage.getItem('blindChefGame'); |
|
|
if (savedGame) { |
|
|
const parsedGame = JSON.parse(savedGame); |
|
|
Object.assign(gameState, parsedGame); |
|
|
speakFeedback("Game loaded successfully"); |
|
|
addKitchenLog("Loaded game from browser storage"); |
|
|
updateUI(); |
|
|
playSound('success-sound'); |
|
|
} else { |
|
|
speakFeedback("No saved game found"); |
|
|
} |
|
|
} catch (e) { |
|
|
playSound('error-sound'); |
|
|
speakFeedback("Error loading game: " + e.message); |
|
|
} |
|
|
} |
|
|
function resetGame() { |
|
|
if (confirm("Are you sure you want to reset all game data?")) { |
|
|
localStorage.removeItem('blindChefGame'); |
|
|
Object.assign(gameState, { |
|
|
customersWaiting: 0, |
|
|
dishesPrepared: 0, |
|
|
totalCustomers: 0, |
|
|
totalDishes: 0, |
|
|
dailySpecial: "", |
|
|
popularDishes: {}, |
|
|
kitchenLog: [], |
|
|
lastSaved: null |
|
|
}); |
|
|
speakFeedback("Game has been reset"); |
|
|
updateUI(); |
|
|
playSound('success-sound'); |
|
|
} else { |
|
|
speakFeedback("Reset cancelled"); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateUI() { |
|
|
|
|
|
document.getElementById('customer-count').textContent = gameState.customersWaiting; |
|
|
document.getElementById('dish-count').textContent = gameState.dishesPrepared; |
|
|
document.getElementById('total-customers').textContent = gameState.totalCustomers; |
|
|
document.getElementById('total-dishes').textContent = gameState.totalDishes; |
|
|
|
|
|
|
|
|
const dailySpecialEl = document.getElementById('daily-special'); |
|
|
if (gameState.dailySpecial) { |
|
|
dailySpecialEl.textContent = gameState.dailySpecial; |
|
|
} else { |
|
|
dailySpecialEl.textContent = "No special set yet"; |
|
|
} |
|
|
|
|
|
|
|
|
const kitchenStatusEl = document.getElementById('kitchen-status'); |
|
|
kitchenStatusEl.innerHTML = ''; |
|
|
if (gameState.kitchenLog.length > 0) { |
|
|
gameState.kitchenLog.forEach(log => { |
|
|
const p = document.createElement('p'); |
|
|
p.textContent = log; |
|
|
kitchenStatusEl.appendChild(p); |
|
|
}); |
|
|
} else { |
|
|
kitchenStatusEl.innerHTML = '<p>Your kitchen is quiet. Start preparing dishes!</p>'; |
|
|
} |
|
|
|
|
|
|
|
|
const popularDishesEl = document.getElementById('popular-dishes'); |
|
|
popularDishesEl.innerHTML = ''; |
|
|
|
|
|
if (Object.keys(gameState.popularDishes).length > 0) { |
|
|
|
|
|
const sortedDishes = Object.entries(gameState.popularDishes) |
|
|
.sort((a, b) => b[1] - a[1]) |
|
|
.slice(0, 3); |
|
|
|
|
|
sortedDishes.forEach(([dish, count]) => { |
|
|
const li = document.createElement('li'); |
|
|
li.textContent = `${dish} (${count})`; |
|
|
popularDishesEl.appendChild(li); |
|
|
}); |
|
|
} else { |
|
|
const li = document.createElement('li'); |
|
|
li.textContent = "No dishes prepared yet"; |
|
|
popularDishesEl |
|
|
</body> |
|
|
</html> |