Skip to content

Commit ad0cf2e

Browse files
committed
Add translations and update "Whack-A-Bug" game with localization support, refined UI, and improved game mechanics
1 parent d9c7ffa commit ad0cf2e

5 files changed

Lines changed: 118 additions & 27 deletions

File tree

common/messages/de.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1358,5 +1358,19 @@
13581358
"vote.toast.created": "Vorschlag erstellt",
13591359
"vote.urgent": "Dringend",
13601360
"vote.voted": "Stimme gespeichert",
1361-
"vote.with_priority": "mit Priorität"
1361+
"vote.with_priority": "mit Priorität",
1362+
"game.whackabug.title": "WHACK-A-BUG",
1363+
"game.whackabug.subtitle": "// die Schuld für den Fehler",
1364+
"game.whackabug.instructions_lives": "Du hast {lives} Leben. Entkommende Bugs = Leben verloren.",
1365+
"game.whackabug.instructions_time": "{seconds} Sekunden. Zerstöre so viele wie möglich.",
1366+
"game.whackabug.victory_title": "// SIEG — Zeit abgelaufen",
1367+
"game.whackabug.victory_message": "Du findest immer noch Bugs in freier Wildbahn? Kontaktiere uns und wir werden sie aufspüren.",
1368+
"game.whackabug.play_again": "NOCHMAL SPIELEN",
1369+
"game.whackabug.gameover_title": "// GAME OVER — alle Bugs entkommen",
1370+
"game.whackabug.gameover_message": "Wenn die Bugs zu viel werden, kontaktiere uns und wir erledigen sie für dich.",
1371+
"game.whackabug.retry": "WIEDERHOLEN",
1372+
"game.whackabug.start_game": "SPIEL STARTEN",
1373+
"game.whackabug.time": "ZEIT",
1374+
"game.whackabug.seo_title": "Whack-A-Bug",
1375+
"game.whackabug.seo_description": "Schiebe die Schuld für den Fehler auf die Bugs - ein lustiges Spiel"
13621376
}

common/messages/fr.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1357,5 +1357,19 @@
13571357
"vote.toast.created": "Proposition créée",
13581358
"vote.urgent": "Urgente",
13591359
"vote.voted": "Vote enregistré",
1360-
"vote.with_priority": "avec priorité"
1360+
"vote.with_priority": "avec priorité",
1361+
"game.whackabug.title": "WHACK-A-BUG",
1362+
"game.whackabug.subtitle": "// blâmez les bugs pour l'erreur",
1363+
"game.whackabug.instructions_lives": "Vous avez {lives} vies. Bugs qui s'échappent = vie perdue.",
1364+
"game.whackabug.instructions_time": "{seconds} secondes. Écrasez-en autant que possible.",
1365+
"game.whackabug.victory_title": "// VICTOIRE — le temps est écoulé",
1366+
"game.whackabug.victory_message": "Vous trouvez toujours des bugs dans la nature? Contactez-nous et nous les traquerons.",
1367+
"game.whackabug.play_again": "REJOUER",
1368+
"game.whackabug.gameover_title": "// GAME OVER — tous les bugs se sont échappés",
1369+
"game.whackabug.gameover_message": "Si les bugs sont trop difficiles à gérer, contactez-nous et nous les écraserons pour vous.",
1370+
"game.whackabug.retry": "RÉESSAYER",
1371+
"game.whackabug.start_game": "DÉMARRER",
1372+
"game.whackabug.time": "TEMPS",
1373+
"game.whackabug.seo_title": "Whack-A-Bug",
1374+
"game.whackabug.seo_description": "Blâmez les bugs pour l'erreur - un jeu amusant"
13611375
}

web/components/error-boundary.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Component, ErrorInfo, ReactNode} from 'react'
22
import {Button} from 'web/components/buttons/button'
3+
import WhackABug from 'web/components/game/whack-a-bug'
34

45
interface Props {
56
children?: ReactNode
@@ -61,6 +62,9 @@ export class ErrorBoundary extends Component<Props, State> {
6162
Go home
6263
</Button>
6364
</div>
65+
<div className="max-w-lg mx-auto mt-4">
66+
<WhackABug />
67+
</div>
6468
</div>
6569
</div>
6670
)

web/components/game/whack-a-bug.tsx

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import {debug} from 'common/logger'
44
import {useCallback, useEffect, useRef, useState} from 'react'
5+
import {useT} from 'web/lib/locale'
56

67
const GRID_SIZE = 9
78
const MAX_LIVES = 3
8-
const GAME_DURATION = 30
9+
const GAME_DURATION = 20
910

1011
const BUG_TYPES = [
1112
{icon: '🐛', label: 'worm', duration: 1600},
@@ -72,7 +73,7 @@ const css = `
7273
align-items: center;
7374
width: 100%;
7475
max-width: 380px;
75-
background: #0d170d;
76+
background: canvas-100;
7677
border: 1px solid #1f3a1f;
7778
border-radius: 8px;
7879
padding: 8px 16px;
@@ -85,7 +86,7 @@ const css = `
8586
.wab-timer-bar-wrap {
8687
width: 100%;
8788
max-width: 380px;
88-
background: #0d170d;
89+
background: canvas-100;
8990
border: 1px solid #1f3a1f;
9091
border-radius: 4px;
9192
height: 6px;
@@ -109,7 +110,7 @@ const css = `
109110
110111
.wab-hole {
111112
aspect-ratio: 1;
112-
background: #050c05;
113+
background: canvas-100;
113114
border: 2px solid #1a2e1a;
114115
border-radius: 50%;
115116
display: flex;
@@ -222,6 +223,7 @@ const css = `
222223
`
223224

224225
export default function WhackABug() {
226+
const t = useT()
225227
const [gameState, setGameState] = useState('idle')
226228
const [lives, setLives] = useState(MAX_LIVES)
227229
const [timeLeft, setTimeLeft] = useState(GAME_DURATION)
@@ -233,6 +235,9 @@ export default function WhackABug() {
233235
const spawnRef = useRef<ReturnType<typeof setTimeout> | null>(null)
234236
const livesRef = useRef(MAX_LIVES)
235237
const gameRef = useRef('idle')
238+
// Tracks IDs of bugs that were successfully whacked — checked synchronously
239+
// in disappear timeouts to avoid the async state-updater mutation race.
240+
const whackedIdsRef = useRef(new Set<number>())
236241

237242
const clearAll = useCallback(() => {
238243
timersRef.current.forEach((t) => clearTimeout(t))
@@ -248,38 +253,45 @@ export default function WhackABug() {
248253
setHoles(Array(GRID_SIZE).fill(null))
249254
}, [clearAll])
250255

256+
const wonGame = useCallback(() => {
257+
gameRef.current = 'won'
258+
setGameState('won')
259+
clearAll()
260+
setHoles(Array(GRID_SIZE).fill(null))
261+
}, [clearAll])
262+
251263
const spawnBug = useCallback(() => {
252264
if (gameRef.current !== 'playing') return
253265

254266
const type = BUG_TYPES[Math.floor(Math.random() * BUG_TYPES.length)]
255267
const index = Math.floor(Math.random() * GRID_SIZE)
268+
// Stable ID created once per spawn so the disappear timeout can reference it
269+
const bugId = Date.now() + Math.random()
256270

257271
setHoles((prev) => {
258272
if (prev[index]) {
259273
scheduleNext()
260274
return prev
261275
}
262276
const next = [...prev]
263-
next[index] = {type, id: Date.now() + Math.random(), whacked: false}
277+
next[index] = {type, id: bugId, whacked: false}
264278
return next
265279
})
266280

267281
const disappear = setTimeout(() => {
268282
if (gameRef.current !== 'playing') return
269283

270-
let wasWhacked = false
284+
// If this specific bug was whacked, its ID is in the set — skip penalty.
285+
if (whackedIdsRef.current.has(bugId)) return
286+
287+
// Bug escaped: clear the hole and deduct a life.
271288
setHoles((prev) => {
272-
if (!prev[index] || prev[index].whacked) {
273-
wasWhacked = true
274-
return prev
275-
}
289+
if (!prev[index] || prev[index].id !== bugId) return prev
276290
const next = [...prev]
277291
next[index] = null
278292
return next
279293
})
280294

281-
if (wasWhacked) return
282-
283295
livesRef.current -= 1
284296
setLives(livesRef.current)
285297
setEffects((prev) => [...prev, {id: Date.now(), index, type: 'miss'}])
@@ -299,6 +311,7 @@ export default function WhackABug() {
299311
clearAll()
300312
livesRef.current = MAX_LIVES
301313
gameRef.current = 'playing'
314+
whackedIdsRef.current.clear()
302315
setLives(MAX_LIVES)
303316
setTimeLeft(GAME_DURATION)
304317
setHoles(Array(GRID_SIZE).fill(null))
@@ -308,21 +321,23 @@ export default function WhackABug() {
308321
timerRef.current = setInterval(() => {
309322
setTimeLeft((t) => {
310323
if (t <= 1) {
311-
endGame()
324+
wonGame()
312325
return 0
313326
}
314327
return t - 1
315328
})
316329
}, 1000)
317330

318331
spawnRef.current = setTimeout(spawnBug, 400)
319-
}, [clearAll, endGame, spawnBug])
332+
}, [clearAll, endGame, spawnBug, wonGame])
320333

321334
const whack = useCallback((index: number) => {
322335
debug('Wacked')
323336
setHoles((prev) => {
324337
const hole = prev[index]
325338
if (!hole || hole.whacked) return prev
339+
// Register this bug as whacked before the disappear timeout can fire
340+
whackedIdsRef.current.add(hole.id)
326341
const next = [...prev]
327342
next[index] = {...hole, whacked: true}
328343
const t = setTimeout(() => {
@@ -353,15 +368,23 @@ export default function WhackABug() {
353368
})
354369
}, [effects])
355370

371+
useEffect(() => {
372+
setTimeout(() => {
373+
startGame()
374+
}, 5000)
375+
}, [])
376+
356377
const pct = (timeLeft / GAME_DURATION) * 100
357378
const barClass = pct <= 20 ? 'crit' : pct <= 40 ? 'warn' : ''
358379

359380
return (
360381
<>
361382
<style>{css}</style>
362383
<div className="wab-wrap">
363-
<h2 className="wab-title">WHACK-A-BUG</h2>
364-
<p className="wab-subtitle">// blame the bugs for the error</p>
384+
<h2 className="wab-title">{t('game.whackabug.title', 'WHACK-A-BUG')}</h2>
385+
<p className="wab-subtitle">
386+
{t('game.whackabug.subtitle', '// blame the bugs for the error')}
387+
</p>
365388

366389
{
367390
<div className="wab-idle">
@@ -373,23 +396,54 @@ export default function WhackABug() {
373396
))}
374397
</div>
375398
<p className={'text-ink-1000/75 text-sm'}>
376-
You have {MAX_LIVES} lives. Bugs escape = lost life.
399+
{t(
400+
'game.whackabug.instructions_lives',
401+
'You have {lives} lives. Bugs escape = lost life.',
402+
{lives: MAX_LIVES},
403+
)}
377404
<br />
378-
30 seconds. Smash as many as you can.
405+
{t(
406+
'game.whackabug.instructions_time',
407+
'{seconds} seconds. Smash as many as you can.',
408+
{seconds: GAME_DURATION},
409+
)}
379410
</p>
380411
</div>
381412
}
382413

383-
{gameState === 'gameover' ? (
414+
{gameState === 'won' ? (
384415
<div className="wab-gameover">
385-
<p style={{color: '#ff4444', fontSize: 14}}>// GAME OVER — all bugs escaped</p>
416+
<p style={{color: '#39ff14', fontSize: 14}}>
417+
{t('game.whackabug.victory_title', '// VICTORY — time is up')}
418+
</p>
419+
<p className={'guidance'}>
420+
{t(
421+
'game.whackabug.victory_message',
422+
"Still finding bugs in the wild? Contact us and we'll track them down.",
423+
)}
424+
</p>
425+
<button className="wab-btn" onClick={startGame}>
426+
{t('game.whackabug.play_again', 'PLAY AGAIN')}
427+
</button>
428+
</div>
429+
) : gameState === 'gameover' ? (
430+
<div className="wab-gameover">
431+
<p style={{color: '#ff4444', fontSize: 14}}>
432+
{t('game.whackabug.gameover_title', '// GAME OVER — all bugs escaped')}
433+
</p>
434+
<p className={'guidance'}>
435+
{t(
436+
'game.whackabug.gameover_message',
437+
"If the bugs are too much to handle, contact us and we'll squash them for you.",
438+
)}
439+
</p>
386440
<button className="wab-btn" onClick={startGame}>
387-
RETRY
441+
{t('game.whackabug.retry', 'RETRY')}
388442
</button>
389443
</div>
390444
) : (
391445
<button className="wab-btn" onClick={startGame}>
392-
START GAME
446+
{t('game.whackabug.start_game', 'START GAME')}
393447
</button>
394448
)}
395449

@@ -402,7 +456,7 @@ export default function WhackABug() {
402456
))}
403457
</div>
404458
<div style={{textAlign: 'right'}}>
405-
<span className="wab-hud-label">TIME</span>
459+
<span className="wab-hud-label">{t('game.whackabug.time', 'TIME')}</span>
406460
<span
407461
className="wab-hud-val"
408462
style={{color: timeLeft <= 6 ? '#ff4444' : '#39ff14'}}

web/pages/whack-a-bug.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33
import WhackABug from 'web/components/game/whack-a-bug'
44
import {PageBase} from 'web/components/page-base'
55
import {SEO} from 'web/components/SEO'
6+
import {useT} from 'web/lib/locale'
67

78
export default function WhackABugPage() {
9+
const t = useT()
810
return (
911
<PageBase trackPageView={'whack-a-bug'} className="relative p-2 sm:pt-0">
1012
<SEO
11-
title="Whack-A-Bug"
12-
description="Blame the bugs for the error - a fun game"
13+
title={t('game.whackabug.seo_title', 'Whack-A-Bug')}
14+
description={t(
15+
'game.whackabug.seo_description',
16+
'Blame the bugs for the error - a fun game',
17+
)}
1318
url="/whack-a-bug"
1419
/>
1520
<div className="max-w-lg mx-auto">

0 commit comments

Comments
 (0)