chore: mobile improvments mvp a3505575
Steve · 2026-03-27 19:57 5 file(s) · +129 −19
index.html +9 −0
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>
src/game/DinoGame.js +5 −0
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
  }
src/index.js +42 −18
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)
src/sounds.js +12 −1
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() {
src/style.css +61 −0
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 +
}