mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-05 23:36:07 -04:00
Add translations and update "Whack-A-Bug" game with localization support, refined UI, and improved game mechanics
This commit is contained in:
@@ -1358,5 +1358,19 @@
|
||||
"vote.toast.created": "Vorschlag erstellt",
|
||||
"vote.urgent": "Dringend",
|
||||
"vote.voted": "Stimme gespeichert",
|
||||
"vote.with_priority": "mit Priorität"
|
||||
"vote.with_priority": "mit Priorität",
|
||||
"game.whackabug.title": "WHACK-A-BUG",
|
||||
"game.whackabug.subtitle": "// die Schuld für den Fehler",
|
||||
"game.whackabug.instructions_lives": "Du hast {lives} Leben. Entkommende Bugs = Leben verloren.",
|
||||
"game.whackabug.instructions_time": "{seconds} Sekunden. Zerstöre so viele wie möglich.",
|
||||
"game.whackabug.victory_title": "// SIEG — Zeit abgelaufen",
|
||||
"game.whackabug.victory_message": "Du findest immer noch Bugs in freier Wildbahn? Kontaktiere uns und wir werden sie aufspüren.",
|
||||
"game.whackabug.play_again": "NOCHMAL SPIELEN",
|
||||
"game.whackabug.gameover_title": "// GAME OVER — alle Bugs entkommen",
|
||||
"game.whackabug.gameover_message": "Wenn die Bugs zu viel werden, kontaktiere uns und wir erledigen sie für dich.",
|
||||
"game.whackabug.retry": "WIEDERHOLEN",
|
||||
"game.whackabug.start_game": "SPIEL STARTEN",
|
||||
"game.whackabug.time": "ZEIT",
|
||||
"game.whackabug.seo_title": "Whack-A-Bug",
|
||||
"game.whackabug.seo_description": "Schiebe die Schuld für den Fehler auf die Bugs - ein lustiges Spiel"
|
||||
}
|
||||
|
||||
@@ -1357,5 +1357,19 @@
|
||||
"vote.toast.created": "Proposition créée",
|
||||
"vote.urgent": "Urgente",
|
||||
"vote.voted": "Vote enregistré",
|
||||
"vote.with_priority": "avec priorité"
|
||||
"vote.with_priority": "avec priorité",
|
||||
"game.whackabug.title": "WHACK-A-BUG",
|
||||
"game.whackabug.subtitle": "// blâmez les bugs pour l'erreur",
|
||||
"game.whackabug.instructions_lives": "Vous avez {lives} vies. Bugs qui s'échappent = vie perdue.",
|
||||
"game.whackabug.instructions_time": "{seconds} secondes. Écrasez-en autant que possible.",
|
||||
"game.whackabug.victory_title": "// VICTOIRE — le temps est écoulé",
|
||||
"game.whackabug.victory_message": "Vous trouvez toujours des bugs dans la nature? Contactez-nous et nous les traquerons.",
|
||||
"game.whackabug.play_again": "REJOUER",
|
||||
"game.whackabug.gameover_title": "// GAME OVER — tous les bugs se sont échappés",
|
||||
"game.whackabug.gameover_message": "Si les bugs sont trop difficiles à gérer, contactez-nous et nous les écraserons pour vous.",
|
||||
"game.whackabug.retry": "RÉESSAYER",
|
||||
"game.whackabug.start_game": "DÉMARRER",
|
||||
"game.whackabug.time": "TEMPS",
|
||||
"game.whackabug.seo_title": "Whack-A-Bug",
|
||||
"game.whackabug.seo_description": "Blâmez les bugs pour l'erreur - un jeu amusant"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Component, ErrorInfo, ReactNode} from 'react'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import WhackABug from 'web/components/game/whack-a-bug'
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode
|
||||
@@ -61,6 +62,9 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
Go home
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-w-lg mx-auto mt-4">
|
||||
<WhackABug />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import {debug} from 'common/logger'
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
const GRID_SIZE = 9
|
||||
const MAX_LIVES = 3
|
||||
const GAME_DURATION = 30
|
||||
const GAME_DURATION = 20
|
||||
|
||||
const BUG_TYPES = [
|
||||
{icon: '🐛', label: 'worm', duration: 1600},
|
||||
@@ -72,7 +73,7 @@ const css = `
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
background: #0d170d;
|
||||
background: canvas-100;
|
||||
border: 1px solid #1f3a1f;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
@@ -85,7 +86,7 @@ const css = `
|
||||
.wab-timer-bar-wrap {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
background: #0d170d;
|
||||
background: canvas-100;
|
||||
border: 1px solid #1f3a1f;
|
||||
border-radius: 4px;
|
||||
height: 6px;
|
||||
@@ -109,7 +110,7 @@ const css = `
|
||||
|
||||
.wab-hole {
|
||||
aspect-ratio: 1;
|
||||
background: #050c05;
|
||||
background: canvas-100;
|
||||
border: 2px solid #1a2e1a;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
@@ -222,6 +223,7 @@ const css = `
|
||||
`
|
||||
|
||||
export default function WhackABug() {
|
||||
const t = useT()
|
||||
const [gameState, setGameState] = useState('idle')
|
||||
const [lives, setLives] = useState(MAX_LIVES)
|
||||
const [timeLeft, setTimeLeft] = useState(GAME_DURATION)
|
||||
@@ -233,6 +235,9 @@ export default function WhackABug() {
|
||||
const spawnRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const livesRef = useRef(MAX_LIVES)
|
||||
const gameRef = useRef('idle')
|
||||
// Tracks IDs of bugs that were successfully whacked — checked synchronously
|
||||
// in disappear timeouts to avoid the async state-updater mutation race.
|
||||
const whackedIdsRef = useRef(new Set<number>())
|
||||
|
||||
const clearAll = useCallback(() => {
|
||||
timersRef.current.forEach((t) => clearTimeout(t))
|
||||
@@ -248,11 +253,20 @@ export default function WhackABug() {
|
||||
setHoles(Array(GRID_SIZE).fill(null))
|
||||
}, [clearAll])
|
||||
|
||||
const wonGame = useCallback(() => {
|
||||
gameRef.current = 'won'
|
||||
setGameState('won')
|
||||
clearAll()
|
||||
setHoles(Array(GRID_SIZE).fill(null))
|
||||
}, [clearAll])
|
||||
|
||||
const spawnBug = useCallback(() => {
|
||||
if (gameRef.current !== 'playing') return
|
||||
|
||||
const type = BUG_TYPES[Math.floor(Math.random() * BUG_TYPES.length)]
|
||||
const index = Math.floor(Math.random() * GRID_SIZE)
|
||||
// Stable ID created once per spawn so the disappear timeout can reference it
|
||||
const bugId = Date.now() + Math.random()
|
||||
|
||||
setHoles((prev) => {
|
||||
if (prev[index]) {
|
||||
@@ -260,26 +274,24 @@ export default function WhackABug() {
|
||||
return prev
|
||||
}
|
||||
const next = [...prev]
|
||||
next[index] = {type, id: Date.now() + Math.random(), whacked: false}
|
||||
next[index] = {type, id: bugId, whacked: false}
|
||||
return next
|
||||
})
|
||||
|
||||
const disappear = setTimeout(() => {
|
||||
if (gameRef.current !== 'playing') return
|
||||
|
||||
let wasWhacked = false
|
||||
// If this specific bug was whacked, its ID is in the set — skip penalty.
|
||||
if (whackedIdsRef.current.has(bugId)) return
|
||||
|
||||
// Bug escaped: clear the hole and deduct a life.
|
||||
setHoles((prev) => {
|
||||
if (!prev[index] || prev[index].whacked) {
|
||||
wasWhacked = true
|
||||
return prev
|
||||
}
|
||||
if (!prev[index] || prev[index].id !== bugId) return prev
|
||||
const next = [...prev]
|
||||
next[index] = null
|
||||
return next
|
||||
})
|
||||
|
||||
if (wasWhacked) return
|
||||
|
||||
livesRef.current -= 1
|
||||
setLives(livesRef.current)
|
||||
setEffects((prev) => [...prev, {id: Date.now(), index, type: 'miss'}])
|
||||
@@ -299,6 +311,7 @@ export default function WhackABug() {
|
||||
clearAll()
|
||||
livesRef.current = MAX_LIVES
|
||||
gameRef.current = 'playing'
|
||||
whackedIdsRef.current.clear()
|
||||
setLives(MAX_LIVES)
|
||||
setTimeLeft(GAME_DURATION)
|
||||
setHoles(Array(GRID_SIZE).fill(null))
|
||||
@@ -308,7 +321,7 @@ export default function WhackABug() {
|
||||
timerRef.current = setInterval(() => {
|
||||
setTimeLeft((t) => {
|
||||
if (t <= 1) {
|
||||
endGame()
|
||||
wonGame()
|
||||
return 0
|
||||
}
|
||||
return t - 1
|
||||
@@ -316,13 +329,15 @@ export default function WhackABug() {
|
||||
}, 1000)
|
||||
|
||||
spawnRef.current = setTimeout(spawnBug, 400)
|
||||
}, [clearAll, endGame, spawnBug])
|
||||
}, [clearAll, endGame, spawnBug, wonGame])
|
||||
|
||||
const whack = useCallback((index: number) => {
|
||||
debug('Wacked')
|
||||
setHoles((prev) => {
|
||||
const hole = prev[index]
|
||||
if (!hole || hole.whacked) return prev
|
||||
// Register this bug as whacked before the disappear timeout can fire
|
||||
whackedIdsRef.current.add(hole.id)
|
||||
const next = [...prev]
|
||||
next[index] = {...hole, whacked: true}
|
||||
const t = setTimeout(() => {
|
||||
@@ -353,6 +368,12 @@ export default function WhackABug() {
|
||||
})
|
||||
}, [effects])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
startGame()
|
||||
}, 5000)
|
||||
}, [])
|
||||
|
||||
const pct = (timeLeft / GAME_DURATION) * 100
|
||||
const barClass = pct <= 20 ? 'crit' : pct <= 40 ? 'warn' : ''
|
||||
|
||||
@@ -360,8 +381,10 @@ export default function WhackABug() {
|
||||
<>
|
||||
<style>{css}</style>
|
||||
<div className="wab-wrap">
|
||||
<h2 className="wab-title">WHACK-A-BUG</h2>
|
||||
<p className="wab-subtitle">// blame the bugs for the error</p>
|
||||
<h2 className="wab-title">{t('game.whackabug.title', 'WHACK-A-BUG')}</h2>
|
||||
<p className="wab-subtitle">
|
||||
{t('game.whackabug.subtitle', '// blame the bugs for the error')}
|
||||
</p>
|
||||
|
||||
{
|
||||
<div className="wab-idle">
|
||||
@@ -373,23 +396,54 @@ export default function WhackABug() {
|
||||
))}
|
||||
</div>
|
||||
<p className={'text-ink-1000/75 text-sm'}>
|
||||
You have {MAX_LIVES} lives. Bugs escape = lost life.
|
||||
{t(
|
||||
'game.whackabug.instructions_lives',
|
||||
'You have {lives} lives. Bugs escape = lost life.',
|
||||
{lives: MAX_LIVES},
|
||||
)}
|
||||
<br />
|
||||
30 seconds. Smash as many as you can.
|
||||
{t(
|
||||
'game.whackabug.instructions_time',
|
||||
'{seconds} seconds. Smash as many as you can.',
|
||||
{seconds: GAME_DURATION},
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
{gameState === 'gameover' ? (
|
||||
{gameState === 'won' ? (
|
||||
<div className="wab-gameover">
|
||||
<p style={{color: '#ff4444', fontSize: 14}}>// GAME OVER — all bugs escaped</p>
|
||||
<p style={{color: '#39ff14', fontSize: 14}}>
|
||||
{t('game.whackabug.victory_title', '// VICTORY — time is up')}
|
||||
</p>
|
||||
<p className={'guidance'}>
|
||||
{t(
|
||||
'game.whackabug.victory_message',
|
||||
"Still finding bugs in the wild? Contact us and we'll track them down.",
|
||||
)}
|
||||
</p>
|
||||
<button className="wab-btn" onClick={startGame}>
|
||||
RETRY
|
||||
{t('game.whackabug.play_again', 'PLAY AGAIN')}
|
||||
</button>
|
||||
</div>
|
||||
) : gameState === 'gameover' ? (
|
||||
<div className="wab-gameover">
|
||||
<p style={{color: '#ff4444', fontSize: 14}}>
|
||||
{t('game.whackabug.gameover_title', '// GAME OVER — all bugs escaped')}
|
||||
</p>
|
||||
<p className={'guidance'}>
|
||||
{t(
|
||||
'game.whackabug.gameover_message',
|
||||
"If the bugs are too much to handle, contact us and we'll squash them for you.",
|
||||
)}
|
||||
</p>
|
||||
<button className="wab-btn" onClick={startGame}>
|
||||
{t('game.whackabug.retry', 'RETRY')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button className="wab-btn" onClick={startGame}>
|
||||
START GAME
|
||||
{t('game.whackabug.start_game', 'START GAME')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -402,7 +456,7 @@ export default function WhackABug() {
|
||||
))}
|
||||
</div>
|
||||
<div style={{textAlign: 'right'}}>
|
||||
<span className="wab-hud-label">TIME</span>
|
||||
<span className="wab-hud-label">{t('game.whackabug.time', 'TIME')}</span>
|
||||
<span
|
||||
className="wab-hud-val"
|
||||
style={{color: timeLeft <= 6 ? '#ff4444' : '#39ff14'}}
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
import WhackABug from 'web/components/game/whack-a-bug'
|
||||
import {PageBase} from 'web/components/page-base'
|
||||
import {SEO} from 'web/components/SEO'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
export default function WhackABugPage() {
|
||||
const t = useT()
|
||||
return (
|
||||
<PageBase trackPageView={'whack-a-bug'} className="relative p-2 sm:pt-0">
|
||||
<SEO
|
||||
title="Whack-A-Bug"
|
||||
description="Blame the bugs for the error - a fun game"
|
||||
title={t('game.whackabug.seo_title', 'Whack-A-Bug')}
|
||||
description={t(
|
||||
'game.whackabug.seo_description',
|
||||
'Blame the bugs for the error - a fun game',
|
||||
)}
|
||||
url="/whack-a-bug"
|
||||
/>
|
||||
<div className="max-w-lg mx-auto">
|
||||
|
||||
Reference in New Issue
Block a user