chore: mobile improvments mvp
a3505575
5 file(s) · +129 −19
| 23 | 23 | </head> |
|
| 24 | 24 | <body> |
|
| 25 | 25 | <div id="game-container"></div> |
|
| 26 | + | <div id="cheatsheet"> |
|
| 27 | + | <span>SPACE / UP / TAP — jump</span> |
|
| 28 | + | <span>DOWN / 2-FINGER TAP — duck</span> |
|
| 29 | + | <span>M — mute</span> |
|
| 30 | + | </div> |
|
| 31 | + | <div id="touch-zones"> |
|
| 32 | + | <div id="touch-duck">DUCK</div> |
|
| 33 | + | <div id="touch-jump">JUMP</div> |
|
| 34 | + | </div> |
|
| 26 | 35 | <script type="module" src="./src/index.js"></script> |
|
| 27 | 36 | </body> |
|
| 28 | 37 | </html> |
| 70 | 70 | ||
| 71 | 71 | this.gameToken = null |
|
| 72 | 72 | this.onGameOver = null |
|
| 73 | + | this.onGameStart = null |
|
| 73 | 74 | } |
|
| 74 | 75 | ||
| 75 | 76 | createCanvas(width, height) { |
|
| 190 | 191 | value: 0, |
|
| 191 | 192 | }, |
|
| 192 | 193 | }) |
|
| 194 | + | ||
| 195 | + | if (typeof this.onGameStart === 'function') { |
|
| 196 | + | this.onGameStart() |
|
| 197 | + | } |
|
| 193 | 198 | ||
| 194 | 199 | this.start() |
|
| 195 | 200 | } |
|
| 1 | 1 | import DinoGame from './game/DinoGame.js' |
|
| 2 | + | import { toggleMute } from './sounds.js' |
|
| 2 | 3 | ||
| 3 | 4 | const game = new DinoGame(600, 150, document.getElementById('game-container')) |
|
| 4 | 5 | ||
| 75 | 76 | } |
|
| 76 | 77 | }) |
|
| 77 | 78 | ||
| 78 | - | // Touch to dismiss overlay (tap background) |
|
| 79 | + | // Touch to dismiss overlay (tap background) - only closes modal, doesn't restart |
|
| 79 | 80 | overlay.addEventListener('click', (e) => { |
|
| 80 | 81 | if (e.target === overlay && overlayState.visible) { |
|
| 81 | 82 | hideOverlay() |
|
| 82 | - | game.onInput('jump') |
|
| 83 | 83 | } |
|
| 84 | 84 | }) |
|
| 85 | 85 | ||
| 281 | 281 | } |
|
| 282 | 282 | } |
|
| 283 | 283 | ||
| 284 | + | const cheatsheet = document.getElementById('cheatsheet') |
|
| 285 | + | let cheatsheetTimer = null |
|
| 286 | + | ||
| 287 | + | game.onGameStart = () => { |
|
| 288 | + | cheatsheet.classList.remove('hidden') |
|
| 289 | + | touchZones.classList.remove('hidden') |
|
| 290 | + | clearTimeout(cheatsheetTimer) |
|
| 291 | + | cheatsheetTimer = setTimeout(() => { |
|
| 292 | + | cheatsheet.classList.add('hidden') |
|
| 293 | + | touchZones.classList.add('hidden') |
|
| 294 | + | }, 2000) |
|
| 295 | + | } |
|
| 296 | + | ||
| 284 | 297 | game.onGameOver = (score, tokenPromise) => { |
|
| 285 | 298 | setTimeout(() => showOverlay(score, tokenPromise), 300) |
|
| 286 | 299 | } |
|
| 293 | 306 | game.onInput('duck') |
|
| 294 | 307 | } |
|
| 295 | 308 | } else { |
|
| 296 | - | // Escape key closes modal |
|
| 309 | + | // Escape key closes modal only (must press Play Again to restart) |
|
| 297 | 310 | if (e.key === 'Escape') { |
|
| 298 | 311 | hideOverlay() |
|
| 299 | - | game.onInput('jump') |
|
| 300 | 312 | return |
|
| 301 | 313 | } |
|
| 302 | - | // Space/Enter closes only if not in input field |
|
| 314 | + | // Space/Up closes modal only if not in input field (must press Play Again to restart) |
|
| 303 | 315 | if (document.activeElement !== initialsInput && keycodes.JUMP[e.keyCode]) { |
|
| 304 | 316 | hideOverlay() |
|
| 305 | - | game.onInput('jump') |
|
| 306 | 317 | } |
|
| 307 | 318 | } |
|
| 308 | 319 | }) |
|
| 313 | 324 | } |
|
| 314 | 325 | }) |
|
| 315 | 326 | ||
| 316 | - | document.addEventListener( |
|
| 327 | + | const touchZones = document.getElementById('touch-zones') |
|
| 328 | + | const touchDuck = document.getElementById('touch-duck') |
|
| 329 | + | const touchJump = document.getElementById('touch-jump') |
|
| 330 | + | ||
| 331 | + | touchDuck.addEventListener( |
|
| 317 | 332 | 'touchstart', |
|
| 318 | 333 | (e) => { |
|
| 319 | - | if (!overlayState.visible) { |
|
| 320 | - | e.preventDefault() |
|
| 321 | - | if (e.touches.length === 1) { |
|
| 322 | - | game.onInput('jump') |
|
| 323 | - | } else if (e.touches.length === 2) { |
|
| 324 | - | game.onInput('duck') |
|
| 325 | - | } |
|
| 326 | - | } |
|
| 334 | + | e.preventDefault() |
|
| 335 | + | if (!overlayState.visible) game.onInput('duck') |
|
| 327 | 336 | }, |
|
| 328 | 337 | { passive: false } |
|
| 329 | 338 | ) |
|
| 330 | 339 | ||
| 331 | - | document.addEventListener('touchend', (e) => { |
|
| 332 | - | if (!overlayState.visible) { |
|
| 333 | - | game.onInput('stop-duck') |
|
| 340 | + | touchDuck.addEventListener('touchend', (e) => { |
|
| 341 | + | if (!overlayState.visible) game.onInput('stop-duck') |
|
| 342 | + | }) |
|
| 343 | + | ||
| 344 | + | touchJump.addEventListener( |
|
| 345 | + | 'touchstart', |
|
| 346 | + | (e) => { |
|
| 347 | + | e.preventDefault() |
|
| 348 | + | if (!overlayState.visible) game.onInput('jump') |
|
| 349 | + | }, |
|
| 350 | + | { passive: false } |
|
| 351 | + | ) |
|
| 352 | + | ||
| 353 | + | // Mute toggle with 'M' key |
|
| 354 | + | document.addEventListener('keydown', (e) => { |
|
| 355 | + | if (e.key === 'm' || e.key === 'M') { |
|
| 356 | + | toggleMute() |
|
| 334 | 357 | } |
|
| 335 | 358 | }) |
|
| 336 | 359 | ||
| 337 | 360 | game.start().catch(console.error) |
|
| 361 | + | loadLeaderboard(false) |
|
| 3 | 3 | const soundNames = ['game-over', 'jump', 'level-up'] |
|
| 4 | 4 | const soundBuffers = {} |
|
| 5 | 5 | let SOUNDS_LOADED = false |
|
| 6 | + | let muted = localStorage.getItem('dino-muted') === 'true' |
|
| 6 | 7 | ||
| 7 | 8 | loadSounds().catch(console.error) |
|
| 8 | 9 | export function playSound(name) { |
|
| 9 | - | if (SOUNDS_LOADED) { |
|
| 10 | + | if (SOUNDS_LOADED && !muted) { |
|
| 10 | 11 | audioContext.resume() |
|
| 11 | 12 | playBuffer(soundBuffers[name]) |
|
| 12 | 13 | } |
|
| 14 | + | } |
|
| 15 | + | ||
| 16 | + | export function isMuted() { |
|
| 17 | + | return muted |
|
| 18 | + | } |
|
| 19 | + | ||
| 20 | + | export function toggleMute() { |
|
| 21 | + | muted = !muted |
|
| 22 | + | localStorage.setItem('dino-muted', muted) |
|
| 23 | + | return muted |
|
| 13 | 24 | } |
|
| 14 | 25 | ||
| 15 | 26 | async function loadSounds() { |
| 14 | 14 | body { |
|
| 15 | 15 | background-color: #121113; |
|
| 16 | 16 | display: flex; |
|
| 17 | + | flex-direction: column; |
|
| 17 | 18 | align-items: center; |
|
| 18 | 19 | justify-content: center; |
|
| 19 | 20 | } |
|
| 182 | 183 | .error { |
|
| 183 | 184 | color: #e85a5a; |
|
| 184 | 185 | } |
|
| 186 | + | ||
| 187 | + | #cheatsheet { |
|
| 188 | + | display: flex; |
|
| 189 | + | flex-direction: column; |
|
| 190 | + | justify-content: center; |
|
| 191 | + | gap: 24px; |
|
| 192 | + | padding: 12px 16px; |
|
| 193 | + | font-family: 'PressStart2P', monospace; |
|
| 194 | + | font-size: 8px; |
|
| 195 | + | color: #4a4a4b; |
|
| 196 | + | letter-spacing: 0.5px; |
|
| 197 | + | padding-top: 24px; |
|
| 198 | + | opacity: 1; |
|
| 199 | + | transition: opacity 0.6s ease; |
|
| 200 | + | } |
|
| 201 | + | ||
| 202 | + | #cheatsheet.hidden { |
|
| 203 | + | opacity: 0; |
|
| 204 | + | } |
|
| 205 | + | ||
| 206 | + | #touch-zones { |
|
| 207 | + | display: none; |
|
| 208 | + | position: fixed; |
|
| 209 | + | bottom: 0; |
|
| 210 | + | left: 0; |
|
| 211 | + | right: 0; |
|
| 212 | + | top: 60%; |
|
| 213 | + | gap: 1px; |
|
| 214 | + | opacity: 1; |
|
| 215 | + | transition: opacity 0.6s ease; |
|
| 216 | + | } |
|
| 217 | + | ||
| 218 | + | #touch-zones.hidden { |
|
| 219 | + | opacity: 0; |
|
| 220 | + | pointer-events: none; |
|
| 221 | + | } |
|
| 222 | + | ||
| 223 | + | #touch-zones > div { |
|
| 224 | + | flex: 1; |
|
| 225 | + | display: flex; |
|
| 226 | + | align-items: center; |
|
| 227 | + | justify-content: center; |
|
| 228 | + | border: 2px solid #3a3a3b; |
|
| 229 | + | border-radius: 4px; |
|
| 230 | + | font-family: 'PressStart2P', monospace; |
|
| 231 | + | font-size: 10px; |
|
| 232 | + | color: #4a4a4b; |
|
| 233 | + | user-select: none; |
|
| 234 | + | -webkit-user-select: none; |
|
| 235 | + | } |
|
| 236 | + | ||
| 237 | + | @media (pointer: coarse) { |
|
| 238 | + | #cheatsheet { |
|
| 239 | + | display: none; |
|
| 240 | + | } |
|
| 241 | + | ||
| 242 | + | #touch-zones { |
|
| 243 | + | display: flex; |
|
| 244 | + | } |
|
| 245 | + | } |
|