mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
Merge branch 'main' into data-driven
This commit is contained in:
commit
1b08393f65
60 files changed
+434
-474
No files matched your search
@@ -23,10 +23,12 @@ import {
|
||||
TextField,
|
||||
} from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { debounce, times } from "lodash-es";
|
||||
import { debounce } from "lodash-es";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { api } from "../api/dynamic";
|
||||
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||
import { awaitableTiers } from "../core/awaitables/tiers";
|
||||
import { api } from "../core/blitzkit/api";
|
||||
import { useLocale } from "../hooks/useLocale";
|
||||
import { Guess, GuessState } from "../stores/guess";
|
||||
import { classIcons } from "./ClassIcon";
|
||||
@@ -34,9 +36,10 @@ import { SearchResults } from "./SearchResults";
|
||||
|
||||
const { go } = fuzzysort;
|
||||
|
||||
const [tankNames, tankDefinitions] = await Promise.all([
|
||||
const [tankNames, tankDefinitions, TIERS] = await Promise.all([
|
||||
awaitableTankNames,
|
||||
api.tanks(),
|
||||
api.tankDefinitions(),
|
||||
awaitableTiers,
|
||||
]);
|
||||
|
||||
const ids = Object.keys(tankDefinitions.tanks);
|
||||
@@ -189,8 +192,7 @@ export function Guesser() {
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{times(10, (index) => {
|
||||
const tier = 10 - index;
|
||||
{TIERS.map((tier) => {
|
||||
const isSelected = tiers.includes(tier);
|
||||
|
||||
return (
|
||||
|
||||
@@ -82,6 +82,7 @@ export function Lighting({ hasPbr }: Props) {
|
||||
blur={2 ** 1}
|
||||
resolution={2 ** 9}
|
||||
depthWrite={false}
|
||||
renderOrder={1}
|
||||
/>
|
||||
|
||||
{animate && (
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
<script>
|
||||
import locales from "@blitzkit/i18n/locales.json";
|
||||
import { getBestLocale } from "../core/i18n/getBestLocale";
|
||||
|
||||
const pathname = window.location.pathname;
|
||||
const hasPreference = localStorage.getItem("preferred-locale") !== null;
|
||||
const bestLocale = getBestLocale();
|
||||
const currentLocale =
|
||||
locales.supported.find(
|
||||
({ locale }) =>
|
||||
pathname === `/${locale}` || pathname.startsWith(`/${locale}/`),
|
||||
)?.locale ?? locales.default;
|
||||
|
||||
if (
|
||||
bestLocale !== currentLocale &&
|
||||
(hasPreference || currentLocale === locales.default)
|
||||
) {
|
||||
const rawPath =
|
||||
currentLocale === locales.default
|
||||
? pathname
|
||||
: pathname.slice(`/${currentLocale}`.length) || "/";
|
||||
|
||||
window.location.pathname = `${
|
||||
bestLocale === locales.default ? "" : `/${bestLocale}`
|
||||
}${rawPath}`;
|
||||
}
|
||||
</script>
|
||||
@@ -25,6 +25,7 @@ import { TankSearchFilters } from "./TankSearchFilters";
|
||||
const tankDefinitions = await api.tanks();
|
||||
|
||||
const POOL_HEIGHT = "16rem";
|
||||
const FILTERS_WIDTH = "32rem";
|
||||
|
||||
export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
const [largeWarningOpen, setLargeWarningOpen] = useState(false);
|
||||
@@ -80,14 +81,20 @@ export function PlaylistGenerator({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
<Flex justify="center" width="100%" align="center" px="4">
|
||||
<Flex
|
||||
flexGrow="1"
|
||||
gap="9"
|
||||
gap="6"
|
||||
align="center"
|
||||
justify="center"
|
||||
maxWidth="80rem"
|
||||
direction={{ initial: "column-reverse", md: "row" }}
|
||||
>
|
||||
<Flex direction="column" gap="4" align="center">
|
||||
<TankSearchFilters />
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="4"
|
||||
align="center"
|
||||
flexShrink="0"
|
||||
width={{ initial: "100%", md: FILTERS_WIDTH }}
|
||||
>
|
||||
<FilterControl />
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
@@ -128,19 +128,30 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
? shotStatusColors[shot.out.status]
|
||||
: undefined;
|
||||
const inTitleColor = shotStatusColors[shot.in.status];
|
||||
const hasOutLayers = shot.out !== undefined && shot.out.layers.length > 0;
|
||||
const penetrationChance = shot.penetrationChance !== undefined && (
|
||||
<Text color="gray" weight="bold">
|
||||
{literals(strings.common.units.percentage, {
|
||||
value: (shot.penetrationChance * 100).toFixed(0),
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card variant="classic" {...props}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex direction="column" gap="1">
|
||||
<Text color={inTitleColor} weight="bold">
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.status[
|
||||
shot.in.status
|
||||
],
|
||||
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
||||
)}
|
||||
</Text>
|
||||
<Flex justify="between" align="center" gap="4">
|
||||
<Text color={inTitleColor} weight="bold">
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
.status[shot.in.status],
|
||||
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{!hasOutLayers && penetrationChance}
|
||||
</Flex>
|
||||
|
||||
<table
|
||||
style={{
|
||||
@@ -204,13 +215,17 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
|
||||
</Flex>
|
||||
</Inset>
|
||||
|
||||
<Text color={outTitleColor} weight="bold">
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
.status[shot.out.status],
|
||||
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
||||
)}
|
||||
</Text>
|
||||
<Flex justify="between" align="center" gap="4">
|
||||
<Text color={outTitleColor} weight="bold">
|
||||
{literals(
|
||||
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
|
||||
.status[shot.out.status],
|
||||
{ damage: Math.round(shot.damage).toLocaleString(locale) }
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{penetrationChance}
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column">
|
||||
{shot.out.layers.map((layer, index) => {
|
||||
|
||||
@@ -56,6 +56,36 @@ export type ArmorUserData = {
|
||||
}
|
||||
);
|
||||
|
||||
const PENETRATION_RANDOMIZATION = 0.05;
|
||||
|
||||
function resolvePenetrationChance(shoot: (scale: number) => Shot | null) {
|
||||
function penetrates(scale: number) {
|
||||
const shot = shoot(scale);
|
||||
|
||||
return (shot?.out ?? shot?.in)?.status === "penetration";
|
||||
}
|
||||
|
||||
let low = 1 - PENETRATION_RANDOMIZATION;
|
||||
let high = 1 + PENETRATION_RANDOMIZATION;
|
||||
|
||||
if (penetrates(low)) return 1;
|
||||
if (!penetrates(high)) return 0;
|
||||
|
||||
for (let iteration = 0; iteration < 16; iteration++) {
|
||||
const middle = (low + high) / 2;
|
||||
|
||||
if (penetrates(middle)) {
|
||||
high = middle;
|
||||
} else {
|
||||
low = middle;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(1 + PENETRATION_RANDOMIZATION - high) / (2 * PENETRATION_RANDOMIZATION)
|
||||
);
|
||||
}
|
||||
|
||||
const omitMaterial = new MeshBasicMaterial({
|
||||
colorWrite: false,
|
||||
depthWrite: true,
|
||||
@@ -82,6 +112,7 @@ export function SpacedArmorSceneComponent({
|
||||
intersections: Intersection[],
|
||||
allowRicochet: boolean,
|
||||
remainingPenetrationInput?: number,
|
||||
penetrationScale = 1,
|
||||
) => {
|
||||
const protagonist = getDuel("protagonist", DuelModule.Tank);
|
||||
const antagonist = getDuel("antagonist", DuelModule.Tank);
|
||||
@@ -119,6 +150,7 @@ export function SpacedArmorSceneComponent({
|
||||
Tankopedia.state.protagonist.equipment,
|
||||
);
|
||||
const penetration =
|
||||
penetrationScale *
|
||||
shell.penetration!.near *
|
||||
resolvePenetrationCoefficient(
|
||||
hasCalibratedShells,
|
||||
@@ -361,6 +393,7 @@ export function SpacedArmorSceneComponent({
|
||||
ricochetIntersections,
|
||||
false,
|
||||
remainingPenetration,
|
||||
penetrationScale,
|
||||
);
|
||||
|
||||
shot.in.status = "ricochet";
|
||||
@@ -416,6 +449,16 @@ export function SpacedArmorSceneComponent({
|
||||
|
||||
const shot = shoot(event.point, event.intersections, true)!;
|
||||
|
||||
shot.penetrationChance = resolvePenetrationChance((scale) =>
|
||||
shoot(
|
||||
event.point,
|
||||
event.intersections,
|
||||
true,
|
||||
undefined,
|
||||
scale,
|
||||
),
|
||||
);
|
||||
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = shot;
|
||||
});
|
||||
|
||||
@@ -62,11 +62,17 @@ export function TankModel() {
|
||||
|
||||
invalidate();
|
||||
|
||||
if (material.map) material.map.offset.y += offset;
|
||||
if (material.aoMap) material.aoMap.offset.y += offset;
|
||||
if (material.normalMap) material.normalMap.offset.y += offset;
|
||||
if (material.roughnessMap) material.roughnessMap.offset.y += offset;
|
||||
if (material.metalnessMap) material.metalnessMap.offset.y += offset;
|
||||
const textures = new Set([
|
||||
material.map,
|
||||
material.aoMap,
|
||||
material.normalMap,
|
||||
material.roughnessMap,
|
||||
material.metalnessMap,
|
||||
]);
|
||||
|
||||
for (const texture of textures) {
|
||||
if (texture) texture.offset.y += offset;
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerDown(event: ThreeEvent<PointerEvent>) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { memo } from "react";
|
||||
import { awaitableTiers } from "../../../core/awaitables/tiers";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
|
||||
const TIERS = await awaitableTiers;
|
||||
|
||||
export const TierFilter = memo(() => {
|
||||
const by = TankSort.use((state) => state.by);
|
||||
const search = TankFilters.use((state) => state.search);
|
||||
@@ -19,8 +21,7 @@ export const TierFilter = memo(() => {
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
>
|
||||
{times(10, (index) => {
|
||||
const tier = 10 - index;
|
||||
{TIERS.map((tier) => {
|
||||
const selected = tiers.includes(tier);
|
||||
|
||||
return (
|
||||
|
||||
@@ -153,7 +153,7 @@ const TANK_TYPE_COLORS: Record<TankType, string> = {
|
||||
|
||||
const MAX_ICONS = 4;
|
||||
|
||||
const TIERS = times(10, (i) => 10 - i);
|
||||
const TIERS = await awaitableTiers;
|
||||
|
||||
export function TankSearchFilters() {
|
||||
return (
|
||||
|
||||
+3
-1
@@ -13,6 +13,7 @@ import { modelTransformEvent } from "../../../../../../../core/blitzkit/modelTra
|
||||
import { Var } from "../../../../../../../core/radix/var";
|
||||
import { useLocale } from "../../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../../stores/tankopedia";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../../../../../types/maybeSkeletonComponentProps";
|
||||
import { VisualizerCard } from "../VisualizerCard";
|
||||
import { VisualizerCornerStat } from "../VisualizerCornerStat";
|
||||
@@ -43,6 +44,7 @@ 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 tankModel = modelDefinition.models[tank.id];
|
||||
const turretModel = tankModel.turrets[turret.id];
|
||||
@@ -193,7 +195,7 @@ export function GunFlexibilityVisualizer({
|
||||
top="0"
|
||||
left="0"
|
||||
>
|
||||
{!skeleton && <FlexibilityCanvas />}
|
||||
{!skeleton && modelRequested && <FlexibilityCanvas />}
|
||||
|
||||
<Box
|
||||
position="absolute"
|
||||
|
||||
+8
@@ -24,6 +24,14 @@ export function Survivability({ stats }: StatsAcceptorProps) {
|
||||
decimals={0}
|
||||
value={(stats) => stats.fireChance * 100}
|
||||
/>
|
||||
{stats.trackRepairTime !== undefined && (
|
||||
<InfoWithDelta
|
||||
value="trackRepairTime"
|
||||
stats={stats}
|
||||
deltaType="lowerIsBetter"
|
||||
decimals={2}
|
||||
/>
|
||||
)}
|
||||
<InfoWithDelta value="viewRange" stats={stats} decimals={0} />
|
||||
<ViewRangeVisualizer stats={stats} />
|
||||
<Info
|
||||
|
||||
+7
-13
@@ -28,23 +28,17 @@ export function Equalizer() {
|
||||
width="100%"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
borderRadius: "var(--radius-3)",
|
||||
backgroundImage: `url(${alias("api", `/gamemodes/Insanity/banner.webp`)})`,
|
||||
backgroundSize: "cover",
|
||||
borderRadius: "var(--radius-3) 0 0 var(--radius-3)",
|
||||
backgroundImage: `
|
||||
linear-gradient(90deg, var(--black-a3), var(--gray-1)),
|
||||
url(${alias("api", `/gamemodes/Insanity/banner.webp`)})
|
||||
`,
|
||||
backgroundSize: "auto, fit",
|
||||
backgroundPosition: "50% 25%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
width="100%"
|
||||
justify="between"
|
||||
p="4"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, var(--black-a3), var(--gray-1))",
|
||||
}}
|
||||
>
|
||||
<Flex align="center" width="100%" justify="between" p="4" style={{}}>
|
||||
<Text
|
||||
style={{
|
||||
textShadow: "0 0 var(--space-1) var(--black-a12)",
|
||||
|
||||
+2
-2
@@ -31,11 +31,11 @@ export function Thicknesses({ thicknessRange, skeleton }: ThicknessesProps) {
|
||||
return (
|
||||
<Flex
|
||||
position="absolute"
|
||||
right={display === TankopediaDisplay.StaticArmor ? "0" : "-6rem"}
|
||||
right="0"
|
||||
top="50%"
|
||||
mr="4"
|
||||
style={{
|
||||
transform: "translateY(-50%)",
|
||||
transform: `translate(${display === TankopediaDisplay.StaticArmor ? "0" : "calc(100% + var(--space-4))"}, -50%)`,
|
||||
userSelect: "none",
|
||||
transitionDuration: "200ms",
|
||||
}}
|
||||
|
||||
@@ -77,6 +77,7 @@ 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 equalize = Duel.use((state) => state.equalize);
|
||||
const equalizer = Duel.use((state) => state.antagonist.tank.equalizer);
|
||||
|
||||
@@ -283,7 +284,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
<SpallLinerSwitcher />
|
||||
</Flex>
|
||||
|
||||
{!skeleton && (
|
||||
{!skeleton && modelRequested && (
|
||||
<Suspense>
|
||||
<DynamicArmorSwitcher />
|
||||
</Suspense>
|
||||
|
||||
+38
-4
@@ -1,11 +1,14 @@
|
||||
import { Flex, Heading } from "@radix-ui/themes";
|
||||
import { UpdateIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Flex, Heading } from "@radix-ui/themes";
|
||||
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";
|
||||
|
||||
export function Title() {
|
||||
const { unwrap } = useLocale();
|
||||
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);
|
||||
@@ -21,6 +24,8 @@ export function Title() {
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!modelRequested) return;
|
||||
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.revealed = true;
|
||||
});
|
||||
@@ -60,7 +65,36 @@ export function Title() {
|
||||
{name}
|
||||
</Heading>
|
||||
|
||||
{!revealed && <Tracker fontSize={fontSize} />}
|
||||
{!skeleton && !revealed && modelRequested && (
|
||||
<Tracker fontSize={fontSize} />
|
||||
)}
|
||||
|
||||
{!skeleton && !modelRequested && (
|
||||
<Flex
|
||||
position="absolute"
|
||||
left="50%"
|
||||
top="50%"
|
||||
width="min(100%, 16rem)"
|
||||
justify="center"
|
||||
style={{
|
||||
transform: `translate(-50%, calc(${fontSize} * 0.75 + var(--space-4) - 50%))`,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="solid"
|
||||
highContrast
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={() => {
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.modelRequested = true;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<UpdateIcon />
|
||||
{strings.website.tools.tankopedia.sandbox.load_model}
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ 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 thicknessRange = useMemo(() => {
|
||||
const entries = Object.values(tankDefinitions.tanks);
|
||||
const filtered = entries.filter(
|
||||
@@ -131,13 +132,15 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
top="0"
|
||||
left="0"
|
||||
>
|
||||
{!skeleton && (
|
||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||
{!skeleton && modelRequested && (
|
||||
<Suspense>
|
||||
<TankSandbox ref={canvas} thicknessRange={thicknessRange} />
|
||||
</Suspense>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Title />
|
||||
<Title skeleton={skeleton} />
|
||||
|
||||
<Options
|
||||
skeleton={skeleton}
|
||||
|
||||
@@ -127,7 +127,7 @@ export function Node({ id, highlight, nextIds, skeleton }: NodeProps) {
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{averages && nextTanks && tank.tier !== 10 && (
|
||||
{averages && nextTanks && (
|
||||
<Text color="gray" size="1" mt="2">
|
||||
{skeleton && <Skeleton height="1em" width="4em" />}
|
||||
{!skeleton &&
|
||||
|
||||
@@ -192,7 +192,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
);
|
||||
})}
|
||||
|
||||
{master.tier < 10 && (
|
||||
{master.successors.length > 0 && (
|
||||
<>
|
||||
<Arrow />
|
||||
{master.successors!.map((id, index) => (
|
||||
|
||||
Reference in new issue
Block a user