feat: some guessing mechanics

This commit is contained in:
tresabhi committed 2025-05-04 21:02:50 -05:00
1 parent 46441d74d8
commit 232f24181f
10 files changed
+265 -76

No files matched your search

@@ -1,4 +1,5 @@
import { Canvas, invalidate } from '@react-three/fiber';
import { type QuicklimeCallback, Quicklime } from 'quicklime';
import { useCallback, useEffect, useRef } from 'react';
import { Fog } from 'three';
import { Controls } from '../../../../../components/Tankopedia/HeroSection/components/TankSandbox/components/Control';
@@ -7,15 +8,17 @@ import { TankModel } from '../../../../../components/Tankopedia/HeroSection/comp
import { Var } from '../../../../../core/radix/var';
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
const nearConcealed = -40;
const nearConcealed = -45;
const farConcealed = 22;
const nearRevealed = 20;
const farRevealed = 22;
const nearRevealed = 25;
const farRevealed = 25;
const animationDuration = 1;
export const revealEvent = new Quicklime<boolean>();
export function GuessRenderer() {
const fog = useRef(new Fog('black', nearConcealed, farConcealed));
const reveal = useCallback(() => {
const reveal = useCallback((show: boolean) => {
const t0 = Date.now() / 1000;
function frame() {
@@ -30,14 +33,27 @@ export function GuessRenderer() {
invalidate();
if (y < 1) requestAnimationFrame(frame);
if (y < 1) {
requestAnimationFrame(frame);
} else {
fog.current.near = nearRevealed;
fog.current.far = farRevealed;
}
}
requestAnimationFrame(frame);
}, []);
useEffect(() => {
setTimeout(reveal, 2000);
const handleRevealEvent: QuicklimeCallback<boolean> = ({ data }) => {
reveal(data);
};
revealEvent.on(handleRevealEvent);
return () => {
revealEvent.off(handleRevealEvent);
};
}, []);
return (
@@ -48,7 +64,7 @@ export function GuessRenderer() {
camera={{ position: [-5, 5, -5] }}
scene={{ fog: fog.current }}
>
<Controls zoomable={false} />
<Controls distanceScale={2} zoomable={false} />
<Lighting display={TankopediaDisplay.Model} />
<TankModel />
</Canvas>
@@ -0,0 +1,151 @@
import {
SEARCH_KEYS,
TankDefinition,
TIER_ROMAN_NUMERALS,
} from '@blitzkit/core';
import { MagnifyingGlassIcon, PaperPlaneIcon } from '@radix-ui/react-icons';
import {
Box,
Button,
Card,
Flex,
Spinner,
Text,
TextField,
} from '@radix-ui/themes';
import fuzzysort from 'fuzzysort';
import { debounce } from 'lodash-es';
import { useCallback, useRef, useState } from 'react';
import { classIcons } from '../../../../../components/ClassIcon';
import { SearchResults } from '../../../../../components/SearchResults';
import { awaitableTankDefinitions } from '../../../../../core/awaitables/tankDefinitions';
import { awaitableTankNames } from '../../../../../core/awaitables/tankNames';
import { useLocale } from '../../../../../hooks/useLocale';
import { revealEvent } from './GuessRenderer';
const { go } = fuzzysort;
const [tankNames, tankDefinitions] = await Promise.all([
awaitableTankNames,
awaitableTankDefinitions,
]);
export function Guesser() {
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 requestSearch = useCallback(() => {
setSelected(null);
setSearching(true);
search();
}, []);
const search = useCallback(
debounce(() => {
if (!input.current) return;
setSearching(false);
const trimmed = input.current.value.trim();
if (trimmed.length === 0) {
setResults(null);
return;
}
const searchResults = go(trimmed, tankNames, {
keys: SEARCH_KEYS,
limit: 6,
});
setResults(
searchResults.map((result) => tankDefinitions.tanks[result.obj.id]),
);
}, 500),
[],
);
return (
<Flex
direction="column"
position="absolute"
bottom="0"
left="50%"
style={{ transform: 'translateX(-50%)' }}
width="100%"
p="4"
maxWidth="25rem"
gap="4"
>
{results !== null && (
<Card variant="classic">
<Box py="2" px="3">
{results.length === 0 && (
<Flex justify="center">
<Text color="gray">
{strings.website.tools.guess.search.no_results}
</Text>
</Flex>
)}
<SearchResults.Root>
{results.map((result) => {
const Icon = classIcons[result.class];
return (
<SearchResults.Item
onClick={() => {
if (!input.current) return;
input.current.value = unwrap(result.name);
setSelected(result);
setResults(null);
}}
text={unwrap(result.name)}
discriminator={
<Flex
align="center"
gap="1"
width="38px"
justify="center"
>
<Icon width="1em" height="1em" />
{TIER_ROMAN_NUMERALS[result.tier]}
</Flex>
}
/>
);
})}
</SearchResults.Root>
</Box>
</Card>
)}
<Flex gap="3">
<TextField.Root
ref={input}
onChange={requestSearch}
style={{ flex: 1 }}
placeholder={strings.website.tools.guess.search.placeholder}
size="3"
variant="classic"
>
<TextField.Slot>
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
</TextField.Slot>
</TextField.Root>
<Button
size="3"
disabled={selected === null}
onClick={() => {
revealEvent.dispatch(true);
}}
>
{strings.website.tools.guess.search.button} <PaperPlaneIcon />
</Button>
</Flex>
</Flex>
);
}