mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
Merge branch 'main' into dev
This commit is contained in:
commit
091364d7f3
38 files changed
+1151
-443
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
# BlitzKit 1.27.8
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- The website now uses Inter as the font
|
||||||
|
- Added a cool new non-disturbed state for the Tankopedia because why not
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Removed the Discord plug and language changer from the homepage
|
||||||
+4
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.27.7",
|
"version": "1.27.8",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./types.d.ts",
|
"types": "./types.d.ts",
|
||||||
@@ -40,9 +40,10 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sharp": "^0.33.5",
|
|
||||||
"googleapis": "^144.0.0",
|
"googleapis": "^144.0.0",
|
||||||
"lodash-es": "^4.17.21"
|
"lodash-es": "^4.17.21",
|
||||||
|
"quicklime": "^1.2.0",
|
||||||
|
"sharp": "^0.33.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
|||||||
@@ -854,6 +854,21 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"tools": {
|
"tools": {
|
||||||
|
"guess": {
|
||||||
|
"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",
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Type to guess...",
|
||||||
|
"skip": "Skip",
|
||||||
|
"guess": "Guess",
|
||||||
|
"next": "Next",
|
||||||
|
"no_results": "No results"
|
||||||
|
},
|
||||||
|
"stats": "%s1 / %s2 guessed correctly"
|
||||||
|
},
|
||||||
|
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
"search": "Search a stat...",
|
"search": "Search a stat...",
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@astrolib/seo": "^1.0.0-beta.8",
|
"@astrolib/seo": "^1.0.0-beta.8",
|
||||||
"@blitzkit/core": "workspace:*",
|
"@blitzkit/core": "workspace:*",
|
||||||
"@blitzkit/i18n": "workspace:*",
|
"@blitzkit/i18n": "workspace:*",
|
||||||
|
"@fontsource/inter": "^5.2.5",
|
||||||
"@nanostores/persistent": "^0.10.2",
|
"@nanostores/persistent": "^0.10.2",
|
||||||
"@nanostores/react": "^0.7.3",
|
"@nanostores/react": "^0.7.3",
|
||||||
"@nivo/bar": "^0.87.0",
|
"@nivo/bar": "^0.87.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
@@ -6,7 +6,6 @@ import {
|
|||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
import { literals } from '@blitzkit/i18n/src/literals';
|
||||||
import { CaretLeftIcon, Link2Icon, PersonIcon } from '@radix-ui/react-icons';
|
import { CaretLeftIcon, Link2Icon, PersonIcon } from '@radix-ui/react-icons';
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -22,6 +21,7 @@ import type { embedConfigurations } from '../../constants/embeds';
|
|||||||
import { useLocale } from '../../hooks/useLocale';
|
import { useLocale } from '../../hooks/useLocale';
|
||||||
import { EmbedState, type EmbedStateStore } from '../../stores/embedState';
|
import { EmbedState, type EmbedStateStore } from '../../stores/embedState';
|
||||||
import { CopyButton } from '../CopyButton';
|
import { CopyButton } from '../CopyButton';
|
||||||
|
import { SearchResults } from '../SearchResults';
|
||||||
|
|
||||||
interface GenerateURLProps {
|
interface GenerateURLProps {
|
||||||
embed: keyof typeof embedConfigurations;
|
embed: keyof typeof embedConfigurations;
|
||||||
@@ -119,25 +119,19 @@ export function GenerateURL({ embed }: GenerateURLProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!searching && results.length > 0 && (
|
{!searching && results.length > 0 && (
|
||||||
<Flex direction="column" gap="2" width="100%">
|
<SearchResults.Root>
|
||||||
{results.map((result) => (
|
{results.map((result) => (
|
||||||
<Button
|
<SearchResults.Item
|
||||||
variant="ghost"
|
text={result.nickname}
|
||||||
|
discriminator={strings.common.regions.short[result.region]}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCopyUser(result);
|
setCopyUser(result);
|
||||||
setCopyDialogOpen(true);
|
setCopyDialogOpen(true);
|
||||||
setSearchDialogOpen(false);
|
setSearchDialogOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Flex justify="between" width="100%" gap="2">
|
|
||||||
{result.nickname}
|
|
||||||
<Badge>
|
|
||||||
{strings.common.regions.short[result.region]}
|
|
||||||
</Badge>
|
|
||||||
</Flex>
|
|
||||||
</Button>
|
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</SearchResults.Root>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
|||||||
@@ -16,24 +16,24 @@ const strings = getStrings(locale);
|
|||||||
--p-2: 0%;
|
--p-2: 0%;
|
||||||
--p-3: 0%;
|
--p-3: 0%;
|
||||||
--p-4: 0%;
|
--p-4: 0%;
|
||||||
--p-5: 0%;
|
--p-7: 0%;
|
||||||
|
|
||||||
--g-1-dark: var(--blue-3);
|
--g-1-dark: var(--blue-3);
|
||||||
--g-1-light: var(--blue-5);
|
--g-1-light: var(--blue-7);
|
||||||
--g-2-dark: var(--violet-3);
|
--g-2-dark: var(--violet-3);
|
||||||
--g-2-light: var(--violet-5);
|
--g-2-light: var(--violet-7);
|
||||||
--g-3-dark: var(--purple-3);
|
--g-3-dark: var(--purple-3);
|
||||||
--g-3-light: var(--purple-5);
|
--g-3-light: var(--purple-7);
|
||||||
--g-4-dark: var(--crimson-3);
|
--g-4-dark: var(--crimson-3);
|
||||||
--g-4-light: var(--crimson-5);
|
--g-4-light: var(--crimson-7);
|
||||||
--g-5-dark: var(--tomato-3);
|
--g-7-dark: var(--tomato-3);
|
||||||
--g-5-light: var(--tomato-5);
|
--g-7-light: var(--tomato-7);
|
||||||
|
|
||||||
--g-1: color-mix(in srgb, var(--g-1-dark), var(--g-1-light) var(--p-1));
|
--g-1: color-mix(in srgb, var(--g-1-dark), var(--g-1-light) var(--p-1));
|
||||||
--g-2: color-mix(in srgb, var(--g-2-dark), var(--g-2-light) var(--p-2));
|
--g-2: color-mix(in srgb, var(--g-2-dark), var(--g-2-light) var(--p-2));
|
||||||
--g-3: color-mix(in srgb, var(--g-3-dark), var(--g-3-light) var(--p-3));
|
--g-3: color-mix(in srgb, var(--g-3-dark), var(--g-3-light) var(--p-3));
|
||||||
--g-4: color-mix(in srgb, var(--g-4-dark), var(--g-4-light) var(--p-4));
|
--g-4: color-mix(in srgb, var(--g-4-dark), var(--g-4-light) var(--p-4));
|
||||||
--g-5: color-mix(in srgb, var(--g-5-dark), var(--g-5-light) var(--p-5));
|
--g-7: color-mix(in srgb, var(--g-7-dark), var(--g-7-light) var(--p-7));
|
||||||
|
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
@@ -41,34 +41,34 @@ const strings = getStrings(locale);
|
|||||||
var(--g-2),
|
var(--g-2),
|
||||||
var(--g-3),
|
var(--g-3),
|
||||||
var(--g-4),
|
var(--g-4),
|
||||||
var(--g-5)
|
var(--g-7)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-hero[data-variant='pride'] {
|
#home-hero[data-variant='pride'] {
|
||||||
--g-1-dark: var(--crimson-3);
|
--g-1-dark: var(--crimson-3);
|
||||||
--g-1-light: var(--crimson-5);
|
--g-1-light: var(--crimson-7);
|
||||||
--g-2-dark: var(--orange-3);
|
--g-2-dark: var(--orange-3);
|
||||||
--g-2-light: var(--orange-5);
|
--g-2-light: var(--orange-7);
|
||||||
--g-3-dark: var(--yellow-3);
|
--g-3-dark: var(--yellow-3);
|
||||||
--g-3-light: var(--yellow-5);
|
--g-3-light: var(--yellow-7);
|
||||||
--g-4-dark: var(--blue-3);
|
--g-4-dark: var(--blue-3);
|
||||||
--g-4-light: var(--blue-5);
|
--g-4-light: var(--blue-7);
|
||||||
--g-5-dark: var(--purple-3);
|
--g-7-dark: var(--purple-3);
|
||||||
--g-5-light: var(--purple-5);
|
--g-7-light: var(--purple-7);
|
||||||
}
|
}
|
||||||
|
|
||||||
#home-hero[data-variant='spooky'] {
|
#home-hero[data-variant='spooky'] {
|
||||||
--g-1-dark: var(--orange-3);
|
--g-1-dark: var(--orange-3);
|
||||||
--g-1-light: var(--orange-5);
|
--g-1-light: var(--orange-7);
|
||||||
--g-2-dark: var(--tomato-3);
|
--g-2-dark: var(--tomato-3);
|
||||||
--g-2-light: var(--tomato-5);
|
--g-2-light: var(--tomato-7);
|
||||||
--g-3-dark: var(--orange-3);
|
--g-3-dark: var(--orange-3);
|
||||||
--g-3-light: var(--orange-5);
|
--g-3-light: var(--orange-7);
|
||||||
--g-4-dark: var(--tomato-3);
|
--g-4-dark: var(--tomato-3);
|
||||||
--g-4-light: var(--tomato-5);
|
--g-4-light: var(--tomato-7);
|
||||||
--g-5-dark: var(--orange-3);
|
--g-7-dark: var(--orange-3);
|
||||||
--g-5-light: var(--orange-5);
|
--g-7-light: var(--orange-7);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import { getStrings } from '../core/i18n/getStrings';
|
|||||||
import { Var } from '../core/radix/var';
|
import { Var } from '../core/radix/var';
|
||||||
import { LinkI18n } from './LinkI18n';
|
import { LinkI18n } from './LinkI18n';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
integrated?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { integrated } = Astro.props;
|
||||||
const toolsFiltered = TOOLS.filter((tool) => !('href' in tool));
|
const toolsFiltered = TOOLS.filter((tool) => !('href' in tool));
|
||||||
const { locale } = Astro.params;
|
const { locale } = Astro.params;
|
||||||
const branchName = resolveBranchName(locale);
|
const branchName = resolveBranchName(locale);
|
||||||
@@ -21,6 +26,19 @@ const strings = getStrings(locale);
|
|||||||
---
|
---
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.navbar,
|
||||||
|
.padder {
|
||||||
|
transition-duration: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.no-navbar .navbar {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.no-navbar .padder {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar[data-expanded='false'] *[data-expanded] {
|
.navbar[data-expanded='false'] *[data-expanded] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -59,7 +77,11 @@ const strings = getStrings(locale);
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Box height={{ initial: '3.5rem', sm: '3rem' }} />
|
{
|
||||||
|
!integrated && (
|
||||||
|
<Box className="padder" height={{ initial: '3.5rem', sm: '3rem' }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
@@ -71,6 +93,7 @@ const strings = getStrings(locale);
|
|||||||
position="fixed"
|
position="fixed"
|
||||||
style={{
|
style={{
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
backdropFilter: 'blur(4rem)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -79,6 +102,7 @@ const strings = getStrings(locale);
|
|||||||
p={{ initial: '4', sm: '3' }}
|
p={{ initial: '4', sm: '3' }}
|
||||||
direction="column"
|
direction="column"
|
||||||
align="center"
|
align="center"
|
||||||
|
style={{ backgroundColor: integrated ? undefined : Var('gray-1') }}
|
||||||
>
|
>
|
||||||
<Flex gap="4" direction="column" maxWidth="80rem" width="100%">
|
<Flex gap="4" direction="column" maxWidth="80rem" width="100%">
|
||||||
<Flex align="center">
|
<Flex align="center">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function PageWrapper({
|
|||||||
style={{
|
style={{
|
||||||
flex: noFlex1 ? undefined : 1,
|
flex: noFlex1 ? undefined : 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
minHeight: noMinHeight ? 'unset' : `calc(100dvh - ${NAVBAR_HEIGHT}px)`,
|
minHeight: noMinHeight ? 'unset' : `calc(100svh - ${NAVBAR_HEIGHT}px)`,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
...containerProps?.style,
|
...containerProps?.style,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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 <Flex direction="column" gap="2" width="100%" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ItemProps extends ButtonProps {
|
||||||
|
text: string;
|
||||||
|
discriminator?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Item({ text, discriminator, ...props }: ItemProps) {
|
||||||
|
return (
|
||||||
|
<Button variant="ghost" {...props} highContrast>
|
||||||
|
<Flex
|
||||||
|
justify={discriminator === undefined ? 'center' : 'between'}
|
||||||
|
width="100%"
|
||||||
|
gap="2"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
{discriminator !== undefined && <Badge>{discriminator}</Badge>}
|
||||||
|
</Flex>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchResults = { Root, Item };
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
.tank-search-card .image {
|
.tank-search-card .image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 6rem;
|
height: 8rem;
|
||||||
object-position: center right;
|
object-position: center right;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
filter: drop-shadow(-1px -1px 4px black);
|
filter: drop-shadow(-1px -1px 4px black);
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ export function TankCardWrapper(props: GridProps) {
|
|||||||
py="4"
|
py="4"
|
||||||
columns={{
|
columns={{
|
||||||
initial: 'repeat(auto-fill, minmax(6rem, 1fr))',
|
initial: 'repeat(auto-fill, minmax(6rem, 1fr))',
|
||||||
sm: 'repeat(auto-fill, minmax(8rem, 1fr))',
|
sm: 'repeat(auto-fill, minmax(10rem, 1fr))',
|
||||||
}}
|
}}
|
||||||
flow="dense"
|
flow="dense"
|
||||||
gap="2"
|
gap="4"
|
||||||
gapY="6"
|
gapY="8"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
.quick-inputs {
|
.quick-inputs {
|
||||||
top: -2rem;
|
left: -5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-inputs:focus-within,
|
.quick-inputs:focus-within,
|
||||||
.tank-sandbox-container:hover .quick-inputs {
|
.tank-sandbox-container:hover .quick-inputs {
|
||||||
top: 1rem;
|
left: 1rem;
|
||||||
}
|
}
|
||||||
+4
-3
@@ -66,12 +66,13 @@ export function QuickInputs() {
|
|||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
gap="2"
|
gap="2"
|
||||||
width="10rem"
|
width="4rem"
|
||||||
|
direction="column"
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: '50%',
|
top: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
transitionDuration: '0.2s',
|
transitionDuration: '0.2s',
|
||||||
}}
|
}}
|
||||||
className="quick-inputs"
|
className="quick-inputs"
|
||||||
|
|||||||
+192
-161
@@ -90,10 +90,12 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||||
const { strings, unwrap } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
|
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||||
|
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<QuickInputs />
|
{disturbed && <QuickInputs />}
|
||||||
|
|
||||||
{display === TankopediaDisplay.ShootingRange && (
|
{display === TankopediaDisplay.ShootingRange && (
|
||||||
<Box
|
<Box
|
||||||
@@ -118,7 +120,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
gap="2"
|
gap="2"
|
||||||
direction="column"
|
direction="column"
|
||||||
top="50%"
|
top="50%"
|
||||||
right={display === TankopediaDisplay.DynamicArmor ? '3' : '-3rem'}
|
right={display === TankopediaDisplay.DynamicArmor ? '3' : '-4rem'}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
@@ -323,9 +325,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
direction="column"
|
direction="column"
|
||||||
align="center"
|
align="center"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
bottom="4"
|
bottom={revealed ? '4' : '-100%'}
|
||||||
left="50%"
|
left="50%"
|
||||||
style={{ transform: 'translateX(-50%)' }}
|
style={{ transform: 'translateX(-50%)', transitionDuration: '200ms' }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
@@ -401,7 +403,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
<Flex gap="3" align="center" mt="2">
|
<Flex gap="3" align="center" mt="2">
|
||||||
<SegmentedControl.Root
|
<SegmentedControl.Root
|
||||||
variant="classic"
|
variant="classic"
|
||||||
value={`${display}`}
|
value={`${disturbed ? display : -1}`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
mutateTankopediaEphemeral((draft) => {
|
mutateTankopediaEphemeral((draft) => {
|
||||||
draft.disturbed = true;
|
draft.disturbed = true;
|
||||||
@@ -455,185 +457,214 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
|||||||
</SegmentedControl.Item> */}
|
</SegmentedControl.Item> */}
|
||||||
</SegmentedControl.Root>
|
</SegmentedControl.Root>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
{disturbed && (
|
||||||
<DropdownMenu.Trigger>
|
<>
|
||||||
<IconButton variant="ghost" color="gray">
|
<DropdownMenu.Root>
|
||||||
<EyeOpenIcon />
|
<DropdownMenu.Trigger>
|
||||||
</IconButton>
|
<IconButton variant="ghost" color="gray">
|
||||||
</DropdownMenu.Trigger>
|
<EyeOpenIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.HullDown)}>
|
<DropdownMenu.Item
|
||||||
{strings.website.tools.tankopedia.sandbox.poses.hull_down}
|
onClick={() => poseEvent.emit(Pose.HullDown)}
|
||||||
</DropdownMenu.Item>
|
>
|
||||||
|
{strings.website.tools.tankopedia.sandbox.poses.hull_down}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.FaceHug)}>
|
<DropdownMenu.Item
|
||||||
{strings.website.tools.tankopedia.sandbox.poses.face_hug}
|
onClick={() => poseEvent.emit(Pose.FaceHug)}
|
||||||
</DropdownMenu.Item>
|
>
|
||||||
|
{strings.website.tools.tankopedia.sandbox.poses.face_hug}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<DropdownMenu.Item onClick={() => poseEvent.emit(Pose.Default)}>
|
<DropdownMenu.Item
|
||||||
{strings.website.tools.tankopedia.sandbox.poses.default}
|
onClick={() => poseEvent.emit(Pose.Default)}
|
||||||
</DropdownMenu.Item>
|
>
|
||||||
</DropdownMenu.Content>
|
{strings.website.tools.tankopedia.sandbox.poses.default}
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<IconButton variant="ghost" color="gray">
|
<IconButton variant="ghost" color="gray">
|
||||||
<GearIcon />
|
<GearIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
{display === TankopediaDisplay.DynamicArmor && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Label>
|
||||||
|
{
|
||||||
|
strings.website.tools.tankopedia.sandbox.settings
|
||||||
|
.armor
|
||||||
|
}
|
||||||
|
</DropdownMenu.Label>
|
||||||
|
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={greenPenetration}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.greenPenetration = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
strings.website.tools.tankopedia.sandbox.settings
|
||||||
|
.green_penetration
|
||||||
|
}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={hideTankModelUnderArmor}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.hideTankModelUnderArmor = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
strings.website.tools.tankopedia.sandbox.settings
|
||||||
|
.hide_model_under_armor
|
||||||
|
}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={opaque}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.opaque = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
strings.website.tools.tankopedia.sandbox.settings
|
||||||
|
.opaque
|
||||||
|
}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
|
||||||
|
{developerMode && (
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={wireframe}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
mutateTankopediaPersistent((draft) => {
|
||||||
|
draft.wireframe = checked;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
strings.website.tools.tankopedia.sandbox.settings
|
||||||
|
.dev_wireframe
|
||||||
|
}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
{display === TankopediaDisplay.DynamicArmor && (
|
|
||||||
<>
|
|
||||||
<DropdownMenu.Label>
|
<DropdownMenu.Label>
|
||||||
{strings.website.tools.tankopedia.sandbox.settings.armor}
|
{
|
||||||
|
strings.website.tools.tankopedia.sandbox.settings
|
||||||
|
.environment
|
||||||
|
}
|
||||||
</DropdownMenu.Label>
|
</DropdownMenu.Label>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<DropdownMenu.CheckboxItem
|
||||||
checked={greenPenetration}
|
checked={showGrid}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
mutateTankopediaPersistent((draft) => {
|
mutateTankopediaPersistent((draft) => {
|
||||||
draft.greenPenetration = checked;
|
draft.showGrid = checked;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
strings.website.tools.tankopedia.sandbox.settings
|
||||||
.green_penetration
|
.show_grid
|
||||||
}
|
}
|
||||||
</DropdownMenu.CheckboxItem>
|
</DropdownMenu.CheckboxItem>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<Popover.Root>
|
||||||
checked={hideTankModelUnderArmor}
|
<Popover.Trigger>
|
||||||
onCheckedChange={(checked) => {
|
<IconButton color="gray" variant="ghost">
|
||||||
mutateTankopediaPersistent((draft) => {
|
<CameraIcon />
|
||||||
draft.hideTankModelUnderArmor = checked;
|
</IconButton>
|
||||||
});
|
</Popover.Trigger>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
|
||||||
.hide_model_under_armor
|
|
||||||
}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
<Popover.Content>
|
||||||
checked={opaque}
|
<Flex direction="column" gap="2">
|
||||||
onCheckedChange={(checked) => {
|
<Popover.Close>
|
||||||
mutateTankopediaPersistent((draft) => {
|
<Button
|
||||||
draft.opaque = checked;
|
onClick={() => {
|
||||||
});
|
if (!canvas.current) return;
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.sandbox.settings.opaque}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
|
|
||||||
{developerMode && (
|
const anchor = document.createElement('a');
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={wireframe}
|
anchor.setAttribute(
|
||||||
onCheckedChange={(checked) => {
|
'download',
|
||||||
mutateTankopediaPersistent((draft) => {
|
`${protagonistTank.name}.png`,
|
||||||
draft.wireframe = checked;
|
);
|
||||||
});
|
anchor.setAttribute(
|
||||||
}}
|
'href',
|
||||||
>
|
canvas.current.toDataURL('image/png'),
|
||||||
{
|
);
|
||||||
strings.website.tools.tankopedia.sandbox.settings
|
anchor.click();
|
||||||
.dev_wireframe
|
}}
|
||||||
}
|
>
|
||||||
</DropdownMenu.CheckboxItem>
|
<DownloadIcon />
|
||||||
|
{
|
||||||
|
strings.website.tools.tankopedia.sandbox.screenshot
|
||||||
|
.download
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</Popover.Close>
|
||||||
|
<Popover.Close>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (!canvas.current) return;
|
||||||
|
|
||||||
|
canvas.current.toBlob((blob) => {
|
||||||
|
if (!blob) return;
|
||||||
|
|
||||||
|
navigator.clipboard.write([
|
||||||
|
new ClipboardItem({ 'image/png': blob }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
{
|
||||||
|
strings.website.tools.tankopedia.sandbox.screenshot
|
||||||
|
.copy
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</Popover.Close>
|
||||||
|
</Flex>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
|
||||||
|
{fullScreenAvailable && (
|
||||||
|
<IconButton
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
if (isFullScreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else document.body.requestFullscreen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFullScreen ? (
|
||||||
|
<ExitFullScreenIcon />
|
||||||
|
) : (
|
||||||
|
<EnterFullScreenIcon />
|
||||||
)}
|
)}
|
||||||
</>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
<DropdownMenu.Label>
|
|
||||||
{strings.website.tools.tankopedia.sandbox.settings.environment}
|
|
||||||
</DropdownMenu.Label>
|
|
||||||
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
checked={showGrid}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
mutateTankopediaPersistent((draft) => {
|
|
||||||
draft.showGrid = checked;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{strings.website.tools.tankopedia.sandbox.settings.show_grid}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
|
|
||||||
<Popover.Root>
|
|
||||||
<Popover.Trigger>
|
|
||||||
<IconButton color="gray" variant="ghost">
|
|
||||||
<CameraIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Popover.Trigger>
|
|
||||||
|
|
||||||
<Popover.Content>
|
|
||||||
<Flex direction="column" gap="2">
|
|
||||||
<Popover.Close>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (!canvas.current) return;
|
|
||||||
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
|
|
||||||
anchor.setAttribute(
|
|
||||||
'download',
|
|
||||||
`${protagonistTank.name}.png`,
|
|
||||||
);
|
|
||||||
anchor.setAttribute(
|
|
||||||
'href',
|
|
||||||
canvas.current.toDataURL('image/png'),
|
|
||||||
);
|
|
||||||
anchor.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
{
|
|
||||||
strings.website.tools.tankopedia.sandbox.screenshot
|
|
||||||
.download
|
|
||||||
}
|
|
||||||
</Button>
|
|
||||||
</Popover.Close>
|
|
||||||
<Popover.Close>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
if (!canvas.current) return;
|
|
||||||
|
|
||||||
canvas.current.toBlob((blob) => {
|
|
||||||
if (!blob) return;
|
|
||||||
|
|
||||||
navigator.clipboard.write([
|
|
||||||
new ClipboardItem({ 'image/png': blob }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CopyIcon />
|
|
||||||
{strings.website.tools.tankopedia.sandbox.screenshot.copy}
|
|
||||||
</Button>
|
|
||||||
</Popover.Close>
|
|
||||||
</Flex>
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover.Root>
|
|
||||||
|
|
||||||
{fullScreenAvailable && (
|
|
||||||
<IconButton
|
|
||||||
color="gray"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
if (isFullScreen) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
} else document.body.requestFullscreen();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isFullScreen ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
|
|
||||||
</IconButton>
|
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
.scroll-hint {
|
||||||
|
position: absolute;
|
||||||
|
animation: dropFade 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropFade {
|
||||||
|
0% {
|
||||||
|
top: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
top: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
top: 50%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
top: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
import { Box } from '@radix-ui/themes';
|
||||||
|
import { Var } from '../../../../../core/radix/var';
|
||||||
|
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
export function ScrollHint() {
|
||||||
|
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||||
|
|
||||||
|
if (disturbed) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
bottom="0"
|
||||||
|
mb="5"
|
||||||
|
right="50%"
|
||||||
|
height="2rem"
|
||||||
|
width="1rem"
|
||||||
|
style={{
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
outline: `2px solid ${Var('gray-11')}`,
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className="scroll-hint"
|
||||||
|
width="0.75rem"
|
||||||
|
height="0.75rem"
|
||||||
|
m="0.125rem"
|
||||||
|
style={{
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: Var('gray-12'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
import { TankType } from '@blitzkit/core';
|
||||||
|
import { Box, Heading } from '@radix-ui/themes';
|
||||||
|
import { Var } from '../../../../../core/radix/var';
|
||||||
|
import { useLocale } from '../../../../../hooks/useLocale';
|
||||||
|
import { Duel } from '../../../../../stores/duel';
|
||||||
|
import type { RadixColor } from '../../../../../stores/embedState';
|
||||||
|
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
|
||||||
|
|
||||||
|
interface TitleProps {
|
||||||
|
outline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NATION_COLORS: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
text: RadixColor;
|
||||||
|
background: RadixColor[];
|
||||||
|
tint: RadixColor;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
france: {
|
||||||
|
text: 'mint',
|
||||||
|
tint: 'purple',
|
||||||
|
background: ['mint', 'cyan', 'jade'],
|
||||||
|
},
|
||||||
|
germany: {
|
||||||
|
text: 'sage',
|
||||||
|
tint: 'blue',
|
||||||
|
background: ['slate', 'sage', 'mauve'],
|
||||||
|
},
|
||||||
|
usa: {
|
||||||
|
text: 'gold',
|
||||||
|
tint: 'red',
|
||||||
|
background: ['brown', 'gold', 'gray'],
|
||||||
|
},
|
||||||
|
european: {
|
||||||
|
text: 'sky',
|
||||||
|
tint: 'violet',
|
||||||
|
background: ['cyan', 'blue', 'indigo'],
|
||||||
|
},
|
||||||
|
ussr: {
|
||||||
|
text: 'bronze',
|
||||||
|
tint: 'red',
|
||||||
|
background: ['bronze', 'tomato', 'bronze'],
|
||||||
|
},
|
||||||
|
uk: {
|
||||||
|
text: 'gold',
|
||||||
|
tint: 'red',
|
||||||
|
background: ['bronze', 'gold', 'brown'],
|
||||||
|
},
|
||||||
|
japan: {
|
||||||
|
text: 'mint',
|
||||||
|
tint: 'violet',
|
||||||
|
background: ['green', 'teal', 'jade'],
|
||||||
|
},
|
||||||
|
china: {
|
||||||
|
text: 'mint',
|
||||||
|
tint: 'pink',
|
||||||
|
background: ['green', 'teal', 'jade'],
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
text: 'amber',
|
||||||
|
tint: 'sky',
|
||||||
|
background: ['amber', 'yellow', 'gray'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Title({ outline }: TitleProps) {
|
||||||
|
const { unwrap } = useLocale();
|
||||||
|
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||||
|
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||||
|
const revealed = TankopediaEphemeral.use((state) => state.revealed);
|
||||||
|
const nationColors = NATION_COLORS[protagonist.nation];
|
||||||
|
const color = Var(
|
||||||
|
disturbed
|
||||||
|
? protagonist.type === TankType.COLLECTOR
|
||||||
|
? 'blue-11'
|
||||||
|
: protagonist.type === TankType.PREMIUM
|
||||||
|
? 'amber-11'
|
||||||
|
: 'gray-12'
|
||||||
|
: `${nationColors.text}-12`,
|
||||||
|
);
|
||||||
|
const name = unwrap(protagonist.name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={disturbed ? '6rem' : '50%'}
|
||||||
|
left="50%"
|
||||||
|
style={{
|
||||||
|
transitionDuration: '1s',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading
|
||||||
|
style={{
|
||||||
|
fontWeight: 900,
|
||||||
|
userSelect: 'none',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
fontSize: revealed
|
||||||
|
? disturbed
|
||||||
|
? '2rem'
|
||||||
|
: `min(65vh, ${125 / name.length}vw)`
|
||||||
|
: `min(48vh, ${75 / name.length}vw)`,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
color: outline ? Var(`${nationColors.tint}-a2`) : color,
|
||||||
|
opacity: outline && disturbed ? 0 : 1,
|
||||||
|
WebkitTextStroke: outline
|
||||||
|
? `${revealed ? (disturbed ? '1px' : 'min(2px, 0.2vw)') : 0} ${color}`
|
||||||
|
: undefined,
|
||||||
|
letterSpacing: disturbed || !revealed ? 0 : '-0.03em',
|
||||||
|
transition: `
|
||||||
|
letter-spacing 1.5s ${disturbed ? '' : 'cubic-bezier(.81,-2,.68,1)'},
|
||||||
|
font-size 1s,
|
||||||
|
-webkit-text-stroke 2s,
|
||||||
|
opacity 1s
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
wrap="nowrap"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Heading>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
+48
-6
@@ -1,3 +1,4 @@
|
|||||||
|
import { I_HAT, J_HAT } from '@blitzkit/core';
|
||||||
import { OrbitControls } from '@react-three/drei';
|
import { OrbitControls } from '@react-three/drei';
|
||||||
import { invalidate, useThree } from '@react-three/fiber';
|
import { invalidate, useThree } from '@react-three/fiber';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
@@ -33,9 +34,19 @@ const ARCADE_MODE_FOV = 54;
|
|||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
naked?: boolean;
|
naked?: boolean;
|
||||||
|
autoRotate?: boolean;
|
||||||
|
zoomable?: boolean;
|
||||||
|
enableRotate?: boolean;
|
||||||
|
distanceScale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Controls({ naked }: ControlsProps) {
|
export function Controls({
|
||||||
|
naked,
|
||||||
|
autoRotate = true,
|
||||||
|
zoomable = true,
|
||||||
|
enableRotate = true,
|
||||||
|
distanceScale = 1,
|
||||||
|
}: ControlsProps) {
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||||
const display = TankopediaEphemeral.use((state) => state.display);
|
const display = TankopediaEphemeral.use((state) => state.display);
|
||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
@@ -82,15 +93,16 @@ export function Controls({ naked }: ControlsProps) {
|
|||||||
antagonistModelDefinition.turret_origin.y +
|
antagonistModelDefinition.turret_origin.y +
|
||||||
antagonistTurretModelDefinition.gun_origin.y;
|
antagonistTurretModelDefinition.gun_origin.y;
|
||||||
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||||
|
const doAutoRotate = autoRotate && !disturbed;
|
||||||
const gunHeight =
|
const gunHeight =
|
||||||
protagonistHullOrigin.y +
|
protagonistHullOrigin.y +
|
||||||
protagonistTurretOrigin.y +
|
protagonistTurretOrigin.y +
|
||||||
protagonistGunOrigin.y;
|
protagonistGunOrigin.y;
|
||||||
const inspectModeInitialPosition = [
|
const inspectModeInitialPosition = new Vector3(
|
||||||
-8,
|
-8,
|
||||||
gunHeight + (naked ? 10 : 2),
|
gunHeight + (naked ? 10 : 2),
|
||||||
-13,
|
-13,
|
||||||
] as const;
|
).multiplyScalar(distanceScale);
|
||||||
const protagonistGunOriginOnlyY = new Vector3(
|
const protagonistGunOriginOnlyY = new Vector3(
|
||||||
0,
|
0,
|
||||||
protagonistTurretModelDefinition.gun_origin.y,
|
protagonistTurretModelDefinition.gun_origin.y,
|
||||||
@@ -101,6 +113,34 @@ export function Controls({ naked }: ControlsProps) {
|
|||||||
.add(protagonistTurretOrigin)
|
.add(protagonistTurretOrigin)
|
||||||
.add(protagonistGunOriginOnlyY);
|
.add(protagonistGunOriginOnlyY);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!orbitControls.current || !doAutoRotate) return;
|
||||||
|
|
||||||
|
const t0 = Date.now() / 1000;
|
||||||
|
let cancel = false;
|
||||||
|
const position = inspectModeInitialPosition.clone();
|
||||||
|
|
||||||
|
function frame() {
|
||||||
|
if (!cancel) requestAnimationFrame(frame);
|
||||||
|
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
const t = now - t0;
|
||||||
|
|
||||||
|
camera.position
|
||||||
|
.copy(inspectModeInitialPosition)
|
||||||
|
.applyAxisAngle(I_HAT, (Math.PI / 32) * Math.sin(t / 9))
|
||||||
|
.applyAxisAngle(J_HAT, (-Math.PI / 16) * Math.sin(t / 7));
|
||||||
|
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
frame();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancel = true;
|
||||||
|
};
|
||||||
|
}, [doAutoRotate, camera]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe(
|
||||||
(state) => state.controlsEnabled,
|
(state) => state.controlsEnabled,
|
||||||
@@ -195,7 +235,7 @@ export function Controls({ naked }: ControlsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case Pose.Default: {
|
case Pose.Default: {
|
||||||
camera.position.set(...inspectModeInitialPosition);
|
camera.position.copy(inspectModeInitialPosition);
|
||||||
orbitControls.current?.target.set(0, 1.25, 0);
|
orbitControls.current?.target.set(0, 1.25, 0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -304,7 +344,7 @@ export function Controls({ naked }: ControlsProps) {
|
|||||||
.add(shellOrigin);
|
.add(shellOrigin);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
camera.position.set(...inspectModeInitialPosition);
|
camera.position.copy(inspectModeInitialPosition);
|
||||||
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
|
||||||
orbitControls.current.enablePan = true;
|
orbitControls.current.enablePan = true;
|
||||||
orbitControls.current.enableZoom = true;
|
orbitControls.current.enableZoom = true;
|
||||||
@@ -342,12 +382,14 @@ export function Controls({ naked }: ControlsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<OrbitControls
|
<OrbitControls
|
||||||
|
enableRotate={enableRotate}
|
||||||
maxDistance={20}
|
maxDistance={20}
|
||||||
minDistance={5}
|
minDistance={5}
|
||||||
|
enableZoom={zoomable}
|
||||||
|
zoomSpeed={zoomable ? undefined : 0}
|
||||||
ref={orbitControls}
|
ref={orbitControls}
|
||||||
enabled={tankopediaEphemeralStore.getState().controlsEnabled}
|
enabled={tankopediaEphemeralStore.getState().controlsEnabled}
|
||||||
enableDamping={false}
|
enableDamping={false}
|
||||||
autoRotate={!disturbed}
|
|
||||||
autoRotateSpeed={1 / 4}
|
autoRotateSpeed={1 / 4}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
@@ -35,6 +35,7 @@ const [modelDefinitions, equipmentDefinitions, provisionDefinitions] =
|
|||||||
const emptyVector = new Vector2();
|
const emptyVector = new Vector2();
|
||||||
|
|
||||||
export function SceneProps() {
|
export function SceneProps() {
|
||||||
|
const disturbed = TankopediaEphemeral.use((state) => state.disturbed);
|
||||||
const targetCircleWrapper = useRef<Group>(null);
|
const targetCircleWrapper = useRef<Group>(null);
|
||||||
const clientTargetCircle = useRef<HTMLDivElement>(null);
|
const clientTargetCircle = useRef<HTMLDivElement>(null);
|
||||||
const serverTargetCircle = useRef<HTMLDivElement>(null);
|
const serverTargetCircle = useRef<HTMLDivElement>(null);
|
||||||
@@ -233,6 +234,8 @@ export function SceneProps() {
|
|||||||
serverTargetCircle.current.style.height = targetCircleDotsSize;
|
serverTargetCircle.current.style.height = targetCircleDotsSize;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!disturbed) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{display !== TankopediaDisplay.ShootingRange && (
|
{display !== TankopediaDisplay.ShootingRange && (
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { assertSecret, TankType } from '@blitzkit/core';
|
import { TankType } from '@blitzkit/core';
|
||||||
import { CaretLeftIcon, MixIcon, UpdateIcon } from '@radix-ui/react-icons';
|
import { Box, Flex } from '@radix-ui/themes';
|
||||||
import { Box, Dialog, Flex, Heading, Link, Spinner } from '@radix-ui/themes';
|
|
||||||
import { times } from 'lodash-es';
|
import { times } from 'lodash-es';
|
||||||
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { NAVBAR_HEIGHT } from '../../../constants/navbar';
|
|
||||||
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 { tankToDuelMember } from '../../../core/blitzkit/tankToDuelMember';
|
|
||||||
import { Var } from '../../../core/radix/var';
|
import { Var } from '../../../core/radix/var';
|
||||||
import { useFullScreen } from '../../../hooks/useFullScreen';
|
import { useFullScreen } from '../../../hooks/useFullScreen';
|
||||||
import { useLocale } from '../../../hooks/useLocale';
|
import { useLocale } from '../../../hooks/useLocale';
|
||||||
@@ -16,10 +13,9 @@ import { TankopediaEphemeral } from '../../../stores/tankopediaEphemeral';
|
|||||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||||
import type { ThicknessRange } from '../../Armor/components/StaticArmor';
|
import type { ThicknessRange } from '../../Armor/components/StaticArmor';
|
||||||
import { classIcons } from '../../ClassIcon';
|
import { classIcons } from '../../ClassIcon';
|
||||||
import { ScienceIcon } from '../../ScienceIcon';
|
|
||||||
import { TankSearch } from '../../TankSearch';
|
|
||||||
import { Options } from './components/Options';
|
import { Options } from './components/Options';
|
||||||
import { TankSandbox } from './components/TankSandbox';
|
import { TankSandbox } from './components/TankSandbox';
|
||||||
|
import { NATION_COLORS, Title } from './components/TankSandbox/Title';
|
||||||
|
|
||||||
const [provisionDefinitions, modelDefinitions, tankDefinitions] =
|
const [provisionDefinitions, modelDefinitions, tankDefinitions] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -69,6 +65,13 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
const duelStore = Duel.useStore();
|
const duelStore = Duel.useStore();
|
||||||
const mutateDuel = Duel.useMutation();
|
const mutateDuel = Duel.useMutation();
|
||||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||||
|
const nationColors = NATION_COLORS[protagonist.nation];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disturbed) {
|
||||||
|
document.body.classList.remove('no-navbar');
|
||||||
|
}
|
||||||
|
}, [disturbed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
@@ -92,16 +95,39 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="center">
|
<Flex
|
||||||
|
overflow="hidden"
|
||||||
|
justify="center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'black',
|
||||||
|
}}
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
style={{
|
||||||
|
opacity: disturbed ? 0 : 1,
|
||||||
|
background: `linear-gradient(${nationColors.background
|
||||||
|
.map((color) => Var(`${color}-2`))
|
||||||
|
.join(',')})`,
|
||||||
|
transitionDuration: '1s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <ScrollHint /> */}
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction={{ initial: 'column-reverse', md: 'row' }}
|
direction={{ initial: 'column-reverse', md: 'row' }}
|
||||||
style={{
|
style={{
|
||||||
zIndex: isFullScreen ? 2 : undefined,
|
zIndex: isFullScreen ? 2 : undefined,
|
||||||
backgroundColor: 'black',
|
transitionDuration: '1s',
|
||||||
|
background: isFullScreen ? 'black' : undefined,
|
||||||
}}
|
}}
|
||||||
height={
|
height={disturbed && !isFullScreen ? 'calc(100vh - 6rem)' : '100vh'}
|
||||||
isFullScreen ? '100vh' : `calc(100vh - ${NAVBAR_HEIGHT}px - 8rem)`
|
|
||||||
}
|
|
||||||
maxHeight={isFullScreen ? undefined : '60rem'}
|
maxHeight={isFullScreen ? undefined : '60rem'}
|
||||||
maxWidth={isFullScreen ? undefined : '120rem'}
|
maxWidth={isFullScreen ? undefined : '120rem'}
|
||||||
flexGrow="1"
|
flexGrow="1"
|
||||||
@@ -110,6 +136,8 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
top={isFullScreen ? '0' : undefined}
|
top={isFullScreen ? '0' : undefined}
|
||||||
left={isFullScreen ? '0' : undefined}
|
left={isFullScreen ? '0' : undefined}
|
||||||
>
|
>
|
||||||
|
<Title />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
className="tank-sandbox-container"
|
className="tank-sandbox-container"
|
||||||
flexGrow="1"
|
flexGrow="1"
|
||||||
@@ -122,27 +150,23 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
position="relative"
|
position="relative"
|
||||||
left={disturbed ? '0' : { initial: '0', md: '12.5%' }}
|
style={{
|
||||||
style={{ transitionDuration: '200ms' }}
|
transitionDuration: '1s',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{!revealed && (
|
<Box
|
||||||
<Spinner
|
style={{
|
||||||
style={{
|
width: '100%',
|
||||||
position: 'absolute',
|
height: '100%',
|
||||||
top: '50%',
|
transitionDuration: '2s',
|
||||||
left: '50%',
|
opacity: revealed ? 1 : 0,
|
||||||
transform: 'translate(-50%, -50%)',
|
filter: disturbed ? undefined : `drop-shadow(0 0 2rem black)`,
|
||||||
width: Var('space-6'),
|
}}
|
||||||
height: Var('space-6'),
|
>
|
||||||
}}
|
<Suspense>
|
||||||
/>
|
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||||
)}
|
</Suspense>
|
||||||
|
</Box>
|
||||||
{/* {skeleton && <TankSandboxLoader id={protagonist.id} />} */}
|
|
||||||
|
|
||||||
<Suspense>
|
|
||||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
|
||||||
</Suspense>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Options
|
<Options
|
||||||
@@ -153,157 +177,7 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Flex
|
<Title outline />
|
||||||
className="tankopedia-title-bar"
|
|
||||||
justify="center"
|
|
||||||
position={{ initial: 'static', md: 'absolute' }}
|
|
||||||
left={{
|
|
||||||
initial: '0',
|
|
||||||
md: disturbed ? '3' : '5',
|
|
||||||
lg: disturbed ? '7' : '9',
|
|
||||||
}}
|
|
||||||
pt="7"
|
|
||||||
pb="4"
|
|
||||||
top={{ initial: '8', md: '50%' }}
|
|
||||||
mt={{ initial: '0', md: '-9' }}
|
|
||||||
style={{
|
|
||||||
transitionDuration: '200ms',
|
|
||||||
userSelect: 'none',
|
|
||||||
// background: 'red',
|
|
||||||
}}
|
|
||||||
direction="column"
|
|
||||||
align="center"
|
|
||||||
gap="2"
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
gap="3"
|
|
||||||
width="100%"
|
|
||||||
justify={{ initial: 'center', md: 'start' }}
|
|
||||||
>
|
|
||||||
<Heading
|
|
||||||
color={treeColor}
|
|
||||||
trim="end"
|
|
||||||
size={
|
|
||||||
disturbed
|
|
||||||
? { initial: '6', lg: '7' }
|
|
||||||
: { initial: '7', lg: '8' }
|
|
||||||
}
|
|
||||||
style={{ transitionDuration: '200ms', position: 'relative' }}
|
|
||||||
>
|
|
||||||
<Icon width="1em" height="1em" />
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Heading
|
|
||||||
weight="medium"
|
|
||||||
size={
|
|
||||||
disturbed
|
|
||||||
? { initial: '7', lg: '8' }
|
|
||||||
: { initial: '8', lg: '9' }
|
|
||||||
}
|
|
||||||
style={{ transitionDuration: '200ms' }}
|
|
||||||
wrap="nowrap"
|
|
||||||
color={treeColor}
|
|
||||||
>
|
|
||||||
{unwrap(protagonist.name)}
|
|
||||||
</Heading>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
gap="4"
|
|
||||||
ml={disturbed ? '0' : '-2'}
|
|
||||||
style={{ transitionDuration: '200ms' }}
|
|
||||||
>
|
|
||||||
<Link color="gray" underline="always" href="/tools/tankopedia">
|
|
||||||
<Flex align="center" gap="1">
|
|
||||||
<CaretLeftIcon />
|
|
||||||
{strings.website.tools.tankopedia.meta.back}
|
|
||||||
</Flex>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
color="gray"
|
|
||||||
underline="always"
|
|
||||||
href={`/tools/compare?tanks=${compareTanks.join('%2C')}`}
|
|
||||||
>
|
|
||||||
<Flex align="center" gap="1">
|
|
||||||
<MixIcon />
|
|
||||||
{strings.website.tools.tankopedia.meta.compare}
|
|
||||||
</Flex>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Dialog.Root open={showSwapDialog} onOpenChange={setShowSwapDialog}>
|
|
||||||
<Dialog.Trigger>
|
|
||||||
<Link
|
|
||||||
color="gray"
|
|
||||||
underline="always"
|
|
||||||
href="#"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setShowSwapDialog(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex align="center" gap="1">
|
|
||||||
<UpdateIcon />
|
|
||||||
{strings.website.tools.tankopedia.meta.swap.button}
|
|
||||||
</Flex>
|
|
||||||
</Link>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
|
|
||||||
<Dialog.Content>
|
|
||||||
<Dialog.Title>
|
|
||||||
{strings.website.tools.tankopedia.meta.swap.title}
|
|
||||||
</Dialog.Title>
|
|
||||||
|
|
||||||
<TankSearch
|
|
||||||
compact
|
|
||||||
onSelect={(tank) => {
|
|
||||||
const model = modelDefinitions.models[tank.id];
|
|
||||||
|
|
||||||
setShowSwapDialog(false);
|
|
||||||
mutateTankopediaEphemeral((draft) => {
|
|
||||||
draft.model = modelDefinitions.models[tank.id];
|
|
||||||
});
|
|
||||||
mutateDuel((draft) => {
|
|
||||||
draft.protagonist = tankToDuelMember(
|
|
||||||
tank,
|
|
||||||
model,
|
|
||||||
provisionDefinitions,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.history.replaceState(
|
|
||||||
null,
|
|
||||||
'',
|
|
||||||
`/tools/tankopedia/${tank.id}`,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
|
|
||||||
{assertSecret(import.meta.env.PUBLIC_PROMOTE_OPENTEST) ===
|
|
||||||
'true' && (
|
|
||||||
<Link
|
|
||||||
color="jade"
|
|
||||||
href={`https://${
|
|
||||||
assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH) ===
|
|
||||||
'opentest'
|
|
||||||
? ''
|
|
||||||
: 'opentest.'
|
|
||||||
}blitzkit.app/tools/tankopedia/${protagonist.id}`}
|
|
||||||
>
|
|
||||||
<Flex align="center" gap="1">
|
|
||||||
<ScienceIcon width="1.25em" height="1.25em" />
|
|
||||||
{assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH) ===
|
|
||||||
'opentest'
|
|
||||||
? strings.website.tools.tankopedia.meta.vanilla
|
|
||||||
: strings.website.tools.tankopedia.meta.opentest}
|
|
||||||
</Flex>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function MetaSection() {
|
|||||||
const { locale, strings } = useLocale();
|
const { locale, strings } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="center">
|
<Flex justify="center" mt="-9">
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
background: `url(/assets/images/national-wallpapers/${protagonist.nation}.jpg)`,
|
background: `url(/assets/images/national-wallpapers/${protagonist.nation}.jpg)`,
|
||||||
@@ -50,7 +50,7 @@ export function MetaSection() {
|
|||||||
align="center"
|
align="center"
|
||||||
py="6rem"
|
py="6rem"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(${Var('gray-1')}, ${Var('black-a8')}, ${Var('black-a8')}, ${Var('gray-1')})`,
|
background: `linear-gradient(${Var('black-a8')}, ${Var('black-a8')}, ${Var('gray-1')})`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex direction="column" align="center" gap="6">
|
<Flex direction="column" align="center" gap="6">
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { youtubers } from '@blitzkit/core';
|
import { youtubers } from '@blitzkit/core';
|
||||||
import { literals } from '@blitzkit/i18n/src/literals';
|
|
||||||
import { Box, Flex, Heading, Link, Skeleton, Text } from '@radix-ui/themes';
|
import { Box, Flex, Heading, Link, Skeleton, Text } from '@radix-ui/themes';
|
||||||
import { awaitableReviews } from '../../core/awaitables/reviews';
|
import { awaitableReviews } from '../../core/awaitables/reviews';
|
||||||
import { Var } from '../../core/radix/var';
|
import { Var } from '../../core/radix/var';
|
||||||
@@ -91,38 +90,6 @@ export function VideoSection({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<Link
|
|
||||||
href="https://discord.gg/nDt7AjGJQH"
|
|
||||||
target="_blank"
|
|
||||||
color="gray"
|
|
||||||
highContrast
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
gap="2"
|
|
||||||
p="3"
|
|
||||||
direction="column"
|
|
||||||
height="100%"
|
|
||||||
style={{
|
|
||||||
backgroundColor: Var('color-panel-solid'),
|
|
||||||
borderRadius: Var('radius-3'),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<VerifiedIcon />
|
|
||||||
<Text
|
|
||||||
align="center"
|
|
||||||
style={{
|
|
||||||
width: '9rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{literals(strings.website.tools.tankopedia.review.join, [
|
|
||||||
'BlitzKit',
|
|
||||||
])}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Link>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
|||||||
@@ -117,6 +117,12 @@ export const chartsTool: Tool = {
|
|||||||
button: {
|
button: {
|
||||||
color: 'bronze',
|
color: 'bronze',
|
||||||
},
|
},
|
||||||
|
branches: ['main', 'dev'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const guessTool: Tool = {
|
||||||
|
id: 'guess',
|
||||||
|
button: { color: 'cyan' },
|
||||||
branches: ['main', 'dev', 'opentest', 'preview'],
|
branches: ['main', 'dev', 'opentest', 'preview'],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,6 +130,7 @@ export const TOOLS: Tool[] = [
|
|||||||
tankopediaTool,
|
tankopediaTool,
|
||||||
compareTool,
|
compareTool,
|
||||||
performanceTool,
|
performanceTool,
|
||||||
|
// guessTool,
|
||||||
// chartsTool,
|
// chartsTool,
|
||||||
galleryTool,
|
galleryTool,
|
||||||
sessionTool,
|
sessionTool,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as ColorsRaw from '@radix-ui/colors';
|
|||||||
|
|
||||||
type ExcludeSuffix<T> =
|
type ExcludeSuffix<T> =
|
||||||
T extends `${string}${'A' | `${'P3' | 'Dark'}${'' | 'A'}`}` ? never : T;
|
T extends `${string}${'A' | `${'P3' | 'Dark'}${'' | 'A'}`}` ? never : T;
|
||||||
type RadixColor = ExcludeSuffix<keyof typeof ColorsRaw>;
|
export type RadixColor = ExcludeSuffix<keyof typeof ColorsRaw>;
|
||||||
|
|
||||||
export type VarName =
|
export type VarName =
|
||||||
| `color-${'background' | `panel-${'solid' | 'translucent'}` | 'surface' | 'overlay'}`
|
| `color-${'background' | `panel-${'solid' | 'translucent'}` | 'surface' | 'overlay'}`
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
import '@fontsource/inter';
|
||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from '@radix-ui/themes';
|
||||||
import { BlitzKitTheme } from '../components/BlitzKitTheme';
|
import { BlitzKitTheme } from '../components/BlitzKitTheme';
|
||||||
import { Checks } from '../components/Checks';
|
import { Checks } from '../components/Checks';
|
||||||
@@ -10,9 +11,11 @@ import { DEFAULT_LOCALE } from '@blitzkit/i18n';
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
noFooter?: boolean;
|
noFooter?: boolean;
|
||||||
|
noNavbar?: boolean;
|
||||||
|
integrated?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { noFooter } = Astro.props;
|
const { noFooter, noNavbar, integrated } = Astro.props;
|
||||||
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,10 +56,10 @@ const locale = Astro.params.locale ?? DEFAULT_LOCALE;
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class={noNavbar ? 'no-navbar' : ''}>
|
||||||
<BlitzKitTheme>
|
<BlitzKitTheme>
|
||||||
<Flex direction="column" minHeight="100vh">
|
<Flex direction="column" minHeight="100vh">
|
||||||
<Navbar />
|
<Navbar {integrated} />
|
||||||
|
|
||||||
<Checks {locale} client:only="react" />
|
<Checks {locale} client:only="react" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
@import '@radix-ui/themes/styles.css';
|
@import '@radix-ui/themes/styles.css';
|
||||||
|
|
||||||
|
.radix-themes {
|
||||||
|
--default-font-family: 'Inter';
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,6 @@ import { literals } from '@blitzkit/i18n/src/literals';
|
|||||||
import { Box, Flex, Heading, Link } from '@radix-ui/themes';
|
import { Box, Flex, Heading, Link } from '@radix-ui/themes';
|
||||||
import type { GetStaticPaths } from 'astro';
|
import type { GetStaticPaths } from 'astro';
|
||||||
import { AesonPlug } from '../../components/AesonPlug';
|
import { AesonPlug } from '../../components/AesonPlug';
|
||||||
import { DiscordPlug } from '../../components/Discord';
|
|
||||||
import { HomeLocaleSwitcher } from '../../components/Home/HomeLocaleSwitcher';
|
|
||||||
import HomeHero from '../../components/HomeHero.astro';
|
import HomeHero from '../../components/HomeHero.astro';
|
||||||
import { HomePageHotTanks } from '../../components/HomePageHotTanks';
|
import { HomePageHotTanks } from '../../components/HomePageHotTanks';
|
||||||
import { Plugs } from '../../components/Plugs';
|
import { Plugs } from '../../components/Plugs';
|
||||||
@@ -57,7 +55,7 @@ export const getStaticPaths = (() => {
|
|||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<HomeHero />
|
<HomeHero />
|
||||||
|
|
||||||
<Flex justify="center" py="6" px="4" justify="center">
|
<!-- <Flex justify="center" py="6" px="4" justify="center">
|
||||||
<Flex
|
<Flex
|
||||||
wrap="wrap"
|
wrap="wrap"
|
||||||
justify="between"
|
justify="between"
|
||||||
@@ -71,7 +69,7 @@ export const getStaticPaths = (() => {
|
|||||||
<DiscordPlug {locale} skeleton slot="fallback" />
|
<DiscordPlug {locale} skeleton slot="fallback" />
|
||||||
</DiscordPlug>
|
</DiscordPlug>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex> -->
|
||||||
|
|
||||||
{
|
{
|
||||||
locale !== DEFAULT_LOCALE && (
|
locale !== DEFAULT_LOCALE && (
|
||||||
@@ -118,6 +116,7 @@ export const getStaticPaths = (() => {
|
|||||||
|
|
||||||
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
<Flex gap="4" direction={{ initial: 'column', sm: 'row' }}>
|
||||||
<!-- <ToolCard tool={chartsTool} /> -->
|
<!-- <ToolCard tool={chartsTool} /> -->
|
||||||
|
<!-- <ToolCard tool={guessTool} /> -->
|
||||||
<ToolCard tool={galleryTool} />
|
<ToolCard tool={galleryTool} />
|
||||||
<ToolCard tool={sessionTool} />
|
<ToolCard tool={sessionTool} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { Box, Flex, Heading } 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,
|
||||||
|
useLocale,
|
||||||
|
type LocaleAcceptorProps,
|
||||||
|
} from '../../../../hooks/useLocale';
|
||||||
|
import { Duel } from '../../../../stores/duel';
|
||||||
|
import { GuessEphemeral } from '../../../../stores/guessEphemeral';
|
||||||
|
import { TankopediaEphemeral } from '../../../../stores/tankopediaEphemeral';
|
||||||
|
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([
|
||||||
|
awaitableTankDefinitions,
|
||||||
|
awaitableModelDefinitions,
|
||||||
|
awaitableProvisionDefinitions,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ids = Object.keys(tankDefinitions.tanks);
|
||||||
|
|
||||||
|
export function Page({ locale }: LocaleAcceptorProps) {
|
||||||
|
const initialId = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||||
|
const initialTank = tankDefinitions.tanks[initialId];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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() {
|
||||||
|
const { unwrap } = useLocale();
|
||||||
|
const tank = GuessEphemeral.use((state) => state.tank);
|
||||||
|
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||||
|
const isRevealed = guessState !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexGrow="1" position="relative" overflow="hidden">
|
||||||
|
<Background />
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
left="50%"
|
||||||
|
top="50%"
|
||||||
|
ml="9"
|
||||||
|
style={{
|
||||||
|
opacity: isRevealed ? 1 : 0,
|
||||||
|
transitionDuration: isRevealed ? '2s' : undefined,
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading weight="medium" size="9">
|
||||||
|
{unwrap(tank.name)}
|
||||||
|
</Heading>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
top="0"
|
||||||
|
right={isRevealed ? '16rem' : '0'}
|
||||||
|
style={{
|
||||||
|
transitionDuration: isRevealed ? '2s' : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Suspense fallback={<GuessRendererLoader />}>
|
||||||
|
<GuessRenderer />
|
||||||
|
</Suspense>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
position="absolute"
|
||||||
|
left="50%"
|
||||||
|
top="50%"
|
||||||
|
ml="9"
|
||||||
|
style={{
|
||||||
|
opacity: isRevealed ? 1 : 0,
|
||||||
|
transitionDuration: isRevealed ? '2s' : undefined,
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
color: 'transparent',
|
||||||
|
WebkitTextStroke: `1px ${Var('gray-12')}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading
|
||||||
|
style={{
|
||||||
|
WebkitTextStroke: `1px inherit`,
|
||||||
|
}}
|
||||||
|
weight="medium"
|
||||||
|
size="9"
|
||||||
|
>
|
||||||
|
{unwrap(tank.name)}
|
||||||
|
</Heading>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Guesser />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,81 @@
|
|||||||
|
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 guessState = 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={{
|
||||||
|
transitionDuration: '2s',
|
||||||
|
backgroundColor: Var(
|
||||||
|
guessState === null
|
||||||
|
? 'accent-2'
|
||||||
|
: guessState
|
||||||
|
? 'jade-3'
|
||||||
|
: 'tomato-3',
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(circle, ${Var('color-panel-translucent')}, ${Var('color-panel-solid')})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Canvas } from '@react-three/fiber';
|
||||||
|
import { Controls } from '../../../../../components/Tankopedia/HeroSection/components/TankSandbox/components/Control';
|
||||||
|
import { Lighting } from '../../../../../components/Tankopedia/HeroSection/components/TankSandbox/components/Lighting';
|
||||||
|
import { TankModel } from '../../../../../components/Tankopedia/HeroSection/components/TankSandbox/components/TankModel';
|
||||||
|
import { GuessEphemeral } from '../../../../../stores/guessEphemeral';
|
||||||
|
import { TankopediaDisplay } from '../../../../../stores/tankopediaPersistent/constants';
|
||||||
|
|
||||||
|
const BRIGHTNESS = 2 ** -5;
|
||||||
|
const CONTRAST = 2 ** 2;
|
||||||
|
|
||||||
|
export function GuessRenderer() {
|
||||||
|
const guessState = GuessEphemeral.use((state) => state.guessState);
|
||||||
|
const isRevealed = guessState !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Canvas
|
||||||
|
style={{
|
||||||
|
filter: `grayscale(${isRevealed ? 0 : 1}) contrast(${isRevealed ? 1 : CONTRAST}) brightness(${isRevealed ? 1 : BRIGHTNESS})`,
|
||||||
|
// filter: `drop-shadow(0 0 1rem ${Var('black-a11')})`,
|
||||||
|
transitionDuration: isRevealed ? '2s' : undefined,
|
||||||
|
transitionProperty: 'filter',
|
||||||
|
}}
|
||||||
|
camera={{ position: [-5, 5, -5] }}
|
||||||
|
>
|
||||||
|
<Controls distanceScale={2} zoomable={false} />
|
||||||
|
<Lighting display={TankopediaDisplay.Model} />
|
||||||
|
<TankModel />
|
||||||
|
</Canvas>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import {
|
||||||
|
SEARCH_KEYS,
|
||||||
|
TankDefinition,
|
||||||
|
TIER_ROMAN_NUMERALS,
|
||||||
|
} from '@blitzkit/core';
|
||||||
|
import { literals } from '@blitzkit/i18n';
|
||||||
|
import {
|
||||||
|
ArrowRightIcon,
|
||||||
|
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, useEffect, 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 { GuessEphemeral } from '../../../../../stores/guessEphemeral';
|
||||||
|
|
||||||
|
const { go } = fuzzysort;
|
||||||
|
|
||||||
|
const [tankNames, tankDefinitions] = await Promise.all([
|
||||||
|
awaitableTankNames,
|
||||||
|
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 correctGuesses = GuessEphemeral.use((state) => state.correctGuesses);
|
||||||
|
const totalGuesses = GuessEphemeral.use((state) => state.totalGuesses);
|
||||||
|
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);
|
||||||
|
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),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (guessState !== null) setSelected(null);
|
||||||
|
}, [guessState]);
|
||||||
|
|
||||||
|
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 justify="center">
|
||||||
|
<Text>
|
||||||
|
{literals(strings.website.tools.guess.stats, [
|
||||||
|
`${correctGuesses}`,
|
||||||
|
`${totalGuesses}`,
|
||||||
|
])}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex gap="3">
|
||||||
|
<TextField.Root
|
||||||
|
disabled={guessState !== null}
|
||||||
|
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"
|
||||||
|
color={guessState === null && selected === null ? 'red' : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (guessState === null) {
|
||||||
|
const correct = selected !== null && selected.id === tank.id;
|
||||||
|
|
||||||
|
mutateGuessEphemeral((draft) => {
|
||||||
|
draft.guessState = correct;
|
||||||
|
draft.totalGuesses++;
|
||||||
|
draft.correctGuesses += correct ? 1 : 0;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const id = Number(ids[Math.floor(Math.random() * ids.length)]);
|
||||||
|
const tank = tankDefinitions.tanks[id];
|
||||||
|
|
||||||
|
mutateGuessEphemeral((draft) => {
|
||||||
|
if (!input.current) return;
|
||||||
|
|
||||||
|
draft.tank = tank;
|
||||||
|
draft.guessState = null;
|
||||||
|
input.current.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{guessState === null ? (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
strings.website.tools.guess.search[
|
||||||
|
selected === null ? 'skip' : 'guess'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
{selected === null ? <ArrowRightIcon /> : <PaperPlaneIcon />}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{strings.website.tools.guess.search.next}
|
||||||
|
<ArrowRightIcon />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
import { PageWrapper } from '../../../../components/PageWrapper';
|
||||||
|
import BaseLayout from '../../../../layouts/BaseLayout.astro';
|
||||||
|
import { Page } from './_index';
|
||||||
|
|
||||||
|
export { getStaticPaths } from '../../index.astro';
|
||||||
|
|
||||||
|
const { locale } = Astro.params;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout noFooter>
|
||||||
|
<PageWrapper p="0" maxWidth="unset" color="cyan">
|
||||||
|
<Page {locale} client:load />
|
||||||
|
</PageWrapper>
|
||||||
|
</BaseLayout>
|
||||||
@@ -46,8 +46,8 @@ export function Page({ id, skeleton, locale }: PageProps) {
|
|||||||
>
|
>
|
||||||
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
|
<PageWrapper p="0" maxWidth="unset" color="purple" gap="9" pb="9">
|
||||||
<HeroSection skeleton={skeleton} />
|
<HeroSection skeleton={skeleton} />
|
||||||
<CalloutsSection />
|
|
||||||
<MetaSection />
|
<MetaSection />
|
||||||
|
<CalloutsSection />
|
||||||
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
{tank.type === TankType.RESEARCHABLE && !tank.deprecated && (
|
||||||
<TechTreeSection skeleton={skeleton} />
|
<TechTreeSection skeleton={skeleton} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const description = literals(
|
|||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
<meta name="twitter:image:src" content={icon} />
|
<meta name="twitter:image:src" content={icon} />
|
||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout integrated>
|
||||||
<Page {locale} id={id} client:only="react">
|
<Page {locale} id={id} client:only="react">
|
||||||
<Page {locale} id={id} skeleton slot="fallback" />
|
<Page {locale} id={id} skeleton slot="fallback" />
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
totalGuesses: number;
|
||||||
|
correctGuesses: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GuessEphemeral = createContextualStore((tank: TankDefinition) =>
|
||||||
|
create<GuessEphemeral>()(() => ({
|
||||||
|
tank,
|
||||||
|
guessState: null,
|
||||||
|
totalGuesses: 0,
|
||||||
|
correctGuesses: 0,
|
||||||
|
})),
|
||||||
|
);
|
||||||
Reference in new issue
Block a user