mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: full logic
This commit is contained in:
1 parent
232f24181f
commit
c8cb490b48
8 files changed
+210
-44
No files matched your search
@@ -860,7 +860,8 @@
|
|||||||
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
"seo_description": "Play a tank guessing game with a silhouette in World of Tanks Blitz",
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Type to guess...",
|
"placeholder": "Type to guess...",
|
||||||
"button": "Guess",
|
"guess": "Guess",
|
||||||
|
"next": "Next",
|
||||||
"no_results": "No results"
|
"no_results": "No results"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { Box, Flex } from '@radix-ui/themes';
|
import { Box, Flex } from '@radix-ui/themes';
|
||||||
|
import { Suspense } from 'react';
|
||||||
import { awaitableModelDefinitions } from '../../../../core/awaitables/modelDefinitions';
|
import { awaitableModelDefinitions } from '../../../../core/awaitables/modelDefinitions';
|
||||||
import { awaitableProvisionDefinitions } from '../../../../core/awaitables/provisionDefinitions';
|
import { awaitableProvisionDefinitions } from '../../../../core/awaitables/provisionDefinitions';
|
||||||
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from '../../../../core/awaitables/tankDefinitions';
|
||||||
import { Var } from '../../../../core/radix/var';
|
|
||||||
import {
|
import {
|
||||||
LocaleProvider,
|
LocaleProvider,
|
||||||
type LocaleAcceptorProps,
|
type LocaleAcceptorProps,
|
||||||
} from '../../../../hooks/useLocale';
|
} from '../../../../hooks/useLocale';
|
||||||
import { Duel } from '../../../../stores/duel';
|
import { Duel } from '../../../../stores/duel';
|
||||||
|
import { GuessEphemeral } from '../../../../stores/guessEphemeral';
|
||||||
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||||
import { GuessRenderer } from './components/GuessRenderer';
|
import { Background } from './components/Background';
|
||||||
import { Guesser } from './components/Guesser';
|
import { Guesser } from './components/Guesser';
|
||||||
|
import { GuessRenderer } from './components/GuessRenderer';
|
||||||
|
import { GuessRendererLoader } from './components/GuessRendererLoader';
|
||||||
|
|
||||||
const [tankDefinitions, modelDefinitions, provisionDefinitions] =
|
const [tankDefinitions, modelDefinitions, provisionDefinitions] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -22,52 +25,40 @@ const [tankDefinitions, modelDefinitions, provisionDefinitions] =
|
|||||||
const ids = Object.keys(tankDefinitions.tanks);
|
const ids = Object.keys(tankDefinitions.tanks);
|
||||||
|
|
||||||
export function Page({ locale }: LocaleAcceptorProps) {
|
export function Page({ locale }: LocaleAcceptorProps) {
|
||||||
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
const initialId = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||||
const tank = tankDefinitions.tanks[id];
|
const initialTank = tankDefinitions.tanks[initialId];
|
||||||
const model = modelDefinitions.models[id];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocaleProvider locale={locale}>
|
<GuessEphemeral.Provider data={initialTank}>
|
||||||
<TankopediaEphemeral.Provider data={model}>
|
<LocaleProvider locale={locale}>
|
||||||
<Duel.Provider data={{ tank, model, provisionDefinitions }}>
|
<Page2 />
|
||||||
<Content />
|
</LocaleProvider>
|
||||||
</Duel.Provider>
|
</GuessEphemeral.Provider>
|
||||||
</TankopediaEphemeral.Provider>
|
);
|
||||||
</LocaleProvider>
|
}
|
||||||
|
|
||||||
|
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() {
|
function Content() {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex flexGrow="1" position="relative" overflow="hidden">
|
||||||
flexGrow="1"
|
<Background />
|
||||||
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%)`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
<Box position="absolute" width="100%" height="100%" top="0" left="0">
|
||||||
<GuessRenderer />
|
<Suspense fallback={<GuessRendererLoader />}>
|
||||||
|
<GuessRenderer />
|
||||||
|
</Suspense>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Guesser />
|
<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() {
|
export function GuessRenderer() {
|
||||||
const fog = useRef(new Fog('black', nearConcealed, farConcealed));
|
const fog = useRef(new Fog('black', nearConcealed, farConcealed));
|
||||||
const reveal = useCallback((show: boolean) => {
|
const reveal = useCallback((show: boolean) => {
|
||||||
|
if (!show) {
|
||||||
|
fog.current.near = nearConcealed;
|
||||||
|
fog.current.far = farConcealed;
|
||||||
|
invalidate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const t0 = Date.now() / 1000;
|
const t0 = Date.now() / 1000;
|
||||||
|
|
||||||
function frame() {
|
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,
|
TankDefinition,
|
||||||
TIER_ROMAN_NUMERALS,
|
TIER_ROMAN_NUMERALS,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { MagnifyingGlassIcon, PaperPlaneIcon } from '@radix-ui/react-icons';
|
import {
|
||||||
|
ArrowRightIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
PaperPlaneIcon,
|
||||||
|
} from '@radix-ui/react-icons';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -21,6 +25,7 @@ import { SearchResults } from '../../../../../components/SearchResults';
|
|||||||
import { awaitableTankDefinitions } from '../../../../../core/awaitables/tankDefinitions';
|
import { awaitableTankDefinitions } from '../../../../../core/awaitables/tankDefinitions';
|
||||||
import { awaitableTankNames } from '../../../../../core/awaitables/tankNames';
|
import { awaitableTankNames } from '../../../../../core/awaitables/tankNames';
|
||||||
import { useLocale } from '../../../../../hooks/useLocale';
|
import { useLocale } from '../../../../../hooks/useLocale';
|
||||||
|
import { GuessEphemeral } from '../../../../../stores/guessEphemeral';
|
||||||
import { revealEvent } from './GuessRenderer';
|
import { revealEvent } from './GuessRenderer';
|
||||||
|
|
||||||
const { go } = fuzzysort;
|
const { go } = fuzzysort;
|
||||||
@@ -30,12 +35,17 @@ const [tankNames, tankDefinitions] = await Promise.all([
|
|||||||
awaitableTankDefinitions,
|
awaitableTankDefinitions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const ids = Object.keys(tankDefinitions.tanks);
|
||||||
|
|
||||||
export function Guesser() {
|
export function Guesser() {
|
||||||
|
const tank = GuessEphemeral.use((state) => state.tank);
|
||||||
|
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
const input = useRef<HTMLInputElement>(null);
|
const input = useRef<HTMLInputElement>(null);
|
||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
||||||
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
||||||
|
const mutateGuessEphemeral = GuessEphemeral.useMutation();
|
||||||
|
|
||||||
const requestSearch = useCallback(() => {
|
const requestSearch = useCallback(() => {
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
@@ -124,6 +134,7 @@ export function Guesser() {
|
|||||||
|
|
||||||
<Flex gap="3">
|
<Flex gap="3">
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
|
disabled={guessState !== null}
|
||||||
ref={input}
|
ref={input}
|
||||||
onChange={requestSearch}
|
onChange={requestSearch}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@@ -138,12 +149,44 @@ export function Guesser() {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="3"
|
size="3"
|
||||||
disabled={selected === null}
|
disabled={guessState === null && selected === null}
|
||||||
onClick={() => {
|
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>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</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,
|
||||||
|
})),
|
||||||
|
);
|
||||||
Reference in new issue
Block a user