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;
---
-
-
+
+