@ -2,75 +2,67 @@
< html lang = "en" >
< html lang = "en" >
< head >
< head >
< meta charset = "UTF-8" >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, user-scalable=no " >
< title > 🐭 Mice! - Browser Edition< / title >
< title > 🐭 Mice! - Browser Edition< / title >
<!-- Bootstrap CSS -->
< link href = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel = "stylesheet" >
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" >
< style >
< style >
body {
body {
margin: 0;
margin: 0;
padding: 2 0px;
padding: 1 0px;
font-family: Arial, sans-serif;
font-family: Arial, sans-serif;
background-color: #1e1e1e;
background-color: #1e1e1e;
color: #ffffff;
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
min-height: 100vh;
}
}
h1 {
h1 {
color: #ff9f43;
color: #ff9f43;
margin-bottom: 10px;
margin-bottom: 10px;
}
font-size: 1.8rem;
.container {
max-width: 800px;
width: 100%;
}
}
#status {
#status {
margin: 20px 0;
padding: 8px;
padding: 10px;
background-color: #2d2d2d;
background-color: #2d2d2d;
border-radius: 5px;
border-radius: 5px;
font-family: monospace;
font-family: monospace;
font-size: 0.85rem;
margin-bottom: 10px;
}
}
.canvas-container {
.canvas-container {
margin: 20px 0;
border: 2px solid #ff9f43;
border: 2px solid #ff9f43;
border-radius: 5px;
border-radius: 5px;
background-color: #000;
background-color: #000;
display: inline-block;
width: 100%;
max-width: 640px;
aspect-ratio: 4/3;
position: relative;
}
}
#canvas {
#canvas {
display: block;
display: block;
width: 100%;
height: 100%;
image-rendering: pixelated;
image-rendering: pixelated;
image-rendering: crisp-edges;
image-rendering: crisp-edges;
}
}
.controls {
button, .btn {
margin: 20px 0;
background-color: #ff9f43 !important;
}
color: #1e1e1e !important;
border: none !important;
button {
background-color: #ff9f43;
color: #1e1e1e;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
font-weight: bold;
}
}
button:hover {
button:hover, .btn:hover {
background-color: #ffb366;
background-color: #ffb366 !important;
}
}
button:disabled {
button:disabled, .btn:disabled {
background-color: #666;
background-color: #666 !important;
cursor: not-allowed;
cursor: not-allowed;
}
}
@ -78,135 +70,322 @@
background-color: #2d2d2d;
background-color: #2d2d2d;
padding: 15px;
padding: 15px;
border-radius: 5px;
border-radius: 5px;
margin: 20 px 0;
margin: 15 px 0;
}
}
.info h2 {
.info h2 {
color: #ff9f43;
color: #ff9f43;
margin-top: 0;
margin-top: 0;
font-size: 1.3rem;
}
}
code {
code {
background-color: #1e1e1e;
background-color: #1e1e1e;
padding: 2px 6px;
padding: 2px 6px;
border-radius: 3px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
}
/* Game-specific layout */
.game-layout {
.hud {
margin-top: 8px;
background: rgba(0,0,0,0.6);
padding: 6px 10px;
border-radius: 4px;
color: #dcdcdc;
font-family: monospace;
font-size: 0.85rem;
}
input[type="range"] {
accent-color: #ff9f43;
}
progress {
accent-color: #ff9f43;
height: 8px;
}
select, .form-select {
background-color: #2d2d2d;
color: #fff;
border: 1px solid #555;
}
details summary {
cursor: pointer;
color: #ff9f43;
font-weight: bold;
}
details summary:hover {
color: #ffb366;
}
/* Mobile Touch Controls */
.touch-controls {
margin-top: 15px;
user-select: none;
-webkit-user-select: none;
}
.dpad-container {
position: relative;
width: 160px;
height: 160px;
margin: 0 auto;
}
.dpad-btn {
position: absolute;
width: 50px;
height: 50px;
background-color: #ff9f43;
border: 2px solid #1e1e1e;
border-radius: 8px;
display: flex;
display: flex;
gap: 20px;
align-items: center;
align-items: flex-start;
justify-content: center;
font-size: 1.5rem;
cursor: pointer;
user-select: none;
touch-action: none;
transition: transform 0.1s ease, opacity 0.1s ease;
}
.dpad-btn:active {
background-color: #ffb366;
transform: scale(0.95);
opacity: 0.7;
}
.dpad-up { top: 0; left: 55px; }
.dpad-down { bottom: 0; left: 55px; }
.dpad-left { top: 55px; left: 0; }
.dpad-right { top: 55px; right: 0; }
.dpad-center {
top: 55px;
left: 55px;
background-color: #2d2d2d;
pointer-events: none;
}
}
.game-layout .left {
flex: 1 1 640px;
.action-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-top: 15px;
}
.action-btn {
height: 55px;
font-size: 0.95rem;
display: flex;
display: flex;
flex-direction: column;
flex-direction: column;
align-items: center;
align-items: center;
justify-content: center;
padding: 5px;
touch-action: none;
transition: transform 0.1s ease, opacity 0.1s ease;
}
.action-btn:active {
transform: scale(0.95);
opacity: 0.7;
}
}
.game-layout .right {
width: 320px;
.action-btn small {
flex: 0 0 320px;
font-size: 0.7rem;
font-weight: normal;
margin-top: 2px;
}
}
.hud {
margin-top: 8px;
/* Responsive adjustments */
background: rgba(0,0,0,0.6);
@media (min-width: 768px) {
padding: 6px 10px;
.touch-controls {
border-radius: 4px;
display: none;
}
}
@media (max-width: 767px) {
h1 { font-size: 1.5rem; }
.info h2 { font-size: 1.1rem; }
.info { padding: 10px; font-size: 0.9rem; }
.info ul { padding-left: 20px; }
/* Hide keyboard help on mobile */
.help { display: none; }
}
.form-label {
color: #dcdcdc;
color: #dcdcdc;
font-family: monospace;
}
}
.control-row { margin-top: 10px; display:flex; gap:8px; align-items:center; }
.control-row label { min-width: 70px; font-size:0.95em; }
.modal-content {
.controls { display:flex; flex-wrap:wrap; gap:8px; }
background-color: #2d2d2d;
.progress { margin-top: 12px; }
color: #ffffff;
.help { margin-top: 12px; font-size:0.95em; }
}
input[type="range"] { width: 100%; accent-color: #ff9f43; }
progress { width: 100%; accent-color: #ff9f43; height: 8px; }
.modal-header {
select { background-color: #2d2d2d; color: #fff; border: 1px solid #555; padding: 6px; border-radius: 3px; }
border-bottom: 1px solid #555;
details summary { cursor: pointer; color: #ff9f43; font-weight: bold; }
}
details summary:hover { color: #ffb366; }
.modal-footer {
border-top: 1px solid #555;
}
.form-control {
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #555;
}
.form-control:focus {
background-color: #1e1e1e;
color: #ffffff;
border-color: #ff9f43;
box-shadow: 0 0 0 0.25rem rgba(255, 159, 67, 0.25);
}
.form-check-input {
background-color: #1e1e1e;
border-color: #555;
}
.form-check-input:checked {
background-color: #ff9f43;
border-color: #ff9f43;
}
< / style >
< / style >
< / head >
< / head >
< body >
< body >
< div class = "container" >
< div class = "container-fluid" >
< h1 > 🐭 Mice! - The Game< / h1 >
<!-- Header -->
< p > < strong > Eliminate the rats before they multiply!< / strong > A strategic maze game with bombs, mines, and gas.< / p >
< div class = "row" >
< p style = "font-size:0.9em;color:#aaa;" > Browser Edition powered by Python + Pygame via Pyodide< / p >
< div class = "col-12 text-center" >
< h1 > 🐭 Mice! - The Game< / h1 >
< div id = "status" > Loading Pyodide...< / div >
< p class = "mb-2" > < strong > Eliminate the rats before they multiply!< / strong > < / p >
< p class = "small text-muted" > Browser Edition powered by Python + Pygame via Pyodide< / p >
<!-- Profile Creation Modal -->
< / div >
< div id = "profileModal" title = "Create New Profile" >
< form id = "profileForm" >
< div style = "margin-bottom:15px;" >
< label for = "profileName" > Player Name:< / label > < br >
< input type = "text" id = "profileName" name = "profileName" placeholder = "Enter your name" style = "width:100%;padding:8px;margin-top:5px;border:1px solid #555;background:#2d2d2d;color:#fff;border-radius:3px;" required >
< / div >
< div style = "margin-bottom:15px;" >
< label for = "difficulty" > Difficulty:< / label > < br >
< select id = "difficulty" name = "difficulty" style = "width:100%;padding:8px;margin-top:5px;border:1px solid #555;background:#2d2d2d;color:#fff;border-radius:3px;" >
< option value = "normal" > Normal< / option >
< option value = "hard" > Hard< / option >
< option value = "expert" > Expert< / option >
< / select >
< / div >
< div style = "margin-bottom:15px;" >
< label >
< input type = "checkbox" id = "soundEnabled" name = "soundEnabled" checked > Enable Sound
< / label >
< / div >
< / form >
< / div >
< / div >
< div class = "game-layout" >
<!-- Status -->
< div class = "left" >
< div class = "row mb-2" >
< div class = "canvas-container" >
< div class = "col-12" >
< div id = "status" > Loading Pyodide...< / div >
< / div >
< / div >
<!-- Main Game Area -->
< div class = "row" >
<!-- Canvas Column -->
< div class = "col-lg-8 col-12 mb-3" >
< div class = "canvas-container mx-auto" >
< canvas id = "canvas" width = "640" height = "480" aria-label = "Mice game canvas" > < / canvas >
< canvas id = "canvas" width = "640" height = "480" aria-label = "Mice game canvas" > < / canvas >
< / div >
< / div >
< div id = "hud" class = "hud" > Status: < span id = "hud-status" > Idle< / span > < / div >
< div id = "hud" class = "hud text-center" > Status: < span id = "hud-status" > Idle< / span > < / div >
<!-- Mobile Touch Controls -->
< div class = "touch-controls d-lg-none" >
< div class = "row" >
< div class = "col-6" >
< h6 class = "text-center text-muted mb-2" > Movement< / h6 >
< div class = "dpad-container" >
< div class = "dpad-btn dpad-up" data-key = "ArrowUp" >
< i class = "bi bi-arrow-up" > < / i >
< / div >
< div class = "dpad-btn dpad-left" data-key = "ArrowLeft" >
< i class = "bi bi-arrow-left" > < / i >
< / div >
< div class = "dpad-btn dpad-center" > < / div >
< div class = "dpad-btn dpad-right" data-key = "ArrowRight" >
< i class = "bi bi-arrow-right" > < / i >
< / div >
< div class = "dpad-btn dpad-down" data-key = "ArrowDown" >
< i class = "bi bi-arrow-down" > < / i >
< / div >
< / div >
< / div >
< div class = "col-6" >
< h6 class = "text-center text-muted mb-2" > Actions< / h6 >
< div class = "action-buttons" >
< button class = "btn action-btn" data-key = "Enter" >
▶️ < br > < small > Start< / small >
< / button >
< button class = "btn action-btn" data-key = " " >
💣< br > < small > Bomb< / small >
< / button >
< button class = "btn action-btn" data-key = "m" >
⚠️ < br > < small > Mine< / small >
< / button >
< button class = "btn action-btn" data-key = "g" >
☁️ < br > < small > Gas< / small >
< / button >
< button class = "btn action-btn" data-key = "n" >
☢️ < br > < small > Nuclear< / small >
< / button >
< button class = "btn action-btn" data-key = "p" >
⏸️ < br > < small > Pause< / small >
< / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< div class = "right" >
<!-- Controls Sidebar -->
< div class = "controls" >
< div class = "col-lg-4 col-12" >
< button id = "startBtn" disabled > Start Game< / button >
<!-- Game Controls -->
< button id = "stopBtn" disabled > Stop Game< / button >
< div class = "mb-3" >
< button id = "fullscreenBtn" > Fullscreen< / button >
< div class = "d-flex flex-wrap gap-2" >
< button id = "muteBtn" > Mute< / button >
< button id = "startBtn" class = "btn flex-fill" disabled > Start Game< / button >
< button id = "stopBtn" class = "btn flex-fill" disabled > Stop Game< / button >
< button id = "fullscreenBtn" class = "btn flex-fill" > Fullscreen< / button >
< button id = "muteBtn" class = "btn flex-fill" > Mute< / button >
< / div >
< / div >
< / div >
< div class = "control-row" >
<!-- Volume Control -->
< label for = "volumeRange" > Volume< / label >
< div class = "mb-3" >
< input id = "volumeRange" type = "range" min = "0" max = "100" value = "100" >
< label for = "volumeRange" class = "form-label" > Volume< / label >
< input id = "volumeRange" type = "range" class = "form-range" min = "0" max = "100" value = "100" >
< / div >
< / div >
< div class = "control-row" >
<!-- Profile Selection -->
< label for = "profileSelect" > Profile< / label >
< div class = "mb-3" >
< select id = "profileSelect" > < option value = "" > (None)< / option > < / select >
< label for = "profileSelect" class = "form-label" > Profile< / label >
< button id = "loadProfileBtn" > Load< / button >
< div class = "d-flex gap-2" >
< select id = "profileSelect" class = "form-select flex-fill" >
< option value = "" > (None)< / option >
< / select >
< button id = "loadProfileBtn" class = "btn" > Load< / button >
< / div >
< / div >
< / div >
< div class = "control-row" >
< div class = "mb-3 " >
< button id = "newProfileBtn" style = "flex:1;" > + New Profile< / button >
< button id = "newProfileBtn" class = "btn w-100 "> + New Profile< / button >
< / div >
< / div >
< div class = "progress" >
<!-- Progress -->
< label > Asset load progress< / label >
< div class = "mb-3" >
< progress id = "assetProgress" value = "0" max = "100" > < / progress >
< label class = "form-label" > Asset Load Progress< / label >
< div id = "assetProgressText" > 0 / 0< / div >
< div class = "progress" style = "height: 8px;" >
< div id = "assetProgress" class = "progress-bar" role = "progressbar" style = "width: 0%; background-color: #ff9f43;" aria-valuenow = "0" aria-valuemin = "0" aria-valuemax = "100" > < / div >
< / div >
< small id = "assetProgressText" class = "text-muted" > 0 / 0< / small >
< / div >
< / div >
< details >
<!-- Advanced Options -->
< details class = "mb-3" >
< summary > Advanced< / summary >
< summary > Advanced< / summary >
< div style = "margin-top:8px;" >
< div class = "mt-2 ">
< button id = "runDemoBtn" > Run Pygame Demo< / button >
< button id = "runDemoBtn" class = "btn w-100" > Run Pygame Demo< / button >
< div style = "margin-top:8px;font-size:0.9em;color:#ddd;" > Useful for testing the canvas rendering when debugging.< / div >
< small class = "d-block mt-2 text-muted "> Useful for testing the canvas rendering when debugging.< / small >
< / div >
< / div >
< / details >
< / details >
< div class = "help" >
<!-- Keyboard Controls Help (Desktop Only) -->
< h3 > Controls< / h3 >
< div class = "help d-none d-lg-block" >
< ul >
< h5 > Keyboard Controls< / h5 >
< ul class = "small" >
< li > Arrow keys / WASD — move< / li >
< li > Arrow keys / WASD — move< / li >
< li > Space — bomb< / li >
< li > Space — bomb< / li >
< li > M — mine< / li >
< li > M — mine< / li >
@ -217,33 +396,75 @@
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
< div class = "info" >
<!-- Info Section -->
< h2 > 🎮 How to Play< / h2 >
< div class = "row mt-3" >
< p > Move your cursor around the maze and use your arsenal to eliminate rats before they overrun the level:< / p >
< div class = "col-12" >
< ul >
< div class = "info" >
< li > < strong > Bombs (Space)< / strong > : Primary weapon. Timer detonates in 4 directions. Chain reactions kill multiple rats.< / li >
< h2 > 🎮 How to Play< / h2 >
< li > < strong > Mines (M)< / strong > : Place traps in corridors. Triggers when rats step on them, releasing poison gas.< / li >
< p > Move your cursor around the maze and use your arsenal to eliminate rats before they overrun the level:< / p >
< li > < strong > Gas (G)< / strong > : Poison clouds linger and accumulate damage. Great for rat-dense areas.< / li >
< ul >
< li > < strong > Nuclear Bomb (N)< / strong > : One-time use. Destroys all rats on screen instantly.< / li >
< li > < strong > Bombs (Space)< / strong > : Primary weapon. Timer detonates in 4 directions. Chain reactions kill multiple rats.< / li >
< li > < strong > Pause (P)< / strong > : Stop and plan your strategy.< / li >
< li > < strong > Mines (M)< / strong > : Place traps in corridors. Triggers when rats step on them, releasing poison gas.< / li >
< / ul >
< li > < strong > Gas (G)< / strong > : Poison clouds linger and accumulate damage. Great for rat-dense areas.< / li >
< h3 style = "color:#ff9f43;margin-top:15px;" > 💡 Tips & Strategy< / h3 >
< li > < strong > Nuclear Bomb (N)< / strong > : One-time use. Destroys all rats on screen instantly.< / li >
< ul style = "font-size:0.95em;" >
< li > < strong > Pause (P)< / strong > : Stop and plan your strategy.< / li >
< li > Early game: Target adult rats to prevent reproduction (200+ ticks).< / li >
< / ul >
< li > Use walls to direct bomb explosions efficiently.< / li >
< h3 style = "color:#ff9f43;margin-top:15px;" > 💡 Tips & Strategy< / h3 >
< li > Rat population limit: 200. Exceeding this = Game Over.< / li >
< ul style = "font-size:0.95em;" >
< li > Collect bonus points by killing multiple rats with a single bomb.< / li >
< li > Early game: Target adult rats to prevent reproduction (200+ ticks).< / li >
< li > Save the nuclear bomb for when rats get out of control.< / li >
< li > Use walls to direct bomb explosions efficiently.< / li >
< / ul >
< li > Rat population limit: 200. Exceeding this = Game Over.< / li >
< li > Collect bonus points by killing multiple rats with a single bomb.< / li >
< li > Save the nuclear bomb for when rats get out of control.< / li >
< / ul >
< / div >
< / div >
< / div >
< / div >
<!-- Profile Creation Modal (Bootstrap Modal) -->
< div class = "modal fade" id = "profileModal" tabindex = "-1" aria-labelledby = "profileModalLabel" aria-hidden = "true" >
< div class = "modal-dialog" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" id = "profileModalLabel" > Create New Profile< / h5 >
< button type = "button" class = "btn-close btn-close-white" data-bs-dismiss = "modal" aria-label = "Close" > < / button >
< / div >
< div class = "modal-body" >
< form id = "profileForm" >
< div class = "mb-3" >
< label for = "profileName" class = "form-label" > Player Name:< / label >
< input type = "text" id = "profileName" name = "profileName" class = "form-control" placeholder = "Enter your name" required >
< / div >
< div class = "mb-3" >
< label for = "difficulty" class = "form-label" > Difficulty:< / label >
< select id = "difficulty" name = "difficulty" class = "form-select" >
< option value = "normal" > Normal< / option >
< option value = "hard" > Hard< / option >
< option value = "expert" > Expert< / option >
< / select >
< / div >
< div class = "mb-3" >
< div class = "form-check" >
< input type = "checkbox" id = "soundEnabled" name = "soundEnabled" class = "form-check-input" checked >
< label class = "form-check-label" for = "soundEnabled" > Enable Sound< / label >
< / div >
< / div >
< / form >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "btn btn-secondary" data-bs-dismiss = "modal" > Cancel< / button >
< button type = "button" class = "btn" id = "saveProfileBtn" > Create Profile< / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- Bootstrap JS Bundle -->
< script src = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" > < / script >
< script src = "https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js" > < / script >
< script src = "https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js" > < / script >
< script src = "https://ryanking13.github.io/pyodide-pygame-demo/dist/pyodide.js" > < / script >
< script src = "https://ryanking13.github.io/pyodide-pygame-demo/dist/pyodide.js" > < / script >
< script src = "https://code.jquery.com/jquery-3.6.0.min.js" > < / script >
< link rel = "stylesheet" href = "https://code.jquery.com/ui/1.13.2/themes/dark/jquery-ui.css" >
< script src = "https://code.jquery.com/ui/1.13.2/jquery-ui.min.js" > < / script >
< script type = "text/javascript" >
< script type = "text/javascript" >
// Using the original pyodide.js from the pyodide-pygame-demo distribution
// Using the original pyodide.js from the pyodide-pygame-demo distribution
let pyodide = null;
let pyodide = null;
@ -256,6 +477,132 @@
const stopBtn = document.getElementById('stopBtn');
const stopBtn = document.getElementById('stopBtn');
const canvas = document.getElementById('canvas');
const canvas = document.getElementById('canvas');
// Touch Controls - Simulate keyboard events
function simulateKeyPress(key, type = 'keydown') {
// Map keys to proper keyCodes
const keyCodeMap = {
'ArrowUp': 38,
'ArrowDown': 40,
'ArrowLeft': 37,
'ArrowRight': 39,
' ': 32, // Space
'Enter': 13,
'm': 77,
'M': 77,
'g': 71,
'G': 71,
'n': 78,
'N': 78,
'p': 80,
'P': 80
};
const keyCode = keyCodeMap[key] || key.toUpperCase().charCodeAt(0);
// Determine the correct code property
let code;
if (key.startsWith('Arrow')) {
code = key; // ArrowUp, ArrowDown, etc.
} else if (key === ' ') {
code = 'Space';
} else if (key === 'Enter') {
code = 'Enter';
} else {
code = `Key${key.toUpperCase()}`;
}
const event = new KeyboardEvent(type, {
key: key,
code: code,
keyCode: keyCode,
which: keyCode,
bubbles: true,
cancelable: true
});
console.log(`[Touch] Simulating ${type} for key="${key}", code="${code}", keyCode=${keyCode}`);
// Dispatch to both document and canvas
document.dispatchEvent(event);
canvas.dispatchEvent(event);
// Also try dispatching to window for better compatibility
window.dispatchEvent(event);
}
// Setup touch controls
function setupTouchControls() {
// D-Pad and Action buttons
document.querySelectorAll('.dpad-btn, .action-btn').forEach(btn => {
const key = btn.dataset.key;
if (!key) return;
let isPressed = false;
// Touch start - simulate key down
btn.addEventListener('touchstart', (e) => {
e.preventDefault();
if (isPressed) return;
isPressed = true;
btn.style.transform = 'scale(0.95)';
btn.style.opacity = '0.7';
simulateKeyPress(key, 'keydown');
}, { passive: false });
// Touch end - simulate key up
btn.addEventListener('touchend', (e) => {
e.preventDefault();
if (!isPressed) return;
isPressed = false;
btn.style.transform = 'scale(1)';
btn.style.opacity = '1';
simulateKeyPress(key, 'keyup');
}, { passive: false });
// Touch cancel (when finger moves away)
btn.addEventListener('touchcancel', (e) => {
e.preventDefault();
if (!isPressed) return;
isPressed = false;
btn.style.transform = 'scale(1)';
btn.style.opacity = '1';
simulateKeyPress(key, 'keyup');
}, { passive: false });
// Mouse support for testing on desktop
btn.addEventListener('mousedown', (e) => {
e.preventDefault();
if (isPressed) return;
isPressed = true;
btn.style.transform = 'scale(0.95)';
btn.style.opacity = '0.7';
simulateKeyPress(key, 'keydown');
});
btn.addEventListener('mouseup', (e) => {
e.preventDefault();
if (!isPressed) return;
isPressed = false;
btn.style.transform = 'scale(1)';
btn.style.opacity = '1';
simulateKeyPress(key, 'keyup');
});
btn.addEventListener('mouseleave', (e) => {
if (!isPressed) return;
isPressed = false;
btn.style.transform = 'scale(1)';
btn.style.opacity = '1';
simulateKeyPress(key, 'keyup');
});
// Prevent context menu on long press
btn.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
});
}
function updateStatus(message) {
function updateStatus(message) {
statusDiv.textContent = message;
statusDiv.textContent = message;
console.log(message);
console.log(message);
@ -421,16 +768,19 @@ except Exception as e:
}
}
updateStatus('Loading project files into filesystem...');
updateStatus('Loading project files into filesystem...');
const { texts, bins, totalCount } = await loadProjectFiles();
const { texts, bins } = await loadProjectFiles();
// Setup asset progress UI
// Setup asset progress UI
const assetProgress = document.getElementById('assetProgress');
const assetProgress = document.getElementById('assetProgress');
const assetProgressText = document.getElementById('assetProgressText');
const assetProgressText = document.getElementById('assetProgressText');
let writtenCount = 0;
let writtenCount = 0;
assetProgress.max = totalCount || 1;
const totalCount = texts.length + bins.length;
function incrementProgress() {
function incrementProgress() {
writtenCount += 1;
writtenCount += 1;
assetProgress.value = writtenCount;
const percentage = Math.round((writtenCount / totalCount) * 100);
assetProgress.style.width = percentage + '%';
assetProgress.setAttribute('aria-valuenow', percentage);
assetProgressText.textContent = `${writtenCount} / ${totalCount}`;
assetProgressText.textContent = `${writtenCount} / ${totalCount}`;
document.getElementById('hud-status').textContent = `Loading assets ${writtenCount}/${totalCount}`;
document.getElementById('hud-status').textContent = `Loading assets ${writtenCount}/${totalCount}`;
}
}
@ -475,6 +825,9 @@ except Exception as e:
startBtn.disabled = false;
startBtn.disabled = false;
stopBtn.disabled = true;
stopBtn.disabled = true;
// Initialize touch controls
setupTouchControls();
// Populate profile dropdown from project file if available
// Populate profile dropdown from project file if available
try {
try {
const pres = await fetch('user_profiles.json');
const pres = await fetch('user_profiles.json');
@ -712,40 +1065,25 @@ import demo_pygame
populateProfileDropdown();
populateProfileDropdown();
});
});
// Profile management: localStorage + modal
// Profile management: localStorage + Bootstrap modal
let profileModal = null;
function initProfileModal() {
function initProfileModal() {
// Initialize jQuery UI modal dialog
// Initialize Bootstrap modal
$('#profileModal').dialog({
const modalElement = document.getElementById('profileModal');
autoOpen: false,
profileModal = new bootstrap.Modal(modalElement);
modal: true,
buttons: {
// Handle save button click
'Create Profile': function() {
document.getElementById('saveProfileBtn').addEventListener('click', () => {
saveProfile();
saveProfile();
$(this).dialog('close');
profileModal.hide();
},
'Cancel': function() {
$(this).dialog('close');
}
},
dialogClass: 'profile-dialog'
});
});
// Add CSS for dark theme dialog
$('< style > ' )
.text(`
.ui-dialog { background-color: #2d2d2d !important; }
.ui-dialog-titlebar { background-color: #1e1e1e !important; }
.ui-dialog-title { color: #ff9f43 !important; }
.ui-button { background-color: #ff9f43 !important; color: #1e1e1e !important; }
.ui-button:hover { background-color: #ffb366 !important; }
`)
.appendTo('head');
}
}
function saveProfile() {
function saveProfile() {
const name = $('#profileName').val() ;
const name = document.getElementById('profileName').value;
const difficulty = $('#difficulty').val() ;
const difficulty = document.getElementById('difficulty').value;
const soundEnabled = $('#soundEnabled').is(':checked') ;
const soundEnabled = document.getElementById('soundEnabled').checked;
if (!name || name.trim() === '') {
if (!name || name.trim() === '') {
alert('Please enter a player name');
alert('Please enter a player name');
@ -783,7 +1121,7 @@ import demo_pygame
localStorage.setItem('currentProfile', name);
localStorage.setItem('currentProfile', name);
// Reset form
// Reset form
$('#profileForm')[0] .reset();
document.getElementById('profileForm') .reset();
updateStatus(`Profile "${name}" created!`);
updateStatus(`Profile "${name}" created!`);
console.log('Profile saved:', profile);
console.log('Profile saved:', profile);
@ -816,7 +1154,7 @@ import demo_pygame
}
}
document.getElementById('newProfileBtn').addEventListener('click', () => {
document.getElementById('newProfileBtn').addEventListener('click', () => {
$('#profileModal').dialog('open' );
profileModal.show( );
});
});
// Expose a function that Python can call to sync profile updates back to localStorage
// Expose a function that Python can call to sync profile updates back to localStorage
@ -952,10 +1290,8 @@ import demo_pygame
// Initialize on page load
// Initialize on page load
window.addEventListener('load', () => {
window.addEventListener('load', () => {
$(document).ready(() => {
initProfileModal();
initProfileModal();
populateProfileDropdown();
populateProfileDropdown();
});
loadPyodideAndSetup().catch(err => {
loadPyodideAndSetup().catch(err => {
console.error('Failed to initialize Pyodide:', err);
console.error('Failed to initialize Pyodide:', err);
updateStatus('Failed to load Pyodide: ' + err.message);
updateStatus('Failed to load Pyodide: ' + err.message);