mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-13 20:56:11 +01:00
Merge branch 'main' into data-driven
This commit is contained in:
commit
89d272166f
89 files changed
+2018
-719
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>
|
||||
@@ -42,9 +42,9 @@ const strings = await api.strings(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">
|
||||
@@ -83,9 +83,6 @@ const strings = await api.strings(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"
|
||||
|
||||
@@ -18,7 +18,7 @@ import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants";
|
||||
|
||||
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,11 +46,17 @@ 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.loadedSkin !== state.skin,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
@@ -71,6 +77,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 +127,7 @@ export function Lighting({ hasPbr }: Props) {
|
||||
hasPbr={hasPbr}
|
||||
t0={t0}
|
||||
animationTime={animationTime}
|
||||
holding={holding}
|
||||
wrapper={wrapper}
|
||||
stop={() => {
|
||||
setAnimate(false);
|
||||
@@ -145,11 +183,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);
|
||||
|
||||
@@ -176,7 +222,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 +234,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) {
|
||||
@@ -195,6 +244,12 @@ function Animator({ stop, t0, animationTime, wrapper, hasPbr }: AnimatorProps) {
|
||||
draft.display = requestedDisplay;
|
||||
});
|
||||
}
|
||||
|
||||
if (x >= 1 && Tankopedia.state.skin !== Tankopedia.state.loadedSkin) {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.skin = draft.loadedSkin;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
@@ -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";
|
||||
@@ -103,7 +103,7 @@ const strings = getStrings(locale);
|
||||
<Flex
|
||||
className="content"
|
||||
width="100%"
|
||||
p={{ initial: "4", sm: "3" }}
|
||||
p="3"
|
||||
direction="column"
|
||||
align="center"
|
||||
>
|
||||
@@ -131,42 +131,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(([id, tool]) => {
|
||||
const selected = Astro.url.pathname.includes(
|
||||
`/${tool.path ?? id}`,
|
||||
);
|
||||
const toolStrings =
|
||||
strings.website.tools[
|
||||
(tool.strings ?? 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.path ?? id}`}
|
||||
underline="hover"
|
||||
>
|
||||
{toolStrings.name}
|
||||
</LinkI18n>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
|
||||
<Box flexGrow="1" />
|
||||
|
||||
<Flex align="center" gap="4">
|
||||
<Link
|
||||
style={{ display: "flex", alignContent: "center" }}
|
||||
@@ -190,7 +154,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 />
|
||||
@@ -199,11 +163,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(([id, tool]) => {
|
||||
@@ -228,7 +191,7 @@ const strings = getStrings(locale);
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
py="4"
|
||||
p="6"
|
||||
justify="end"
|
||||
flexGrow="1"
|
||||
style={{
|
||||
@@ -242,7 +205,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" />;
|
||||
}
|
||||
@@ -2,13 +2,14 @@ import { invalidate, useThree, type ThreeEvent } from "@react-three/fiber";
|
||||
import { useRef } from "react";
|
||||
import { Group, Mesh, MeshStandardMaterial, Vector2 } from "three";
|
||||
import { applyPitchYawLimits } from "../core/blitz/applyPitchYawLimits";
|
||||
import { jsxTree } from "../core/blitzkit/jsxTree";
|
||||
import { modelTransformEvent } from "../core/blitzkit/modelTransform";
|
||||
import { skinTree } from "../core/blitzkit/skinTree";
|
||||
import { controlsEnabledEvent } from "../core/controlsEnabled";
|
||||
import { DuelModule, getDuel, useDuel } from "../hooks/useDuel";
|
||||
import { useDuelModel } from "../hooks/useDuelModel";
|
||||
import { hasEquipment } from "../hooks/useEquipment";
|
||||
import { useModel } from "../hooks/useModel";
|
||||
import { useSkin } from "../hooks/useSkin";
|
||||
import { useTankTransform } from "../hooks/useTankTransform";
|
||||
import { Tankopedia } from "../stores/tankopedia";
|
||||
import { TankopediaDisplay } from "../stores/tankopediaPersistent/constants";
|
||||
@@ -27,6 +28,7 @@ export function TankModel() {
|
||||
const gunModelDefinition = turretModelDefinition.guns[protagonist.gun];
|
||||
const { gltf } = useModel(protagonist.tank);
|
||||
const nodes = Object.values(gltf.nodes);
|
||||
const skinGltf = useSkin(protagonist.tank);
|
||||
|
||||
useTankTransform(track, turret, turretContainer, gunContainer);
|
||||
|
||||
@@ -103,7 +105,7 @@ export function TankModel() {
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
return skinTree(node, skinGltf, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
@@ -182,7 +184,7 @@ export function TankModel() {
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
return skinTree(node, skinGltf, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
@@ -265,7 +267,7 @@ export function TankModel() {
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
}
|
||||
|
||||
return jsxTree(node, {
|
||||
return skinTree(node, skinGltf, {
|
||||
mesh(_, props, key) {
|
||||
return (
|
||||
<mesh
|
||||
|
||||
+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>
|
||||
);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
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.requestedSkin);
|
||||
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!),
|
||||
);
|
||||
|
||||
if (skins.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
top="50%"
|
||||
right={
|
||||
disturbed && requestedDisplay === TankopediaDisplay.Model
|
||||
? "3"
|
||||
: "-4rem"
|
||||
}
|
||||
style={{
|
||||
position: "absolute",
|
||||
transform: "translateY(-50%)",
|
||||
transitionDuration: "200ms",
|
||||
pointerEvents: "auto",
|
||||
}}
|
||||
>
|
||||
{skins.map((camouflage, index) => {
|
||||
const name = unwrap(camouflage.name!).replace(
|
||||
"%(camo_num)",
|
||||
SKIN_NUMBER,
|
||||
);
|
||||
const top = index === 0 ? "var(--radius-full)" : "0";
|
||||
const bottom = index === skins.length - 1 ? "var(--radius-full)" : "0";
|
||||
|
||||
return (
|
||||
<Tooltip key={camouflage.id} content={name} side="left">
|
||||
<SidebarIconButton
|
||||
selected={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) => {
|
||||
draft.requestedSkin =
|
||||
draft.requestedSkin === camouflage.id
|
||||
? undefined
|
||||
: camouflage.id;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
+14
-27
@@ -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";
|
||||
|
||||
@@ -87,6 +92,8 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
|
||||
<Thicknesses skeleton={skeleton} thicknessRange={thicknessRange} />
|
||||
|
||||
<SkinSwitcher />
|
||||
|
||||
<Flex
|
||||
gap="2"
|
||||
direction="column"
|
||||
@@ -181,24 +188,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 +204,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"
|
||||
|
||||
+18
-2
@@ -1,30 +1,46 @@
|
||||
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 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.requestedSkin);
|
||||
const camouflage =
|
||||
skin === undefined ? undefined : camouflageDefinitions.camouflages[skin];
|
||||
const tankName = unwrap(protagonist.name!);
|
||||
const name = camouflage
|
||||
? `${tankName} ${unwrap(camouflage.name!).replace("%(camo_num)", SKIN_NUMBER)}`
|
||||
: tankName;
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user