diff --git a/bun.lockb b/bun.lockb index 8f11b0168..8beb97ef4 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index a47611087..e46eb0ea3 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,10 @@ }, "license": "MIT", "dependencies": { - "sharp": "^0.33.5", "googleapis": "^144.0.0", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "quicklime": "^1.2.0", + "sharp": "^0.33.5" }, "devDependencies": { "@types/bun": "latest", diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index f94d49d59..5633a9794 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -857,7 +857,12 @@ "name": "Guess the tank", "button": "Guess away", "description": "Silhouette mini-game", - "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": { + "placeholder": "Type to guess...", + "button": "Guess", + "no_results": "No results" + } }, "tankopedia": { diff --git a/packages/website/src/components/Embeds/GenerateURL.tsx b/packages/website/src/components/Embeds/GenerateURL.tsx index fcbca17c8..eef5bfeae 100644 --- a/packages/website/src/components/Embeds/GenerateURL.tsx +++ b/packages/website/src/components/Embeds/GenerateURL.tsx @@ -6,7 +6,6 @@ import { import { literals } from '@blitzkit/i18n/src/literals'; import { CaretLeftIcon, Link2Icon, PersonIcon } from '@radix-ui/react-icons'; import { - Badge, Button, Dialog, Flex, @@ -22,6 +21,7 @@ import type { embedConfigurations } from '../../constants/embeds'; import { useLocale } from '../../hooks/useLocale'; import { EmbedState, type EmbedStateStore } from '../../stores/embedState'; import { CopyButton } from '../CopyButton'; +import { SearchResults } from '../SearchResults'; interface GenerateURLProps { embed: keyof typeof embedConfigurations; @@ -118,25 +118,19 @@ export function GenerateURL({ embed }: GenerateURLProps) { )} {!searching && results.length > 0 && ( - + {results.map((result) => ( - + /> ))} - + )} diff --git a/packages/website/src/components/SearchResults.tsx b/packages/website/src/components/SearchResults.tsx new file mode 100644 index 000000000..821394068 --- /dev/null +++ b/packages/website/src/components/SearchResults.tsx @@ -0,0 +1,34 @@ +import { + Badge, + Button, + Flex, + type ButtonProps, + type FlexProps, +} from '@radix-ui/themes'; +import type { ReactNode } from 'react'; + +function Root(props: FlexProps) { + return ; +} + +interface ItemProps extends ButtonProps { + text: string; + discriminator?: ReactNode; +} + +function Item({ text, discriminator, ...props }: ItemProps) { + return ( + + ); +} + +export const SearchResults = { Root, Item }; diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx index 2d457af59..90221f211 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx @@ -36,6 +36,7 @@ interface ControlsProps { autoRotate?: boolean; zoomable?: boolean; enableRotate?: boolean; + distanceScale?: number; } export function Controls({ @@ -43,6 +44,7 @@ export function Controls({ autoRotate = true, zoomable = true, enableRotate = true, + distanceScale = 1, }: ControlsProps) { const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); const display = TankopediaEphemeral.use((state) => state.display); @@ -94,11 +96,11 @@ export function Controls({ protagonistHullOrigin.y + protagonistTurretOrigin.y + protagonistGunOrigin.y; - const inspectModeInitialPosition = [ + const inspectModeInitialPosition = new Vector3( -8, gunHeight + (naked ? 10 : 2), -13, - ] as const; + ).multiplyScalar(distanceScale); const protagonistGunOriginOnlyY = new Vector3( 0, protagonistTurretModelDefinition.gun_origin.y, @@ -203,7 +205,7 @@ export function Controls({ } case Pose.Default: { - camera.position.set(...inspectModeInitialPosition); + camera.position.copy(inspectModeInitialPosition); orbitControls.current?.target.set(0, 1.25, 0); break; } @@ -312,7 +314,7 @@ export function Controls({ .add(shellOrigin); } } else { - camera.position.set(...inspectModeInitialPosition); + camera.position.copy(inspectModeInitialPosition); orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0); orbitControls.current.enablePan = true; orbitControls.current.enableZoom = true; diff --git a/packages/website/src/pages/[...locale]/tools/guess/_index.tsx b/packages/website/src/pages/[...locale]/tools/guess/_index.tsx index 3d8def1f8..7ef48b173 100644 --- a/packages/website/src/pages/[...locale]/tools/guess/_index.tsx +++ b/packages/website/src/pages/[...locale]/tools/guess/_index.tsx @@ -3,9 +3,14 @@ import { awaitableModelDefinitions } from '../../../../core/awaitables/modelDefi 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 { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral'; import { GuessRenderer } from './components/GuessRenderer'; +import { Guesser } from './components/Guesser'; const [tankDefinitions, modelDefinitions, provisionDefinitions] = await Promise.all([ @@ -16,17 +21,19 @@ const [tankDefinitions, modelDefinitions, provisionDefinitions] = const ids = Object.keys(tankDefinitions.tanks); -export function Page() { +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]; return ( - - - - - + + + + + + + ); } @@ -39,54 +46,31 @@ function Content() { backgroundColor: Var('cyan-3'), }} > - - - - - - - - - - b - + /> + + + + + + ); } 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 ee8dfcd81..9efb18f3a 100644 --- a/packages/website/src/pages/[...locale]/tools/guess/components/GuessRenderer.tsx +++ b/packages/website/src/pages/[...locale]/tools/guess/components/GuessRenderer.tsx @@ -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(); + 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 = ({ 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 }} > - + diff --git a/packages/website/src/pages/[...locale]/tools/guess/components/Guesser.tsx b/packages/website/src/pages/[...locale]/tools/guess/components/Guesser.tsx new file mode 100644 index 000000000..17bb18d07 --- /dev/null +++ b/packages/website/src/pages/[...locale]/tools/guess/components/Guesser.tsx @@ -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(null); + const [searching, setSearching] = useState(false); + const [results, setResults] = useState(null); + const [selected, setSelected] = useState(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 ( + + {results !== null && ( + + + {results.length === 0 && ( + + + {strings.website.tools.guess.search.no_results} + + + )} + + + {results.map((result) => { + const Icon = classIcons[result.class]; + + return ( + { + if (!input.current) return; + + input.current.value = unwrap(result.name); + setSelected(result); + setResults(null); + }} + text={unwrap(result.name)} + discriminator={ + + + {TIER_ROMAN_NUMERALS[result.tier]} + + } + /> + ); + })} + + + + )} + + + + + {searching ? : } + + + + + + + ); +} diff --git a/packages/website/src/pages/[...locale]/tools/guess/index.astro b/packages/website/src/pages/[...locale]/tools/guess/index.astro index b53bd7755..0aeff805e 100644 --- a/packages/website/src/pages/[...locale]/tools/guess/index.astro +++ b/packages/website/src/pages/[...locale]/tools/guess/index.astro @@ -4,10 +4,12 @@ import BaseLayout from '../../../../layouts/BaseLayout.astro'; import { Page } from './_index'; export { getStaticPaths } from '../../index.astro'; + +const { locale } = Astro.params; --- - - + +