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.
This commit is contained in:
@@ -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
|
||||
+515
-179
@@ -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,36 +70,21 @@
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.game-layout .left {
|
||||
flex: 1 1 640px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.game-layout .right {
|
||||
width: 320px;
|
||||
flex: 0 0 320px;
|
||||
}
|
||||
|
||||
.hud {
|
||||
margin-top: 8px;
|
||||
background: rgba(0,0,0,0.6);
|
||||
@@ -115,98 +92,300 @@
|
||||
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;
|
||||
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-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;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
.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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<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>
|
||||
|
||||
<div id="status">Loading Pyodide...</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>
|
||||
</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 class="container-fluid">
|
||||
<!-- Header -->
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<h1>🐭 Mice! - The Game</h1>
|
||||
<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>
|
||||
|
||||
<div class="game-layout">
|
||||
<div class="left">
|
||||
<div class="canvas-container">
|
||||
|
||||
<!-- Status -->
|
||||
<div class="row mb-2">
|
||||
<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>
|
||||
</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 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>
|
||||
@@ -217,33 +396,75 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<ul>
|
||||
<li><strong>Bombs (Space)</strong>: Primary weapon. Timer detonates in 4 directions. Chain reactions kill multiple rats.</li>
|
||||
<li><strong>Mines (M)</strong>: Place traps in corridors. Triggers when rats step on them, releasing poison gas.</li>
|
||||
<li><strong>Gas (G)</strong>: Poison clouds linger and accumulate damage. Great for rat-dense areas.</li>
|
||||
<li><strong>Nuclear Bomb (N)</strong>: One-time use. Destroys all rats on screen instantly.</li>
|
||||
<li><strong>Pause (P)</strong>: Stop and plan your strategy.</li>
|
||||
</ul>
|
||||
<h3 style="color:#ff9f43;margin-top:15px;">💡 Tips & Strategy</h3>
|
||||
<ul style="font-size:0.95em;">
|
||||
<li>Early game: Target adult rats to prevent reproduction (200+ ticks).</li>
|
||||
<li>Use walls to direct bomb explosions efficiently.</li>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<ul>
|
||||
<li><strong>Bombs (Space)</strong>: Primary weapon. Timer detonates in 4 directions. Chain reactions kill multiple rats.</li>
|
||||
<li><strong>Mines (M)</strong>: Place traps in corridors. Triggers when rats step on them, releasing poison gas.</li>
|
||||
<li><strong>Gas (G)</strong>: Poison clouds linger and accumulate damage. Great for rat-dense areas.</li>
|
||||
<li><strong>Nuclear Bomb (N)</strong>: One-time use. Destroys all rats on screen instantly.</li>
|
||||
<li><strong>Pause (P)</strong>: Stop and plan your strategy.</li>
|
||||
</ul>
|
||||
<h3 style="color:#ff9f43;margin-top:15px;">💡 Tips & Strategy</h3>
|
||||
<ul style="font-size:0.95em;">
|
||||
<li>Early game: Target adult rats to prevent reproduction (200+ ticks).</li>
|
||||
<li>Use walls to direct bomb explosions efficiently.</li>
|
||||
<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>
|
||||
|
||||
<!-- 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
|
||||
function initProfileModal() {
|
||||
// Initialize jQuery UI modal dialog
|
||||
$('#profileModal').dialog({
|
||||
autoOpen: false,
|
||||
modal: true,
|
||||
buttons: {
|
||||
'Create Profile': function() {
|
||||
saveProfile();
|
||||
$(this).dialog('close');
|
||||
},
|
||||
'Cancel': function() {
|
||||
$(this).dialog('close');
|
||||
}
|
||||
},
|
||||
dialogClass: 'profile-dialog'
|
||||
});
|
||||
// Profile management: localStorage + Bootstrap modal
|
||||
let profileModal = null;
|
||||
|
||||
// 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 initProfileModal() {
|
||||
// Initialize Bootstrap modal
|
||||
const modalElement = document.getElementById('profileModal');
|
||||
profileModal = new bootstrap.Modal(modalElement);
|
||||
|
||||
// Handle save button click
|
||||
document.getElementById('saveProfileBtn').addEventListener('click', () => {
|
||||
saveProfile();
|
||||
profileModal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
initProfileModal();
|
||||
populateProfileDropdown();
|
||||
loadPyodideAndSetup().catch(err => {
|
||||
console.error('Failed to initialize Pyodide:', err);
|
||||
updateStatus('Failed to load Pyodide: ' + err.message);
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user