diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 5633a9794..44e40c25f 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -860,7 +860,8 @@ "seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz", "search": { "placeholder": "Type to guess...", - "button": "Guess", + "guess": "Guess", + "next": "Next", "no_results": "No results" } }, diff --git a/packages/website/src/pages/[...locale]/tools/guess/_index.tsx b/packages/website/src/pages/[...locale]/tools/guess/_index.tsx index 7ef48b173..3c72e704b 100644 --- a/packages/website/src/pages/[...locale]/tools/guess/_index.tsx +++ b/packages/website/src/pages/[...locale]/tools/guess/_index.tsx @@ -1,16 +1,19 @@ import { Box, Flex } from '@radix-ui/themes'; +import { Suspense } from 'react'; import { awaitableModelDefinitions } from '../../../../core/awaitables/modelDefinitions'; import { awaitableProvisionDefinitions } from '../../../../core/awaitables/provisionDefinitions'; import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions'; -import { Var } from '../../../../core/radix/var'; import { LocaleProvider, type LocaleAcceptorProps, } from '../../../../hooks/useLocale'; import { Duel } from '../../../../stores/duel'; +import { GuessEphemeral } from '../../../../stores/guessEphemeral'; import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral'; -import { GuessRenderer } from './components/GuessRenderer'; +import { Background } from './components/Background'; import { Guesser } from './components/Guesser'; +import { GuessRenderer } from './components/GuessRenderer'; +import { GuessRendererLoader } from './components/GuessRendererLoader'; const [tankDefinitions, modelDefinitions, provisionDefinitions] = await Promise.all([ @@ -22,52 +25,40 @@ const [tankDefinitions, modelDefinitions, provisionDefinitions] = const ids = Object.keys(tankDefinitions.tanks); export function Page({ locale }: LocaleAcceptorProps) { - const id = Number(ids[Math.floor(Math.random() * ids.length)]); - const tank = tankDefinitions.tanks[id]; - const model = modelDefinitions.models[id]; + const initialId = Number(ids[Math.floor(Math.random() * ids.length)]); + const initialTank = tankDefinitions.tanks[initialId]; return ( - - - - - - - + + + + + + ); +} + +function Page2() { + const tank = GuessEphemeral.use((state) => state.tank); + const model = modelDefinitions.models[tank.id]; + + return ( + + + + + ); } function Content() { return ( - - + + - + }> + + diff --git a/packages/website/src/pages/[...locale]/tools/guess/components/Background/index.css b/packages/website/src/pages/[...locale]/tools/guess/components/Background/index.css new file mode 100644 index 000000000..27600f511 --- /dev/null +++ b/packages/website/src/pages/[...locale]/tools/guess/components/Background/index.css @@ -0,0 +1,14 @@ +.guess-background { + animation: moveBackground 180s linear infinite; +} + +@keyframes moveBackground { + from { + top: 0; + left: 0; + } + to { + top: -100%; + left: -100%; + } +} diff --git a/packages/website/src/pages/[...locale]/tools/guess/components/Background/index.tsx b/packages/website/src/pages/[...locale]/tools/guess/components/Background/index.tsx new file mode 100644 index 000000000..009574341 --- /dev/null +++ b/packages/website/src/pages/[...locale]/tools/guess/components/Background/index.tsx @@ -0,0 +1,76 @@ +import { Box } from '@radix-ui/themes'; +import { useEffect } from 'react'; +import { awaitableModelDefinitions } from '../../../../../../core/awaitables/modelDefinitions'; +import { awaitableProvisionDefinitions } from '../../../../../../core/awaitables/provisionDefinitions'; +import { tankToDuelMember } from '../../../../../../core/blitzkit/tankToDuelMember'; +import { Var } from '../../../../../../core/radix/var'; +import { Duel } from '../../../../../../stores/duel'; +import { GuessEphemeral } from '../../../../../../stores/guessEphemeral'; +import { TankopediaEphemeral } from '../../../../../../stores/tankopediaEphemeral'; +import './index.css'; + +const [modelDefinitions, provisionDefinitions] = await Promise.all([ + awaitableModelDefinitions, + awaitableProvisionDefinitions, +]); + +export function Background() { + const state = GuessEphemeral.use((state) => state.guessState); + const tank = GuessEphemeral.use((state) => state.tank); + const mutateDuel = Duel.useMutation(); + const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); + + useEffect(() => { + mutateDuel((state) => { + const model = modelDefinitions.models[tank.id]; + state.protagonist = tankToDuelMember(tank, model, provisionDefinitions); + }); + mutateTankopediaEphemeral((state) => { + state.model = modelDefinitions.models[tank.id]; + }); + }, [tank]); + + return ( + <> + + + + + + + ); +} diff --git a/packages/website/src/pages/[...locale]/tools/guess/components/GuessRenderer.tsx b/packages/website/src/pages/[...locale]/tools/guess/components/GuessRenderer.tsx index 9efb18f3a..266bff97e 100644 --- a/packages/website/src/pages/[...locale]/tools/guess/components/GuessRenderer.tsx +++ b/packages/website/src/pages/[...locale]/tools/guess/components/GuessRenderer.tsx @@ -19,6 +19,13 @@ export const revealEvent = new Quicklime(); export function GuessRenderer() { const fog = useRef(new Fog('black', nearConcealed, farConcealed)); const reveal = useCallback((show: boolean) => { + if (!show) { + fog.current.near = nearConcealed; + fog.current.far = farConcealed; + invalidate(); + return; + } + const t0 = Date.now() / 1000; function frame() { diff --git a/packages/website/src/pages/[...locale]/tools/guess/components/GuessRendererLoader.tsx b/packages/website/src/pages/[...locale]/tools/guess/components/GuessRendererLoader.tsx new file mode 100644 index 000000000..da9479e91 --- /dev/null +++ b/packages/website/src/pages/[...locale]/tools/guess/components/GuessRendererLoader.tsx @@ -0,0 +1,14 @@ +import { Box, Spinner } from '@radix-ui/themes'; + +export function GuessRendererLoader() { + return ( + + + + ); +} diff --git a/packages/website/src/pages/[...locale]/tools/guess/components/Guesser.tsx b/packages/website/src/pages/[...locale]/tools/guess/components/Guesser.tsx index 17bb18d07..9f20b20d9 100644 --- a/packages/website/src/pages/[...locale]/tools/guess/components/Guesser.tsx +++ b/packages/website/src/pages/[...locale]/tools/guess/components/Guesser.tsx @@ -3,7 +3,11 @@ import { TankDefinition, TIER_ROMAN_NUMERALS, } from '@blitzkit/core'; -import { MagnifyingGlassIcon, PaperPlaneIcon } from '@radix-ui/react-icons'; +import { + ArrowRightIcon, + MagnifyingGlassIcon, + PaperPlaneIcon, +} from '@radix-ui/react-icons'; import { Box, Button, @@ -21,6 +25,7 @@ import { SearchResults } from '../../../../../components/SearchResults'; import { awaitableTankDefinitions } from '../../../../../core/awaitables/tankDefinitions'; import { awaitableTankNames } from '../../../../../core/awaitables/tankNames'; import { useLocale } from '../../../../../hooks/useLocale'; +import { GuessEphemeral } from '../../../../../stores/guessEphemeral'; import { revealEvent } from './GuessRenderer'; const { go } = fuzzysort; @@ -30,12 +35,17 @@ const [tankNames, tankDefinitions] = await Promise.all([ awaitableTankDefinitions, ]); +const ids = Object.keys(tankDefinitions.tanks); + export function Guesser() { + const tank = GuessEphemeral.use((state) => state.tank); + const guessState = GuessEphemeral.use((state) => state.guessState); const { strings, unwrap } = useLocale(); const input = useRef(null); const [searching, setSearching] = useState(false); const [results, setResults] = useState(null); const [selected, setSelected] = useState(null); + const mutateGuessEphemeral = GuessEphemeral.useMutation(); const requestSearch = useCallback(() => { setSelected(null); @@ -124,6 +134,7 @@ export function Guesser() { { - revealEvent.dispatch(true); + if (guessState === null) { + console.log('what'); + const correct = selected!.id === tank.id; + + revealEvent.dispatch(true); + mutateGuessEphemeral((draft) => { + draft.guessState = correct; + draft.totalSeen++; + draft.correct += correct ? 1 : 0; + }); + } else { + const id = Number(ids[Math.floor(Math.random() * ids.length)]); + const tank = tankDefinitions.tanks[id]; + + revealEvent.dispatch(false); + mutateGuessEphemeral((draft) => { + if (!input.current) return; + + draft.tank = tank; + draft.guessState = null; + input.current.value = ''; + }); + } }} > - {strings.website.tools.guess.search.button} + {guessState === null ? ( + <> + {strings.website.tools.guess.search.guess} + + + ) : ( + <> + {strings.website.tools.guess.search.next} + + + )} diff --git a/packages/website/src/stores/guessEphemeral.ts b/packages/website/src/stores/guessEphemeral.ts new file mode 100644 index 000000000..106f481be --- /dev/null +++ b/packages/website/src/stores/guessEphemeral.ts @@ -0,0 +1,20 @@ +import type { TankDefinition } from '@blitzkit/core'; +import { create } from 'zustand'; +import { createContextualStore } from '../core/zustand/createContextualStore'; + +export interface GuessEphemeral { + tank: TankDefinition; + // true = correct, false = incorrect, null = not guessed + guessState: boolean | null; + totalSeen: number; + correct: number; +} + +export const GuessEphemeral = createContextualStore((tank: TankDefinition) => + create()(() => ({ + tank, + guessState: null, + totalSeen: 0, + correct: 0, + })), +);