mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-14 13:16:11 +01:00
Merge branch 'main' into pr/Hitjack007/378-2
This commit is contained in:
commit
c82d2ccf1c
115 files changed
+2967
-1299
No files matched your search
@@ -1,38 +0,0 @@
|
||||
---
|
||||
import { tankIcon } from "@blitzkit/core";
|
||||
import { Flex, Heading, Link, Strong, Text } from "@radix-ui/themes";
|
||||
---
|
||||
|
||||
<Flex justify="center" px="4">
|
||||
<Link
|
||||
color="gray"
|
||||
highContrast
|
||||
style={{ display: "content" }}
|
||||
href="/docs/guides/christmas-2025"
|
||||
underline="none"
|
||||
>
|
||||
<Flex
|
||||
mt="3"
|
||||
px="5"
|
||||
py="4"
|
||||
style={{
|
||||
borderRadius: "var(--radius-3)",
|
||||
backgroundImage: "url(https://i.imgur.com/LRYmSkg.png)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
align="center"
|
||||
gap="5"
|
||||
>
|
||||
<img src={tankIcon(25169)} style={{ height: "min(15vw, 5rem)" }} />
|
||||
|
||||
<Flex direction="column">
|
||||
<Heading size={{ initial: "4", sm: "6" }}>Christmas 2025</Heading>
|
||||
<Text size={{ initial: "2", sm: "3" }}>
|
||||
Win the <Strong>GSOR the TANK</Strong> and more!
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Link>
|
||||
</Flex>
|
||||
@@ -0,0 +1,36 @@
|
||||
.outer {
|
||||
padding: 0 var(--space-4);
|
||||
margin-top: var(--space-3);
|
||||
|
||||
img {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: relative;
|
||||
|
||||
background: linear-gradient(var(--pink-3), var(--purple-2));
|
||||
box-shadow: var(--shadow-3);
|
||||
|
||||
max-width: 40rem;
|
||||
|
||||
padding: var(--space-5);
|
||||
border-radius: var(--radius-4);
|
||||
|
||||
.important {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 50%;
|
||||
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
background-color: inherit;
|
||||
|
||||
padding: var(--space-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { HeartFilledIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Em, Flex, Strong, Text } from "@radix-ui/themes";
|
||||
import { donationTiers } from "../../config/donations";
|
||||
import styles from "./index.module.css";
|
||||
|
||||
export function Beg() {
|
||||
return (
|
||||
<Flex align="center" gap="4" direction="column" className={styles.outer}>
|
||||
<Text size="3" className={styles.important}>
|
||||
<Flex gap="2" align="center">
|
||||
<img src="https://github.com/tresabhi.png" />
|
||||
<Em>We need your attention</Em>
|
||||
</Flex>
|
||||
</Text>
|
||||
|
||||
<Flex direction="column" className={styles.center} gap="4">
|
||||
<Flex direction="column" gap="3">
|
||||
<Text color="tomato">
|
||||
We can no longer keep BlitzKit sustainable for <Strong>free</Strong>
|
||||
.
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
We want to add tank modules, player statistics, rebalancing patch
|
||||
notes, bring back the Discord bot, and more.
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
But paying out of pocket makes that impossible. We are an earnest
|
||||
non-profit, so consider donating to make the above possible. 100% of
|
||||
your money goes back into BlitzKit.
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3">
|
||||
{donationTiers.map(({ name, value, link }) => (
|
||||
<a target="_blank" key={name} href={link}>
|
||||
<Button size="3" color="crimson">
|
||||
{value === null && <HeartFilledIcon />}
|
||||
{value === null ? "Custom" : `$${value}`}
|
||||
</Button>
|
||||
</a>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+10
-10
@@ -3,9 +3,9 @@ import { useEffect, useState, type ReactNode } from "react";
|
||||
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
|
||||
import { InlineSkeleton } from "./InlineSkeleton";
|
||||
|
||||
const TIME = 1764898200 * 1000;
|
||||
const TIME = 1792598400 * 1000;
|
||||
|
||||
export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
export function Countdown({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const [timeLeft, setTimeLeft] = useState<number>(TIME - Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,14 +31,14 @@ export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
|
||||
string = (
|
||||
<>
|
||||
in{" "}
|
||||
revealed in{" "}
|
||||
<Code highContrast>
|
||||
{hours}h {minutes}m {seconds}s
|
||||
</Code>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
string = "has ended. Come again later!";
|
||||
string = "have been revealed.";
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -47,7 +47,7 @@ export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
color="gray"
|
||||
highContrast
|
||||
style={{ display: "content" }}
|
||||
href="https://discord.gg/njQdBhNS?event=1446208951136813117"
|
||||
href="https://preview.blitzkit.app/tanks"
|
||||
underline="none"
|
||||
>
|
||||
<Flex
|
||||
@@ -56,17 +56,17 @@ export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
py="4"
|
||||
style={{
|
||||
borderRadius: "var(--radius-3)",
|
||||
backgroundImage: "url(https://i.imgur.com/LRYmSkg.png)",
|
||||
backgroundImage: "url(https://i.imgur.com/xgngxoy.png)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
boxSizing: "border-box",
|
||||
boxShadow: "0 0 var(--space-7) var(--blue-a5)",
|
||||
boxShadow: "0 0 var(--space-7) var(--white-a2)",
|
||||
}}
|
||||
align="center"
|
||||
gap="5"
|
||||
>
|
||||
<img
|
||||
src="https://i.imgur.com/mvmJQgB.png"
|
||||
src="https://i.imgur.com/kXV3r3P.png"
|
||||
style={{
|
||||
height: "min(15vw, 5rem)",
|
||||
filter: "drop-shadow(0.5rem 0.5rem 1rem #00000080)",
|
||||
@@ -75,11 +75,11 @@ export function Giveaway({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
|
||||
<Flex direction="column">
|
||||
<Heading size={{ initial: "4", sm: "6" }}>
|
||||
Music Box giveaway {!skeleton && string}{" "}
|
||||
Tier XIs {!skeleton && string}{" "}
|
||||
{skeleton && <InlineSkeleton width="10rem" />}
|
||||
</Heading>
|
||||
<Text size={{ initial: "2", sm: "3" }}>
|
||||
Join the BlitzKit Discord server event
|
||||
Get ready to go to the Tankopedia!
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
import { Grid, Text } from "@radix-ui/themes";
|
||||
import { credits } from "../config/credits";
|
||||
import { getStrings } from "../core/i18n/getStrings";
|
||||
|
||||
interface Props {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
|
||||
const strings = getStrings(locale);
|
||||
---
|
||||
|
||||
<style>
|
||||
.credits {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
padding: var(--space-4);
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
|
||||
width: 100%;
|
||||
max-width: 80rem;
|
||||
|
||||
.list {
|
||||
gap: var(--space-5);
|
||||
|
||||
.link {
|
||||
display: contents;
|
||||
|
||||
.person {
|
||||
--profile-size: 5rem;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: var(--space-4);
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--profile-size) / 2);
|
||||
gap: var(--space-1);
|
||||
|
||||
box-shadow: var(--shadow-3);
|
||||
border-radius: var(--radius-3);
|
||||
|
||||
.contain {
|
||||
border-radius: inherit;
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.hint {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
filter: saturate(20) brightness(0.125) blur(4rem);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
|
||||
position: relative;
|
||||
|
||||
margin-top: calc(var(--profile-size) / -2);
|
||||
|
||||
object-fit: cover;
|
||||
width: var(--profile-size);
|
||||
height: var(--profile-size);
|
||||
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="credits">
|
||||
<div class="inner">
|
||||
<Grid
|
||||
className="list"
|
||||
py="4"
|
||||
columns="repeat(auto-fill, minmax(16rem, 1fr))"
|
||||
flow="dense"
|
||||
gap="4"
|
||||
gapY="8"
|
||||
>
|
||||
{
|
||||
credits.map((person) => (
|
||||
<a target="_blank" href={person.url} class="link">
|
||||
<div class="person">
|
||||
<div class="contain">
|
||||
<div
|
||||
class="hint"
|
||||
style={`background-image: url(${person.image})`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<img src={person.image} />
|
||||
|
||||
<Text weight="medium" className="text" color="gray" highContrast>
|
||||
{person.name}
|
||||
</Text>
|
||||
<Text className="text" color="gray" wrap="pretty">
|
||||
{person.description}
|
||||
</Text>
|
||||
</div>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,9 +43,9 @@ const strings = getStrings(locale);
|
||||
}}
|
||||
>
|
||||
<Heading>BlitzKit</Heading>
|
||||
<Text color="gray">{strings.website.footer.motto}</Text>
|
||||
<Text size="1" color="gray">
|
||||
{strings.website.footer.copyright}
|
||||
<Text size="2" color="gray">{strings.website.footer.motto}</Text>
|
||||
<Text size="2" color="gray">
|
||||
{strings.website.footer.association}
|
||||
</Text>
|
||||
|
||||
<Box mt="2">
|
||||
@@ -84,9 +84,6 @@ const strings = getStrings(locale);
|
||||
sm: "start",
|
||||
}}
|
||||
>
|
||||
<LinkI18n {locale} href="/docs/about/credits" color="gray">
|
||||
{strings.website.footer.about.credits}
|
||||
</LinkI18n>
|
||||
<Link
|
||||
href="https://discord.gg/nDt7AjGJQH"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { Link, Table } from '@radix-ui/themes';
|
||||
import MarkdownToJSX from 'markdown-to-jsx';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { docImageUrl, isExternalImage } from '../core/blitzkit/docImages';
|
||||
|
||||
interface MarkdownProps {
|
||||
children: string;
|
||||
}
|
||||
|
||||
function Image(props: ComponentProps<'img'>) {
|
||||
return <img style={{ maxWidth: '100%' }} {...props} />;
|
||||
function Image({ src, ...props }: ComponentProps<'img'>) {
|
||||
return (
|
||||
<img
|
||||
style={{ maxWidth: '100%' }}
|
||||
src={src && isExternalImage(src) ? docImageUrl(src) : src}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Markdown({ children }: MarkdownProps) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
GearIcon,
|
||||
HamburgerMenuIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Box, Code, Flex, IconButton, Link } from "@radix-ui/themes";
|
||||
import { Box, Code, Flex, Grid, IconButton, Link } from "@radix-ui/themes";
|
||||
import { resolveBranchName } from "../core/blitzkit/branches";
|
||||
import { getStrings } from "../core/i18n/getStrings";
|
||||
import { Var } from "../core/radix/var";
|
||||
@@ -20,7 +20,7 @@ export interface Props {
|
||||
|
||||
const { integrated = false } = Astro.props;
|
||||
const toolsFiltered = Object.values(tools).filter(
|
||||
(tool) => !tool.href && !tool.disabled
|
||||
(tool) => !tool.href && !tool.disabled,
|
||||
);
|
||||
const { locale } = Astro.props;
|
||||
const branchName = resolveBranchName(locale);
|
||||
@@ -105,7 +105,7 @@ const strings = getStrings(locale);
|
||||
<Flex
|
||||
className="content"
|
||||
width="100%"
|
||||
p={{ initial: "4", sm: "3" }}
|
||||
p="3"
|
||||
direction="column"
|
||||
align="center"
|
||||
>
|
||||
@@ -133,41 +133,6 @@ const strings = getStrings(locale);
|
||||
|
||||
<Box flexGrow="1" />
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
gap={{ initial: "3", md: "6" }}
|
||||
display={{ initial: "none", sm: "flex" }}
|
||||
justify="center"
|
||||
className="tools"
|
||||
>
|
||||
{
|
||||
toolsFiltered.map((tool) => {
|
||||
const selected = Astro.url.pathname.includes(`/${tool.id}`);
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
(tool.strings ??
|
||||
tool.id) as keyof typeof strings.website.tools
|
||||
];
|
||||
|
||||
return (
|
||||
<LinkI18n
|
||||
{locale}
|
||||
weight={selected ? "medium" : "regular"}
|
||||
color="gray"
|
||||
highContrast={selected}
|
||||
size={{ initial: "1", md: "2" }}
|
||||
href={`/${tool.id}`}
|
||||
underline="hover"
|
||||
>
|
||||
{toolStrings.name}
|
||||
</LinkI18n>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
|
||||
<Box flexGrow="1" />
|
||||
|
||||
<Flex align="center" gap="4">
|
||||
<Link
|
||||
style={{ display: "flex", alignContent: "center" }}
|
||||
@@ -191,7 +156,7 @@ const strings = getStrings(locale);
|
||||
|
||||
<NavbarAccount {locale} client:idle />
|
||||
|
||||
<Flex align="center" display={{ initial: "flex", sm: "none" }}>
|
||||
<Flex align="center">
|
||||
<IconButton variant="ghost" color="gray" className="hamburger">
|
||||
<Cross1Icon data-expanded />
|
||||
<HamburgerMenuIcon data-closed />
|
||||
@@ -200,11 +165,10 @@ const strings = getStrings(locale);
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
<Grid
|
||||
data-expanded
|
||||
columns="repeat(auto-fill, minmax(30rem, 1fr))"
|
||||
gap="3"
|
||||
display={{ initial: "flex", sm: "none" }}
|
||||
>
|
||||
{
|
||||
toolsFiltered.map((tool) => {
|
||||
@@ -229,7 +193,7 @@ const strings = getStrings(locale);
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
py="4"
|
||||
p="6"
|
||||
justify="end"
|
||||
flexGrow="1"
|
||||
style={{
|
||||
@@ -243,7 +207,7 @@ const strings = getStrings(locale);
|
||||
);
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function StupidCat() {
|
||||
return <img src="https://i.imgur.com/6FBBzb9.png" />;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
normalizeBoundingBox,
|
||||
resolveDpm,
|
||||
unionBoundingBox,
|
||||
tankBoundingBox,
|
||||
type TankDefinition,
|
||||
} from "@blitzkit/core";
|
||||
import { useMemo } from "react";
|
||||
@@ -112,10 +112,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
return (tank.camouflage_still * gun.camouflage_loss * 100).toFixed(0);
|
||||
case "survivability.volume": {
|
||||
const dimensions = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.bounding_box!,
|
||||
turretModelDefinition.bounding_box!,
|
||||
),
|
||||
tankBoundingBox(tankModelDefinition, turretModelDefinition),
|
||||
);
|
||||
|
||||
return (dimensions.x * dimensions.y * dimensions.z).toFixed(0);
|
||||
@@ -123,10 +120,7 @@ export function TankSearchCard({ tank, onSelect }: TankSearchCardProps) {
|
||||
|
||||
case "survivability.length": {
|
||||
const bounds = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
tankModelDefinition.bounding_box!,
|
||||
turretModelDefinition.bounding_box!,
|
||||
),
|
||||
tankBoundingBox(tankModelDefinition, turretModelDefinition),
|
||||
);
|
||||
|
||||
return Math.max(bounds.x, bounds.y, bounds.z).toFixed(0);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
normalizeBoundingBox,
|
||||
resolveDpm,
|
||||
SEARCH_KEYS,
|
||||
unionBoundingBox,
|
||||
tankBoundingBox,
|
||||
type TankDefinition,
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
@@ -137,16 +137,10 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const bTurretModelDefinition =
|
||||
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
|
||||
const aSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
aTankModelDefinition.bounding_box!,
|
||||
aTurretModelDefinition.bounding_box!,
|
||||
),
|
||||
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition),
|
||||
);
|
||||
const bSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
bTankModelDefinition.bounding_box!,
|
||||
bTurretModelDefinition.bounding_box!,
|
||||
),
|
||||
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition),
|
||||
);
|
||||
const aVolume = aSize.x * aSize.y * aSize.z;
|
||||
const bVolume = bSize.x * bSize.y * bSize.z;
|
||||
@@ -164,16 +158,10 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const bTurretModelDefinition =
|
||||
bTankModelDefinition.turrets[b.turrets.at(-1)!.id];
|
||||
const aSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
aTankModelDefinition.bounding_box!,
|
||||
aTurretModelDefinition.bounding_box!,
|
||||
),
|
||||
tankBoundingBox(aTankModelDefinition, aTurretModelDefinition),
|
||||
);
|
||||
const bSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
bTankModelDefinition.bounding_box!,
|
||||
bTurretModelDefinition.bounding_box!,
|
||||
),
|
||||
tankBoundingBox(bTankModelDefinition, bTurretModelDefinition),
|
||||
);
|
||||
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
||||
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
||||
|
||||
+3
-1
@@ -44,7 +44,9 @@ export function GunFlexibilityVisualizer({
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const turret = Duel.use((state) => state.protagonist.turret);
|
||||
const gun = Duel.use((state) => state.protagonist.gun);
|
||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||
const modelRequested = Tankopedia.use(
|
||||
(state) => state.loaders.model.requested,
|
||||
);
|
||||
|
||||
const tankModel = modelDefinition.models[tank.id];
|
||||
const turretModel = tankModel.turrets[turret.id];
|
||||
|
||||
+30
-8
@@ -1,5 +1,5 @@
|
||||
import { createDefaultSkills } from "@blitzkit/core";
|
||||
import { Flex, Progress, Text } from "@radix-ui/themes";
|
||||
import { Box, Flex, Progress, Text } from "@radix-ui/themes";
|
||||
import { clamp } from "lodash-es";
|
||||
import { memo, useMemo, type ComponentProps, type ReactNode } from "react";
|
||||
import { api } from "../../../../../../core/blitzkit/api";
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
TankopediaRelativeAgainst,
|
||||
} from "../../../../../../stores/tankopedia";
|
||||
import { Info, type InfoProps } from "./Info";
|
||||
import { StatScale } from "./StatScale";
|
||||
|
||||
type InfoWithDeltaProps = Omit<InfoProps, "name"> & {
|
||||
stats: TankCharacteristics;
|
||||
@@ -122,6 +123,15 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
return othersValue !== undefined;
|
||||
}) as TankCharacteristics[];
|
||||
}, [relativeAgainst, shellIndex, equipmentMatrix, equalize]);
|
||||
const values = useMemo(
|
||||
() =>
|
||||
others.map((tank) =>
|
||||
typeof props.value === "function"
|
||||
? props.value(tank)!
|
||||
: (tank[props.value] as number),
|
||||
),
|
||||
[others],
|
||||
);
|
||||
const betterTanks = others.filter((tank) => {
|
||||
const othersValue =
|
||||
typeof props.value === "function"
|
||||
@@ -161,13 +171,25 @@ export const InfoWithDelta = memo<InfoWithDeltaProps>(
|
||||
|
||||
{!noRanking && (
|
||||
<Flex pl={indent ? "2" : "0"} align="center" gap="2">
|
||||
<Progress
|
||||
variant="soft"
|
||||
size="1"
|
||||
value={goodness * 100}
|
||||
color={color}
|
||||
style={{ height: "0.125rem", opacity: 0.5 }}
|
||||
/>
|
||||
<Box position="relative" flexGrow="1">
|
||||
<StatScale
|
||||
values={values}
|
||||
value={uhWhatDoICallThisVariable}
|
||||
lowerIsBetter={deltaType === "lowerIsBetter"}
|
||||
/>
|
||||
|
||||
<Progress
|
||||
variant="soft"
|
||||
size="1"
|
||||
value={goodness * 100}
|
||||
color={color}
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "0.125rem",
|
||||
opacity: 0.75,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Text color="gray" size="1">
|
||||
{clamp(betterTanks.length + 1, 1, others.length)} /{" "}
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { memo, useEffect, useMemo, useRef } from "react";
|
||||
|
||||
const RESOLUTION = 1000;
|
||||
const HEIGHT = 8;
|
||||
const DENSE_TICKS = 100;
|
||||
|
||||
interface StatScaleProps {
|
||||
values: number[];
|
||||
value: number;
|
||||
lowerIsBetter?: boolean;
|
||||
}
|
||||
|
||||
export const StatScale = memo<StatScaleProps>(
|
||||
({ values, value, lowerIsBetter }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const ticks = useMemo(() => {
|
||||
const min = Math.min(value, ...values);
|
||||
const max = Math.max(value, ...values);
|
||||
const range = max - min;
|
||||
const positions = new Set<number>();
|
||||
|
||||
for (const other of values) {
|
||||
const raw = range === 0 ? 0.5 : (other - min) / range;
|
||||
const x = Math.round((lowerIsBetter ? 1 - raw : raw) * RESOLUTION);
|
||||
|
||||
if (x === 0 || x === RESOLUTION) continue;
|
||||
|
||||
positions.add(x);
|
||||
}
|
||||
|
||||
return Array.from(positions, (x) => ({ x }));
|
||||
}, [values, value, lowerIsBetter]);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const draw = () => {
|
||||
const { width } = canvas.getBoundingClientRect();
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
const canvasWidth = Math.ceil(width * pixelRatio);
|
||||
const canvasHeight = Math.ceil(HEIGHT * pixelRatio);
|
||||
|
||||
if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
}
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return;
|
||||
|
||||
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
context.clearRect(0, 0, width, HEIGHT);
|
||||
|
||||
const styles = getComputedStyle(canvas);
|
||||
for (const { x } of ticks) {
|
||||
context.fillStyle = styles.getPropertyValue("--gray-a6").trim();
|
||||
context.fillRect((x / RESOLUTION) * width, 0, 1, HEIGHT);
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(draw);
|
||||
observer.observe(canvas);
|
||||
window.addEventListener("resize", draw);
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", draw);
|
||||
};
|
||||
}, [ticks]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: HEIGHT,
|
||||
margin: "auto 0",
|
||||
opacity: Math.min(1, Math.sqrt(DENSE_TICKS / ticks.length)),
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { Flex, IconButton, type FlexProps } from "@radix-ui/themes";
|
||||
import type { ComponentProps, CSSProperties } from "react";
|
||||
|
||||
type SidebarIconButtonProps = ComponentProps<typeof IconButton> & {
|
||||
selected: boolean;
|
||||
src: string;
|
||||
alt: string;
|
||||
imageStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
export function SidebarIconGroup({ style, ...props }: FlexProps) {
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)", ...style }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarIconButton({
|
||||
selected,
|
||||
src,
|
||||
alt,
|
||||
imageStyle,
|
||||
...props
|
||||
}: SidebarIconButtonProps) {
|
||||
return (
|
||||
<IconButton
|
||||
color={selected ? undefined : "gray"}
|
||||
variant="soft"
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="none"
|
||||
{...props}
|
||||
>
|
||||
<img
|
||||
alt={alt}
|
||||
src={src}
|
||||
style={{ width: "50%", height: "50%", ...imageStyle }}
|
||||
/>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import { alias } from "@blitzkit/core";
|
||||
import { Flex, Tooltip } from "@radix-ui/themes";
|
||||
import { uniqBy } from "lodash-es";
|
||||
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
|
||||
import { api } from "../../../../../../core/blitzkit/api";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
||||
import { SidebarIconButton } from "./SidebarIconButton";
|
||||
|
||||
const camouflageDefinitions = await api.camouflageDefinitions();
|
||||
|
||||
export function SkinSwitcher() {
|
||||
const tank = Duel.use((state) => state.protagonist.tank);
|
||||
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
|
||||
const attachment = Tankopedia.use(
|
||||
(state) => state.loaders.attachment.requested,
|
||||
);
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
const { unwrap } = useLocale();
|
||||
const skins = uniqBy(
|
||||
Object.values(camouflageDefinitions.camouflages).filter(
|
||||
(camouflage) => camouflage.tank_id === tank.id,
|
||||
),
|
||||
(camouflage) => unwrap(camouflage.name!),
|
||||
);
|
||||
|
||||
// attachments aren't camouflages so they get their own group
|
||||
const groups = [
|
||||
skins.filter((camouflage) => !camouflage.attachment),
|
||||
skins.filter((camouflage) => camouflage.attachment),
|
||||
].filter((group) => group.length > 0);
|
||||
|
||||
if (skins.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="3"
|
||||
top="50%"
|
||||
right={
|
||||
disturbed && requestedDisplay === TankopediaDisplay.Model
|
||||
? "3"
|
||||
: "-4rem"
|
||||
}
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: "translateY(-50%)",
|
||||
transitionDuration: "200ms",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<Flex key={group[0].id} direction="column">
|
||||
{group.map((camouflage, index) => {
|
||||
const name = unwrap(camouflage.name!).replace(
|
||||
"%(camo_num)",
|
||||
SKIN_NUMBER,
|
||||
);
|
||||
const top = index === 0 ? "var(--radius-full)" : "0";
|
||||
const bottom =
|
||||
index === group.length - 1 ? "var(--radius-full)" : "0";
|
||||
|
||||
return (
|
||||
<Tooltip key={camouflage.id} content={name} side="left">
|
||||
<SidebarIconButton
|
||||
selected={
|
||||
(camouflage.attachment ? attachment : skin) ===
|
||||
camouflage.id
|
||||
}
|
||||
alt={name}
|
||||
src={alias("api", `/icons/camouflages/${camouflage.id}.webp`)}
|
||||
size={{ initial: "3", sm: "4" }}
|
||||
radius="full"
|
||||
style={{
|
||||
borderRadius: `${top} ${top} ${bottom} ${bottom}`,
|
||||
}}
|
||||
imageStyle={{
|
||||
width: "70%",
|
||||
height: "70%",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
onClick={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
const loader =
|
||||
draft.loaders[
|
||||
camouflage.attachment ? "attachment" : "skin"
|
||||
];
|
||||
|
||||
loader.requested =
|
||||
loader.requested === camouflage.id
|
||||
? undefined
|
||||
: camouflage.id;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+17
-28
@@ -46,6 +46,11 @@ import { TankSearch } from "../../../../TankSearch";
|
||||
import { CustomShellButton } from "./components/CustomShellButton";
|
||||
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
||||
import { QuickInputs } from "./components/QuickInputs";
|
||||
import {
|
||||
SidebarIconButton,
|
||||
SidebarIconGroup,
|
||||
} from "./components/SidebarIconButton";
|
||||
import { SkinSwitcher } from "./components/SkinSwitcher";
|
||||
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
|
||||
import { Thicknesses } from "./components/Thicknesses";
|
||||
|
||||
@@ -77,7 +82,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
const revealed = Tankopedia.use((state) => state.revealed);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||
const modelRequested = Tankopedia.use(
|
||||
(state) => state.loaders.model.requested,
|
||||
);
|
||||
const equalize = Duel.use((state) => state.equalize);
|
||||
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
|
||||
|
||||
@@ -87,6 +94,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
|
||||
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
||||
|
||||
<SkinSwitcher />
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
direction="column"
|
||||
@@ -181,24 +190,13 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
</Popover.Root>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
style={{
|
||||
borderRadius: "var(--radius-full)",
|
||||
}}
|
||||
overflow="hidden"
|
||||
>
|
||||
<SidebarIconGroup>
|
||||
{antagonistGun.shells.map((thisShell) => (
|
||||
<IconButton
|
||||
color={
|
||||
thisShell.id === antagonistShell.id && !hasCustomShell
|
||||
? undefined
|
||||
: "gray"
|
||||
}
|
||||
variant="soft"
|
||||
<SidebarIconButton
|
||||
key={thisShell.id}
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="none"
|
||||
selected={thisShell.id === antagonistShell.id && !hasCustomShell}
|
||||
alt={unwrap(thisShell.name!)}
|
||||
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
|
||||
onClick={() => {
|
||||
Duel.mutate((draft) => {
|
||||
draft.antagonist.shell = thisShell;
|
||||
@@ -208,19 +206,10 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
draft.customShell = undefined;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={unwrap(thisShell.name!)}
|
||||
src={alias("api", `/icons/shells/${thisShell.icon}.webp`)}
|
||||
style={{
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
/>
|
||||
))}
|
||||
<CustomShellButton />
|
||||
</Flex>
|
||||
</SidebarIconGroup>
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
|
||||
+27
-4
@@ -1,30 +1,53 @@
|
||||
import { UpdateIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Flex, Heading } from "@radix-ui/themes";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { SKIN_NUMBER } from "../../../../../../constants/skinNumber";
|
||||
import { api } from "../../../../../../core/blitzkit/api";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../../../types/maybeSkeletonComponentProps";
|
||||
import { Tracker } from "./components/Tracker";
|
||||
|
||||
const camouflageDefinitions = await api.camouflageDefinitions();
|
||||
|
||||
export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const { unwrap, strings } = useLocale();
|
||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||
const modelRequested = Tankopedia.use(
|
||||
(state) => state.loaders.model.requested,
|
||||
);
|
||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||
const revealed = Tankopedia.use((state) => state.revealed);
|
||||
const disturbed = Tankopedia.use((state) => state.disturbed);
|
||||
const name = unwrap(protagonist.name!);
|
||||
const hasRevealed = useRef(false);
|
||||
const skin = Tankopedia.use((state) => state.loaders.skin.requested);
|
||||
const camouflage =
|
||||
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
|
||||
const tankName = unwrap(protagonist.name!);
|
||||
// legendary camos carry the tank's in-battle name
|
||||
const name = (
|
||||
camouflage
|
||||
? camouflage.tank_name
|
||||
? unwrap(camouflage.tank_name)
|
||||
: `${tankName} ${unwrap(camouflage.name!)}`
|
||||
: tankName
|
||||
).replace("%(camo_num)", SKIN_NUMBER);
|
||||
const fontSize = revealed
|
||||
? disturbed
|
||||
? "1.5rem"
|
||||
: "2rem"
|
||||
: `min(12vh, ${75 / name.length}vw)`;
|
||||
|
||||
useEffect(() => {
|
||||
if (revealed) hasRevealed.current = true;
|
||||
}, [revealed]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!modelRequested) return;
|
||||
if (!modelRequested || hasRevealed.current) return;
|
||||
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
@@ -86,7 +109,7 @@ export function Title({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.modelRequested = true;
|
||||
draft.loaders.model.requested = true;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
+61
-3
@@ -18,7 +18,7 @@ import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent
|
||||
|
||||
const ANGLE = degToRad(15);
|
||||
const REVEAL_ANIMATION_TIME = 3;
|
||||
const TRANSITION_ANIMATION_TIME = 0.5;
|
||||
export const TRANSITION_ANIMATION_TIME = 0.5;
|
||||
|
||||
const LIGHTS_COUNT = 3;
|
||||
const THETA_OFFSET = degToRad(180 - 45);
|
||||
@@ -46,17 +46,26 @@ export function Lighting({ hasPbr }: Props) {
|
||||
|
||||
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const revealed = Tankopedia.use((state) => state.revealed);
|
||||
const skinPending = Tankopedia.use(
|
||||
(state) =>
|
||||
state.loaders.skin.requested !== state.loaders.skin.loaded ||
|
||||
state.loaders.attachment.requested !== state.loaders.attachment.loaded,
|
||||
);
|
||||
|
||||
const animationTime = useRef(REVEAL_ANIMATION_TIME);
|
||||
const t0 = useRef(performance.now() / 1e3 - animationTime.current);
|
||||
const holding = useRef(false);
|
||||
|
||||
const isRevealing = useRef(true);
|
||||
const isFirstReveal = useRef(true);
|
||||
|
||||
const [animate, setAnimate] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
draft.loaders.model.loaded = true;
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -71,6 +80,37 @@ export function Lighting({ hasPbr }: Props) {
|
||||
setAnimate(true);
|
||||
}, [requestedDisplay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstReveal.current) {
|
||||
isFirstReveal.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!revealed) {
|
||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
||||
t0.current = performance.now() / 1e3;
|
||||
holding.current = true;
|
||||
} else if (holding.current) {
|
||||
animationTime.current = REVEAL_ANIMATION_TIME;
|
||||
t0.current = performance.now() / 1e3 - animationTime.current;
|
||||
holding.current = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
setAnimate(true);
|
||||
invalidate();
|
||||
}, [revealed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!skinPending) return;
|
||||
|
||||
animationTime.current = TRANSITION_ANIMATION_TIME;
|
||||
t0.current = performance.now() / 1e3;
|
||||
|
||||
setAnimate(true);
|
||||
}, [skinPending]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContactShadows
|
||||
@@ -90,6 +130,7 @@ export function Lighting({ hasPbr }: Props) {
|
||||
hasPbr={hasPbr}
|
||||
t0={t0}
|
||||
animationTime={animationTime}
|
||||
holding={holding}
|
||||
wrapper={wrapper}
|
||||
stop={() => {
|
||||
setAnimate(false);
|
||||
@@ -124,6 +165,7 @@ export function Lighting({ hasPbr }: Props) {
|
||||
key={index}
|
||||
position={position}
|
||||
intensity={intensity}
|
||||
userData={{ intensity }}
|
||||
penumbra={1}
|
||||
castShadow={highGraphics}
|
||||
decay={1}
|
||||
@@ -145,11 +187,19 @@ interface AnimatorProps {
|
||||
|
||||
t0: RefObject<number>;
|
||||
animationTime: RefObject<number>;
|
||||
holding: RefObject<boolean>;
|
||||
wrapper: RefObject<Group | null>;
|
||||
hasPbr: boolean;
|
||||
}
|
||||
|
||||
function Animator({ stop, t0, animationTime, wrapper, hasPbr }: AnimatorProps) {
|
||||
function Animator({
|
||||
stop,
|
||||
t0,
|
||||
animationTime,
|
||||
holding,
|
||||
wrapper,
|
||||
hasPbr,
|
||||
}: AnimatorProps) {
|
||||
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
|
||||
const display = Tankopedia.use((state) => state.display);
|
||||
|
||||
@@ -166,6 +216,11 @@ function Animator({ stop, t0, animationTime, wrapper, hasPbr }: AnimatorProps) {
|
||||
for (const child of wrapper.current.children) {
|
||||
if (child instanceof SpotLight) {
|
||||
child.angle = ANGLE * t * factor;
|
||||
child.intensity = child.userData.intensity;
|
||||
|
||||
if (child.angle === 0) {
|
||||
child.intensity = 0;
|
||||
}
|
||||
} else if (child instanceof HemisphereLight) {
|
||||
child.intensity = HEMISPHERE_INTENSITY * t * factor;
|
||||
}
|
||||
@@ -176,7 +231,7 @@ function Animator({ stop, t0, animationTime, wrapper, hasPbr }: AnimatorProps) {
|
||||
|
||||
useFrame(({ invalidate }) => {
|
||||
const dt = performance.now() / 1e3 - t0.current;
|
||||
const x = clamp(dt / animationTime.current, 0, 2);
|
||||
const x = clamp(dt / animationTime.current, 0, holding.current ? 1 : 2);
|
||||
|
||||
if (x === 2) {
|
||||
apply(1);
|
||||
@@ -188,6 +243,9 @@ function Animator({ stop, t0, animationTime, wrapper, hasPbr }: AnimatorProps) {
|
||||
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
|
||||
|
||||
apply(t);
|
||||
|
||||
if (holding.current && x === 1) return;
|
||||
|
||||
invalidate();
|
||||
|
||||
if (x >= 1 && Tankopedia.state.display !== requestedDisplay) {
|
||||
|
||||
+45
-5
@@ -1,18 +1,21 @@
|
||||
import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import type { QuicklimeEvent } from "quicklime";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
|
||||
import { applyPitchYawLimits } from "../../../../../../core/blitz/applyPitchYawLimits";
|
||||
import { hasEquipment } from "../../../../../../core/blitzkit/hasEquipment";
|
||||
import { jsxTree } from "../../../../../../core/blitzkit/jsxTree";
|
||||
import { modelTransformEvent } from "../../../../../../core/blitzkit/modelTransform";
|
||||
import { skinTree } from "../../../../../../core/blitzkit/skinTree";
|
||||
import { controlsEnabledEvent } from "../../../../../../core/controlsEnabled";
|
||||
import { useModel } from "../../../../../../hooks/useModel";
|
||||
import { useSkin } from "../../../../../../hooks/useSkin";
|
||||
import { useTankModelDefinition } from "../../../../../../hooks/useTankModelDefinition";
|
||||
import { useTankTransform } from "../../../../../../hooks/useTankTransform";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
import { TankopediaDisplay } from "../../../../../../stores/tankopediaPersistent/constants";
|
||||
import { ModelTankWrapper } from "../../../../../Armor/components/ModelTankWrapper";
|
||||
import { transitionEvent } from "./Lighting";
|
||||
|
||||
export function TankModel() {
|
||||
const protagonist = Duel.use((draft) => draft.protagonist);
|
||||
@@ -28,9 +31,46 @@ export function TankModel() {
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun.id];
|
||||
const { gltf } = useModel(protagonist.tank.id);
|
||||
const nodes = Object.values(gltf.nodes);
|
||||
const skinGltf = useSkin(protagonist.tank.id);
|
||||
const attachmentGltf = useSkin(protagonist.tank.id, "attachment");
|
||||
const skins = [skinGltf, attachmentGltf].filter((skin) => skin !== undefined);
|
||||
|
||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
|
||||
useEffect(() => {
|
||||
const emissiveMaterials = new Set<MeshStandardMaterial>();
|
||||
|
||||
for (const scene of [gltf.scene, ...skins.map((skin) => skin.scene)]) {
|
||||
scene.traverse((object) => {
|
||||
if (
|
||||
object instanceof Mesh &&
|
||||
object.material instanceof MeshStandardMaterial &&
|
||||
object.material.emissiveMap
|
||||
) {
|
||||
emissiveMaterials.add(object.material);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// emissive parts fade with the lights instead of glowing in the dark
|
||||
function applyIntensity(intensity: number) {
|
||||
for (const material of emissiveMaterials) {
|
||||
material.emissiveIntensity = intensity;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTransitionEvent(event: QuicklimeEvent<number>) {
|
||||
applyIntensity(event.data);
|
||||
}
|
||||
|
||||
applyIntensity(transitionEvent.last!);
|
||||
transitionEvent.on(handleTransitionEvent);
|
||||
|
||||
return () => {
|
||||
transitionEvent.off(handleTransitionEvent);
|
||||
};
|
||||
}, [gltf, skinGltf, attachmentGltf]);
|
||||
|
||||
return (
|
||||
<ModelTankWrapper ref={hullContainer}>
|
||||
{nodes.map((node) => {
|
||||
@@ -92,7 +132,7 @@ export function TankModel() {
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
return skinTree(node, skins, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
@@ -169,7 +209,7 @@ export function TankModel() {
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
return skinTree(node, skins, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
@@ -250,7 +290,7 @@ export function TankModel() {
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
return skinTree(node, skins, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
|
||||
@@ -20,7 +20,9 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const canvas = useRef<HTMLCanvasElement>(null!);
|
||||
const isFullScreen = useFullScreen();
|
||||
const protagonist = Duel.use((state) => state.protagonist.tank);
|
||||
const modelRequested = Tankopedia.use((state) => state.modelRequested);
|
||||
const modelRequested = Tankopedia.use(
|
||||
(state) => state.loaders.model.requested,
|
||||
);
|
||||
const thicknessRange = useMemo(() => {
|
||||
const entries = Object.values(tankDefinitions.tanks);
|
||||
const filtered = entries.filter(
|
||||
|
||||
Reference in new issue
Block a user