Browse Source

Enhance mobile experience and touch controls for Mice! game

- Updated index.html to improve layout responsiveness using Bootstrap 5.
- Added touch controls for mobile devices, including a D-Pad and action buttons.
- Implemented visual feedback for touch interactions with scale animations.
- Created a new test_controls.html for testing touch controls functionality.
- Added a comprehensive MOBILE_README.md detailing mobile features, optimizations, and testing instructions.
- Improved accessibility and usability with user-scalable settings and touch-action prevention.
- Enhanced modal for profile creation with Bootstrap styling.
pygame-pyodide
Matteo Benedetto 2 months ago
parent
commit
43c3b872f2
  1. 105
      MOBILE_README.md
  2. 634
      index.html
  3. 302
      test_controls.html

105
MOBILE_README.md

@ -0,0 +1,105 @@
# 🐭 Mice! - Mobile Version
## Caratteristiche Mobile
Il gioco è stato adattato per dispositivi mobile con le seguenti caratteristiche:
### 📱 Layout Responsive con Bootstrap 5
- **Desktop (≥768px)**: Layout a due colonne con canvas a sinistra e controlli a destra
- **Mobile (<768px)**: Layout verticale con controlli touch sotto il canvas
### 🎮 Controlli Touch
#### Pad Direzionale (D-Pad)
- **Freccia Su**: Movimento in alto
- **Freccia Giù**: Movimento in basso
- **Freccia Sinistra**: Movimento a sinistra
- **Freccia Destra**: Movimento a destra
#### Pulsanti Azione
- **💣 Bomb (Spazio)**: Piazza una bomba
- ** Mine (M)**: Piazza una mina
- ** Gas (G)**: Rilascia gas velenoso
- ** Nuclear (N)**: Bomba nucleare (una volta per partita)
- ** Pause (P)**: Metti in pausa il gioco
### 🎨 Design Adattivo
- Canvas responsive che si adatta alla larghezza dello schermo
- Pulsanti touch ottimizzati per il tocco (60px di altezza minima)
- Feedback visivo su touch (scale animation)
- Interfaccia dark theme ottimizzata per mobile
### 🔧 Ottimizzazioni
- `user-scalable=no` per prevenire lo zoom accidentale
- `touch-action: none` sui controlli per prevenire lo scroll durante il gioco
- Prevenzione del menu contestuale su long press
- Eventi sia touch che mouse per compatibilità con desktop
## Test
Per testare su mobile:
1. Avvia un server web locale:
```bash
python -m http.server 8000
```
2. Apri il browser sul tuo smartphone e naviga a:
```
http://[IL-TUO-IP-LOCALE]:8000/index.html
```
3. Verifica:
- [ ] I controlli touch sono visibili solo su mobile
- [ ] Il D-Pad risponde al tocco
- [ ] I pulsanti azione funzionano correttamente
- [ ] Il canvas si adatta correttamente
- [ ] Non c'è zoom accidentale durante il gioco
## Compatibilità Browser
- ✅ Chrome/Edge Mobile (Android/iOS)
- ✅ Safari Mobile (iOS)
- ✅ Firefox Mobile (Android)
- ✅ Samsung Internet
## Note Tecniche
### Simulazione Eventi Tastiera
I controlli touch simulano eventi `KeyboardEvent` nativi per garantire compatibilità con il codice Python/Pygame esistente:
```javascript
function simulateKeyPress(key, type = 'keydown') {
const event = new KeyboardEvent(type, {
key: key,
code: key === ' ' ? 'Space' : `Key${key.toUpperCase()}`,
keyCode: key.charCodeAt(0),
which: key.charCodeAt(0),
bubbles: true,
cancelable: true
});
document.dispatchEvent(event);
canvas.dispatchEvent(event);
}
```
### Bootstrap Components
- **Grid System**: `container-fluid`, `row`, `col-*` per layout responsive
- **Modal**: Per la creazione profilo
- **Form Controls**: Input, select, checkbox con stili dark
- **Progress Bar**: Per indicare il caricamento degli asset
- **Icons**: Bootstrap Icons per le frecce direzionali
## Miglioramenti Futuri
- [ ] Supporto vibrazione per feedback tattile
- [ ] Joystick virtuale con movimento analogico
- [ ] Gesture swipe per movimento rapido
- [ ] Orientamento landscape automatico su mobile
- [ ] PWA manifest per installazione come app nativa
- [ ] Service worker per gioco offline

634
index.html

@ -2,75 +2,67 @@
<html lang="en">
<head>
<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>
<!-- 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>
body {
margin: 0;
padding: 20px;
padding: 10px;
font-family: Arial, sans-serif;
background-color: #1e1e1e;
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
color: #ff9f43;
margin-bottom: 10px;
}
.container {
max-width: 800px;
width: 100%;
font-size: 1.8rem;
}
#status {
margin: 20px 0;
padding: 10px;
padding: 8px;
background-color: #2d2d2d;
border-radius: 5px;
font-family: monospace;
font-size: 0.85rem;
margin-bottom: 10px;
}
.canvas-container {
margin: 20px 0;
border: 2px solid #ff9f43;
border-radius: 5px;
background-color: #000;
display: inline-block;
width: 100%;
max-width: 640px;
aspect-ratio: 4/3;
position: relative;
}
#canvas {
display: block;
width: 100%;
height: 100%;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.controls {
margin: 20px 0;
}
button {
background-color: #ff9f43;
color: #1e1e1e;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
button, .btn {
background-color: #ff9f43 !important;
color: #1e1e1e !important;
border: none !important;
font-weight: bold;
}
button:hover {
background-color: #ffb366;
button:hover, .btn:hover {
background-color: #ffb366 !important;
}
button:disabled {
background-color: #666;
button:disabled, .btn:disabled {
background-color: #666 !important;
cursor: not-allowed;
}
@ -78,135 +70,322 @@
background-color: #2d2d2d;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
margin: 15px 0;
}
.info h2 {
color: #ff9f43;
margin-top: 0;
font-size: 1.3rem;
}
code {
background-color: #1e1e1e;
padding: 2px 6px;
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;
gap: 20px;
align-items: flex-start;
align-items: center;
justify-content: center;
font-size: 1.5rem;
cursor: pointer;
user-select: none;
touch-action: none;
transition: transform 0.1s ease, opacity 0.1s ease;
}
.game-layout .left {
flex: 1 1 640px;
.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;
}
.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;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 5px;
touch-action: none;
transition: transform 0.1s ease, opacity 0.1s ease;
}
.game-layout .right {
width: 320px;
flex: 0 0 320px;
.action-btn:active {
transform: scale(0.95);
opacity: 0.7;
}
.hud {
margin-top: 8px;
background: rgba(0,0,0,0.6);
padding: 6px 10px;
border-radius: 4px;
.action-btn small {
font-size: 0.7rem;
font-weight: normal;
margin-top: 2px;
}
/* Responsive adjustments */
@media (min-width: 768px) {
.touch-controls {
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;
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; }
.controls { display:flex; flex-wrap:wrap; gap:8px; }
.progress { margin-top: 12px; }
.help { margin-top: 12px; font-size:0.95em; }
input[type="range"] { width: 100%; accent-color: #ff9f43; }
progress { width: 100%; accent-color: #ff9f43; height: 8px; }
select { background-color: #2d2d2d; color: #fff; border: 1px solid #555; padding: 6px; border-radius: 3px; }
details summary { cursor: pointer; color: #ff9f43; font-weight: bold; }
details summary:hover { color: #ffb366; }
.modal-content {
background-color: #2d2d2d;
color: #ffffff;
}
.modal-header {
border-bottom: 1px solid #555;
}
.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>
</head>
<body>
<div class="container">
<div class="container-fluid">
<!-- Header -->
<div class="row">
<div class="col-12 text-center">
<h1>🐭 Mice! - The Game</h1>
<p><strong>Eliminate the rats before they multiply!</strong> A strategic maze game with bombs, mines, and gas.</p>
<p style="font-size:0.9em;color:#aaa;">Browser Edition powered by Python + Pygame via Pyodide</p>
<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>
</div>
</div>
<!-- Status -->
<div class="row mb-2">
<div class="col-12">
<div id="status">Loading Pyodide...</div>
</div>
</div>
<!-- Profile Creation Modal -->
<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>
<!-- 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>
</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 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 style="margin-bottom:15px;">
<label>
<input type="checkbox" id="soundEnabled" name="soundEnabled" checked> Enable Sound
</label>
</div>
</form>
</div>
<div class="game-layout">
<div class="left">
<div class="canvas-container">
<canvas id="canvas" width="640" height="480" aria-label="Mice game canvas"></canvas>
</div>
<div id="hud" class="hud">Status: <span id="hud-status">Idle</span></div>
</div>
<div class="right">
<div class="controls">
<button id="startBtn" disabled>Start Game</button>
<button id="stopBtn" disabled>Stop Game</button>
<button id="fullscreenBtn">Fullscreen</button>
<button id="muteBtn">Mute</button>
<!-- Controls Sidebar -->
<div class="col-lg-4 col-12">
<!-- Game Controls -->
<div class="mb-3">
<div class="d-flex flex-wrap gap-2">
<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 class="control-row">
<label for="volumeRange">Volume</label>
<input id="volumeRange" type="range" min="0" max="100" value="100">
<!-- Volume Control -->
<div class="mb-3">
<label for="volumeRange" class="form-label">Volume</label>
<input id="volumeRange" type="range" class="form-range" min="0" max="100" value="100">
</div>
<div class="control-row">
<label for="profileSelect">Profile</label>
<select id="profileSelect"><option value="">(None)</option></select>
<button id="loadProfileBtn">Load</button>
<!-- Profile Selection -->
<div class="mb-3">
<label for="profileSelect" class="form-label">Profile</label>
<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 class="control-row">
<button id="newProfileBtn" style="flex:1;">+ New Profile</button>
<div class="mb-3">
<button id="newProfileBtn" class="btn w-100">+ New Profile</button>
</div>
<div class="progress">
<label>Asset load progress</label>
<progress id="assetProgress" value="0" max="100"></progress>
<div id="assetProgressText">0 / 0</div>
<!-- Progress -->
<div class="mb-3">
<label class="form-label">Asset Load Progress</label>
<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>
<details>
<!-- Advanced Options -->
<details class="mb-3">
<summary>Advanced</summary>
<div style="margin-top:8px;">
<button id="runDemoBtn">Run Pygame Demo</button>
<div style="margin-top:8px;font-size:0.9em;color:#ddd;">Useful for testing the canvas rendering when debugging.</div>
<div class="mt-2">
<button id="runDemoBtn" class="btn w-100">Run Pygame Demo</button>
<small class="d-block mt-2 text-muted">Useful for testing the canvas rendering when debugging.</small>
</div>
</details>
<div class="help">
<h3>Controls</h3>
<ul>
<!-- Keyboard Controls Help (Desktop Only) -->
<div class="help d-none d-lg-block">
<h5>Keyboard Controls</h5>
<ul class="small">
<li>Arrow keys / WASD — move</li>
<li>Space — bomb</li>
<li>M — mine</li>
@ -218,6 +397,9 @@
</div>
</div>
<!-- Info Section -->
<div class="row mt-3">
<div class="col-12">
<div class="info">
<h2>🎮 How to Play</h2>
<p>Move your cursor around the maze and use your arsenal to eliminate rats before they overrun the level:</p>
@ -238,12 +420,51 @@
</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>
<!-- 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://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">
// Using the original pyodide.js from the pyodide-pygame-demo distribution
let pyodide = null;
@ -256,6 +477,132 @@
const stopBtn = document.getElementById('stopBtn');
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) {
statusDiv.textContent = message;
console.log(message);
@ -421,16 +768,19 @@ except Exception as e:
}
updateStatus('Loading project files into filesystem...');
const { texts, bins, totalCount } = await loadProjectFiles();
const { texts, bins } = await loadProjectFiles();
// Setup asset progress UI
const assetProgress = document.getElementById('assetProgress');
const assetProgressText = document.getElementById('assetProgressText');
let writtenCount = 0;
assetProgress.max = totalCount || 1;
const totalCount = texts.length + bins.length;
function incrementProgress() {
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}`;
document.getElementById('hud-status').textContent = `Loading assets ${writtenCount}/${totalCount}`;
}
@ -475,6 +825,9 @@ except Exception as e:
startBtn.disabled = false;
stopBtn.disabled = true;
// Initialize touch controls
setupTouchControls();
// Populate profile dropdown from project file if available
try {
const pres = await fetch('user_profiles.json');
@ -712,40 +1065,25 @@ import demo_pygame
populateProfileDropdown();
});
// Profile management: localStorage + modal
// Profile management: localStorage + Bootstrap modal
let profileModal = null;
function initProfileModal() {
// Initialize jQuery UI modal dialog
$('#profileModal').dialog({
autoOpen: false,
modal: true,
buttons: {
'Create Profile': function() {
// Initialize Bootstrap modal
const modalElement = document.getElementById('profileModal');
profileModal = new bootstrap.Modal(modalElement);
// Handle save button click
document.getElementById('saveProfileBtn').addEventListener('click', () => {
saveProfile();
$(this).dialog('close');
},
'Cancel': function() {
$(this).dialog('close');
}
},
dialogClass: 'profile-dialog'
profileModal.hide();
});
// 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() {
const name = $('#profileName').val();
const difficulty = $('#difficulty').val();
const soundEnabled = $('#soundEnabled').is(':checked');
const name = document.getElementById('profileName').value;
const difficulty = document.getElementById('difficulty').value;
const soundEnabled = document.getElementById('soundEnabled').checked;
if (!name || name.trim() === '') {
alert('Please enter a player name');
@ -783,7 +1121,7 @@ import demo_pygame
localStorage.setItem('currentProfile', name);
// Reset form
$('#profileForm')[0].reset();
document.getElementById('profileForm').reset();
updateStatus(`Profile "${name}" created!`);
console.log('Profile saved:', profile);
@ -816,7 +1154,7 @@ import demo_pygame
}
document.getElementById('newProfileBtn').addEventListener('click', () => {
$('#profileModal').dialog('open');
profileModal.show();
});
// 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
window.addEventListener('load', () => {
$(document).ready(() => {
initProfileModal();
populateProfileDropdown();
});
loadPyodideAndSetup().catch(err => {
console.error('Failed to initialize Pyodide:', err);
updateStatus('Failed to load Pyodide: ' + err.message);

302
test_controls.html

@ -0,0 +1,302 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Test Touch Controls</title>
<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>
body {
background-color: #1e1e1e;
color: white;
padding: 20px;
}
.dpad-container {
position: relative;
width: 160px;
height: 160px;
margin: 20px auto;
}
.dpad-btn {
position: absolute;
width: 50px;
height: 50px;
background-color: #ff9f43;
border: 2px solid #1e1e1e;
border-radius: 8px;
display: flex;
align-items: center;
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-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;
}
.action-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
max-width: 300px;
margin: 20px auto;
}
.action-btn {
height: 55px;
font-size: 0.95rem;
background-color: #ff9f43 !important;
color: #1e1e1e !important;
border: none !important;
font-weight: bold;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 5px;
touch-action: none;
transition: transform 0.1s ease, opacity 0.1s ease;
}
.action-btn small {
font-size: 0.7rem;
font-weight: normal;
margin-top: 2px;
}
#log {
background-color: #000;
border: 1px solid #555;
border-radius: 5px;
padding: 10px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 0.85rem;
margin-top: 20px;
}
.log-entry {
margin: 2px 0;
}
.log-keydown { color: #4ade80; }
.log-keyup { color: #fbbf24; }
</style>
</head>
<body>
<div class="container">
<h1 class="text-center mb-4">🎮 Test Touch Controls</h1>
<div class="row">
<div class="col-md-6">
<h5 class="text-center text-muted mb-2">Movement</h5>
<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-md-6">
<h5 class="text-center text-muted mb-2">Actions</h5>
<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 id="log"></div>
<button class="btn btn-danger mt-2" onclick="clearLog()">Clear Log</button>
</div>
<script>
const logDiv = document.getElementById('log');
let logCount = 0;
function addLog(message, type) {
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.textContent = `[${++logCount}] ${message}`;
logDiv.appendChild(entry);
logDiv.scrollTop = logDiv.scrollHeight;
// Keep only last 50 entries
while (logDiv.children.length > 50) {
logDiv.removeChild(logDiv.firstChild);
}
}
function clearLog() {
logDiv.innerHTML = '';
logCount = 0;
}
// Listen to keyboard events
document.addEventListener('keydown', (e) => {
addLog(`KeyDown: key="${e.key}", code="${e.code}", keyCode=${e.keyCode}`, 'keydown');
});
document.addEventListener('keyup', (e) => {
addLog(`KeyUp: key="${e.key}", code="${e.code}", keyCode=${e.keyCode}`, 'keyup');
});
// Simulate keyboard events
function simulateKeyPress(key, type = 'keydown') {
const keyCodeMap = {
'ArrowUp': 38,
'ArrowDown': 40,
'ArrowLeft': 37,
'ArrowRight': 39,
' ': 32,
'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);
let code;
if (key.startsWith('Arrow')) {
code = key;
} 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
});
document.dispatchEvent(event);
window.dispatchEvent(event);
}
// Setup touch controls
function setupTouchControls() {
document.querySelectorAll('.dpad-btn, .action-btn').forEach(btn => {
const key = btn.dataset.key;
if (!key) return;
let isPressed = false;
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 });
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 });
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 });
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');
});
btn.addEventListener('contextmenu', (e) => {
e.preventDefault();
});
});
}
// Initialize
setupTouchControls();
addLog('Touch controls initialized. Try pressing buttons!', 'keydown');
</script>
</body>
</html>
Loading…
Cancel
Save