feat: full logic

This commit is contained in:
tresabhi committed 2025-05-04 21:39:09 -05:00
1 parent 232f24181f
commit c8cb490b48
8 files changed
+210 -44

No files matched your search

+2 -1
View File
@@ -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"
}
},
@@ -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 (
<LocaleProvider locale={locale}>
<TankopediaEphemeral.Provider data={model}>
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
<Content />
</Duel.Provider>
</TankopediaEphemeral.Provider>
</LocaleProvider>
<GuessEphemeral.Provider data={initialTank}>
<LocaleProvider locale={locale}>
<Page2 />
</LocaleProvider>
</GuessEphemeral.Provider>
);
}
function Page2() {
const tank = GuessEphemeral.use((state) => state.tank);
const model = modelDefinitions.models[tank.id];
return (
<TankopediaEphemeral.Provider data={model}>
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
<Content />
</Duel.Provider>
</TankopediaEphemeral.Provider>
);
}
function Content() {
return (
<Flex
flexGrow="1"
position="relative"
style={{
backgroundColor: Var('cyan-3'),
}}
>
<Box
position="absolute"
width="100%"
height="100%"
top="0"
left="0"
style={{
backgroundImage: `
radial-gradient(circle, ${Var('white-a1')} 20%, transparent 20%),
radial-gradient(circle at 0 0, ${Var('white-a1')} 10%, transparent 10%),
radial-gradient(circle at 100% 0, ${Var('white-a1')} 10%, transparent 10%),
radial-gradient(circle at 0 100%, ${Var('white-a1')} 10%, transparent 10%),
radial-gradient(circle at 100% 100%, ${Var('white-a1')} 10%, transparent 10%)
`,
backgroundSize: '4rem 4rem',
// background: `radial-gradient(circle, ${Var('cyan-a1')} 15%, ${Var('cyan-a3')}, ${Var('cyan-a1')}, ${Var('gray-1')} 50%)`,
// background: `linear-gradient(90deg, ${Var('black-a3')}, ${Var('black-a9')} 66%)`,
}}
/>
<Flex flexGrow="1" position="relative" overflow="hidden">
<Background />
<Box position="absolute" width="100%" height="100%" top="0" left="0">
<GuessRenderer />
<Suspense fallback={<GuessRendererLoader />}>
<GuessRenderer />
</Suspense>
</Box>
<Guesser />
@@ -0,0 +1,14 @@
.guess-background {
animation: moveBackground 180s linear infinite;
}
@keyframes moveBackground {
from {
top: 0;
left: 0;
}
to {
top: -100%;
left: -100%;
}
}
@@ -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 (
<>
<Box
position="absolute"
width="100%"
height="100%"
top="0"
left="0"
style={{
backgroundColor: Var(`${state ? 'jade' : 'tomato'}-3`),
}}
>
<Box
width="100%"
height="100%"
style={{
opacity: state === null ? 1 : 0,
transitionDuration: '200ms',
backgroundColor: Var('cyan-3'),
}}
/>
</Box>
<Box
className="guess-background"
position="absolute"
width="200%"
height="200%"
top="0"
left="0"
style={{
backgroundImage: `
radial-gradient(circle, ${Var('white-a1')} 20%, transparent 20%),
radial-gradient(circle at 0 0, ${Var('white-a1')} 10%, transparent 10%),
radial-gradient(circle at 100% 0, ${Var('white-a1')} 10%, transparent 10%),
radial-gradient(circle at 0 100%, ${Var('white-a1')} 10%, transparent 10%),
radial-gradient(circle at 100% 100%, ${Var('white-a1')} 10%, transparent 10%)
`,
backgroundSize: '4rem 4rem',
}}
/>
</>
);
}
@@ -19,6 +19,13 @@ export const revealEvent = new Quicklime<boolean>();
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() {
@@ -0,0 +1,14 @@
import { Box, Spinner } from '@radix-ui/themes';
export function GuessRendererLoader() {
return (
<Box
position="absolute"
top="50%"
left="50%"
style={{ transform: 'translate(-50%, -50%)' }}
>
<Spinner size="3" />
</Box>
);
}
@@ -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<HTMLInputElement>(null);
const [searching, setSearching] = useState(false);
const [results, setResults] = useState<TankDefinition[] | null>(null);
const [selected, setSelected] = useState<TankDefinition | null>(null);
const mutateGuessEphemeral = GuessEphemeral.useMutation();
const requestSearch = useCallback(() => {
setSelected(null);
@@ -124,6 +134,7 @@ export function Guesser() {
<Flex gap="3">
<TextField.Root
disabled={guessState !== null}
ref={input}
onChange={requestSearch}
style={{ flex: 1 }}
@@ -138,12 +149,44 @@ export function Guesser() {
<Button
size="3"
disabled={selected === null}
disabled={guessState === null && selected === null}
onClick={() => {
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} <PaperPlaneIcon />
{guessState === null ? (
<>
{strings.website.tools.guess.search.guess}
<PaperPlaneIcon />
</>
) : (
<>
{strings.website.tools.guess.search.next}
<ArrowRightIcon />
</>
)}
</Button>
</Flex>
</Flex>
@@ -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<GuessEphemeral>()(() => ({
tank,
guessState: null,
totalSeen: 0,
correct: 0,
})),
);