mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
Merge branch 'main' into playlist-filters-layout
This commit is contained in:
65 files changed
+661
-154
No files matched your search
@@ -18,6 +18,10 @@ import {
|
||||
import { degToRad } from "three/src/math/MathUtils.js";
|
||||
import { hasEquipment } from "../../../../core/blitzkit/hasEquipment";
|
||||
import { jsxTree } from "../../../../core/blitzkit/jsxTree";
|
||||
import {
|
||||
SPALL_LINER_HE_DAMAGE_DELTA,
|
||||
SPALL_LINER_PROVISION_ID,
|
||||
} from "../../../../core/blitzkit/spallLiner";
|
||||
import { defaultEqualizer } from "../../../../core/blitzkit/tankToDuelMember";
|
||||
import { discardClippingPlane } from "../../../../core/three/discardClippingPlane";
|
||||
import { Duel } from "../../../../stores/duel";
|
||||
@@ -384,6 +388,13 @@ export function SpacedArmorSceneComponent({
|
||||
|
||||
shot.damage *= antagonistEqualizer.damage;
|
||||
|
||||
const hasSpallLiner = Duel.state.protagonist.provisions.includes(
|
||||
SPALL_LINER_PROVISION_ID,
|
||||
);
|
||||
if (hasSpallLiner && shell.type === ShellType.SHELL_TYPE_HE) {
|
||||
shot.damage *= 1 + SPALL_LINER_HE_DAMAGE_DELTA;
|
||||
}
|
||||
|
||||
return shot;
|
||||
},
|
||||
[camera],
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
const gameDefinitions = await api.gameDefinitions();
|
||||
const gameVersion = gameDefinitions.version.split("_")[0];
|
||||
const environment = assertSecret(import.meta.env.PUBLIC_ENVIRONMENT);
|
||||
const { locale } = Astro.props;
|
||||
const strings = getStrings(locale);
|
||||
@@ -141,7 +142,7 @@ const strings = getStrings(locale);
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
<Text size="2" color="gray">CDN</Text>
|
||||
<Text size="2" color="gray">Branch</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex direction="column" gap="1" flexGrow="1" flexBasis="0" align="start">
|
||||
@@ -151,7 +152,7 @@ const strings = getStrings(locale);
|
||||
{
|
||||
environment !== "preview" && (
|
||||
<Code size="2" color="gray">
|
||||
{gameDefinitions.version}
|
||||
{gameVersion}
|
||||
</Code>
|
||||
)
|
||||
}
|
||||
@@ -174,13 +175,13 @@ const strings = getStrings(locale);
|
||||
WoTB
|
||||
</Text>
|
||||
<Code size="2" color="gray">
|
||||
{gameDefinitions.version}
|
||||
{gameVersion}
|
||||
</Code>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<Text size="2" color="gray">CDN</Text>
|
||||
<Text size="2" color="gray">Branch</Text>
|
||||
<Code size="2" color="gray">
|
||||
{environment}
|
||||
</Code>
|
||||
|
||||
@@ -27,9 +27,10 @@ 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 { 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";
|
||||
@@ -38,9 +39,10 @@ import { SearchResults } from "./SearchResults";
|
||||
|
||||
const { go } = fuzzysort;
|
||||
|
||||
const [tankNames, tankDefinitions] = await Promise.all([
|
||||
const [tankNames, tankDefinitions, TIERS] = await Promise.all([
|
||||
awaitableTankNames,
|
||||
api.tankDefinitions(),
|
||||
awaitableTiers,
|
||||
]);
|
||||
|
||||
const ids = Object.keys(tankDefinitions.tanks);
|
||||
@@ -193,8 +195,7 @@ export function Guesser() {
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{times(10, (index) => {
|
||||
const tier = 10 - index;
|
||||
{TIERS.map((tier) => {
|
||||
const isSelected = tiers.includes(tier);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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,8 +25,8 @@ import {
|
||||
Tooltip,
|
||||
type FlexProps,
|
||||
} from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { Fragment, type ComponentProps, type ReactNode } from "react";
|
||||
import { awaitableTiers } from "../../../core/awaitables/tiers";
|
||||
import { api } from "../../../core/blitzkit/api";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { App } from "../../../stores/app";
|
||||
@@ -171,7 +171,7 @@ const GUN_TYPES = Object.keys(
|
||||
|
||||
const MAX_ICONS = 4;
|
||||
|
||||
const TIERS = times(10, (i) => 10 - i);
|
||||
const TIERS = await awaitableTiers;
|
||||
|
||||
export function FilterControl() {
|
||||
return (
|
||||
@@ -789,6 +789,7 @@ function OwnershipFilterInternal(props: FlexProps) {
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.showOwned = !draft.showOwned;
|
||||
if (draft.showOwned) draft.showUnowned = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -808,6 +809,7 @@ function OwnershipFilterInternal(props: FlexProps) {
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.showUnowned = !draft.showUnowned;
|
||||
if (draft.showUnowned) draft.showOwned = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -834,6 +836,7 @@ function TestFilter() {
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.showNonTesting = !draft.showNonTesting;
|
||||
if (draft.showNonTesting) draft.showTesting = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -854,6 +857,7 @@ function TestFilter() {
|
||||
onClick={() => {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.showTesting = !draft.showTesting;
|
||||
if (draft.showTesting) draft.showNonTesting = false;
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
draft.search = sanitized.length === 0 ? null : sanitized;
|
||||
});
|
||||
}, 500),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
const handleChange = useCallback(() => {
|
||||
if (!searching) {
|
||||
@@ -46,7 +46,14 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
}, [searching]);
|
||||
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
||||
(event) => {
|
||||
if (event.key !== "Enter" || !topResult || searching) return;
|
||||
if (
|
||||
event.key !== "Enter" ||
|
||||
!topResult ||
|
||||
!TankFilters.state.search ||
|
||||
TankFilters.state.searching
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -56,7 +63,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
window.location.href = `/tanks/${topResult.slug}`;
|
||||
}
|
||||
},
|
||||
[topResult]
|
||||
[topResult],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@ import { AssaultRangesVisualizer } from "./AssaultRangesVisualizer";
|
||||
import { GunFlexibilityVisualizer } from "./GunFlexibilityVisualizer";
|
||||
import { Info } from "./Info";
|
||||
import { InfoWithDelta } from "./InfoWithDelta";
|
||||
import { ReloadVisualizer } from "./ReloadVisualizer";
|
||||
import { RELOAD_PRECISION, ReloadVisualizer } from "./ReloadVisualizer";
|
||||
import { RicochetVisualizer } from "./RicochetVisualizer";
|
||||
import { StatsTableWrapper } from "./StatsTableWrapper";
|
||||
import type { StatsAcceptorProps } from "./TraverseVisualizer";
|
||||
@@ -163,7 +163,7 @@ export function Firepower({
|
||||
.shell_index,
|
||||
{ index: index + 1 },
|
||||
)}
|
||||
decimals={2}
|
||||
decimals={RELOAD_PRECISION}
|
||||
deltaType="lowerIsBetter"
|
||||
noRanking
|
||||
value={() => reload}
|
||||
@@ -173,7 +173,7 @@ export function Firepower({
|
||||
) : (
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
decimals={2}
|
||||
decimals={RELOAD_PRECISION}
|
||||
deltaType="lowerIsBetter"
|
||||
value="shellReload"
|
||||
/>
|
||||
@@ -183,7 +183,7 @@ export function Firepower({
|
||||
<InfoWithDelta
|
||||
stats={stats}
|
||||
indent
|
||||
decimals={2}
|
||||
decimals={RELOAD_PRECISION}
|
||||
deltaType="lowerIsBetter"
|
||||
value="intraClip"
|
||||
/>
|
||||
|
||||
+24
-3
@@ -1,6 +1,7 @@
|
||||
import type { QuicklimeEvent } from "quicklime";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { Group } from "three";
|
||||
import { degToRad } from "three/src/math/MathUtils.js";
|
||||
import { api } from "../../../../../../../../core/blitzkit/api";
|
||||
import {
|
||||
modelTransformEvent,
|
||||
@@ -21,6 +22,13 @@ export function CanvasContent() {
|
||||
const tankModel = modelDefinitions.models[tank.id];
|
||||
const turretModel = tankModel.turrets[turret.id];
|
||||
|
||||
// baked-in turret mount tilt (e.g. Minotauro sits pitched 3deg forward);
|
||||
// applied as a static base rotation so the live gun pitch composes on top
|
||||
// of it, mirroring useTankTransform.ts's turretContainer/gunContainer nesting
|
||||
const initialTurretPitch = -degToRad(
|
||||
tankModel.initial_turret_rotation?.pitch ?? 0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function handleModelTransformEvent(
|
||||
event: QuicklimeEvent<ModelTransformEventData>,
|
||||
@@ -45,10 +53,23 @@ export function CanvasContent() {
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<ModelChunk only="turret" />
|
||||
<group
|
||||
position={[0, turretModel.gun_origin!.y, turretModel.gun_origin!.z]}
|
||||
rotation={[initialTurretPitch, 0, 0]}
|
||||
>
|
||||
<group
|
||||
position={[
|
||||
0,
|
||||
-turretModel.gun_origin!.y,
|
||||
-turretModel.gun_origin!.z,
|
||||
]}
|
||||
>
|
||||
<ModelChunk only="turret" />
|
||||
|
||||
<group ref={gunWrapper}>
|
||||
<ModelChunk only="gun" />
|
||||
<group ref={gunWrapper}>
|
||||
<ModelChunk only="gun" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</>
|
||||
);
|
||||
|
||||
+30
-14
@@ -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,18 +44,33 @@ 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];
|
||||
const gunModel = turretModel.guns[gun.id];
|
||||
|
||||
// turret's own baked-in mount tilt (e.g. Minotauro's turret sits pitched
|
||||
// 3deg forward on the hull); must be folded into displayed pitch values
|
||||
// the same way tankCharacteristics.ts and QuickInputs do
|
||||
const initialTurretPitch = degToRad(
|
||||
tankModel.initial_turret_rotation?.pitch ?? 0,
|
||||
);
|
||||
|
||||
// radial map is in the tilt-compensated frame so the neutrality circle sits
|
||||
// at the displayed 0deg
|
||||
const tiltDeg = radToDeg(initialTurretPitch);
|
||||
const magTilted = (deg: number) => mag(deg + tiltDeg);
|
||||
|
||||
const [yaw, setYaw] = useState(0);
|
||||
const [pitch, setPitch] = useState(0);
|
||||
const [minPitch, setMinPitch] = useState(
|
||||
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max),
|
||||
-degToRad(gunModel.pitch!.front?.max ?? gunModel.pitch!.max) -
|
||||
initialTurretPitch,
|
||||
);
|
||||
const [maxPitch, setMaxPitch] = useState(
|
||||
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min),
|
||||
-degToRad(gunModel.pitch!.front?.min ?? gunModel.pitch!.min) -
|
||||
initialTurretPitch,
|
||||
);
|
||||
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
@@ -70,7 +86,7 @@ export function GunFlexibilityVisualizer({
|
||||
let d = "";
|
||||
|
||||
// right
|
||||
const mo = mag(p.max);
|
||||
const mo = magTilted(p.max);
|
||||
if (y) {
|
||||
d += `M 0 0`;
|
||||
d += `L ${c(90 - y.max, mo)}`;
|
||||
@@ -87,7 +103,7 @@ export function GunFlexibilityVisualizer({
|
||||
|
||||
// front
|
||||
if (f) {
|
||||
const m = mag(f.max);
|
||||
const m = magTilted(f.max);
|
||||
d += `L ${c(90 - f.range / 2, m)}`;
|
||||
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||
}
|
||||
@@ -108,7 +124,7 @@ export function GunFlexibilityVisualizer({
|
||||
|
||||
// back
|
||||
if (!y && b) {
|
||||
const m = mag(b.max);
|
||||
const m = magTilted(b.max);
|
||||
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||
}
|
||||
@@ -116,7 +132,7 @@ export function GunFlexibilityVisualizer({
|
||||
d += `Z`;
|
||||
|
||||
// right
|
||||
const mi = mag(p.min);
|
||||
const mi = magTilted(p.min);
|
||||
if (y) {
|
||||
d += `M 0 0`;
|
||||
d += `L ${c(90 - y.max, mi)}`;
|
||||
@@ -133,7 +149,7 @@ export function GunFlexibilityVisualizer({
|
||||
|
||||
// front
|
||||
if (f) {
|
||||
const m = mag(f.min);
|
||||
const m = magTilted(f.min);
|
||||
d += `L ${c(90 - f.range / 2, m)}`;
|
||||
d += `A ${m} ${m} 0 0 0 ${c(90 + f.range / 2, m)}`;
|
||||
}
|
||||
@@ -154,14 +170,14 @@ export function GunFlexibilityVisualizer({
|
||||
|
||||
// back
|
||||
if (!y && b) {
|
||||
const m = mag(b.min);
|
||||
const m = magTilted(b.min);
|
||||
d += `L ${c(-90 - b.range / 2, m)}`;
|
||||
d += `A ${m} ${m} 0 0 0 ${c(-90 + b.range / 2, m)}`;
|
||||
}
|
||||
|
||||
d += `Z`;
|
||||
|
||||
const r = mag(radToDeg(-pitch));
|
||||
const r = mag(radToDeg(-(pitch - initialTurretPitch)));
|
||||
const dotX = r * Math.sin(yaw);
|
||||
const dotY = r * Math.cos(yaw);
|
||||
|
||||
@@ -179,7 +195,7 @@ export function GunFlexibilityVisualizer({
|
||||
top="0"
|
||||
left="0"
|
||||
>
|
||||
{!skeleton && <FlexibilityCanvas />}
|
||||
{!skeleton && modelRequested && <FlexibilityCanvas />}
|
||||
|
||||
<Box
|
||||
position="absolute"
|
||||
@@ -251,13 +267,13 @@ export function GunFlexibilityVisualizer({
|
||||
yaw = min[1];
|
||||
let t = 2 * Math.sqrt((u / rect.width) ** 2 + (v / rect.height) ** 2);
|
||||
t = clamp(t, 0, 1);
|
||||
let pitch = degToRad(magInverse(-t));
|
||||
let pitch = degToRad(magInverse(-t)) + initialTurretPitch;
|
||||
pitch = clamp(pitch, min[0], max[0]);
|
||||
|
||||
setPitch(pitch);
|
||||
setYaw(yaw);
|
||||
setMinPitch(min[0]);
|
||||
setMaxPitch(max[0]);
|
||||
setMinPitch(min[0] - initialTurretPitch);
|
||||
setMaxPitch(max[0] - initialTurretPitch);
|
||||
|
||||
modelTransformEvent.dispatch({ pitch, yaw });
|
||||
}}
|
||||
@@ -376,7 +392,7 @@ export function GunFlexibilityVisualizer({
|
||||
<VisualizerCornerStat
|
||||
label={strings.website.tools.tankopedia.visualizers.flexibility.pitch}
|
||||
value={literals(strings.common.units.deg, {
|
||||
value: radToDeg(pitch).toFixed(0),
|
||||
value: radToDeg(pitch - initialTurretPitch).toFixed(0),
|
||||
})}
|
||||
side="top-left"
|
||||
/>
|
||||
|
||||
+7
-7
@@ -11,7 +11,7 @@ import { Shell } from './components/Shell';
|
||||
import { Target } from './components/Target';
|
||||
import './index.css';
|
||||
|
||||
const PRECISION = 2;
|
||||
export const RELOAD_PRECISION = 1;
|
||||
|
||||
export interface ReloadUpdateData {
|
||||
reload: number;
|
||||
@@ -82,9 +82,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
totalTime.current.innerHTML = shellReload.toFixed(PRECISION);
|
||||
totalTime.current.innerHTML = shellReload.toFixed(RELOAD_PRECISION);
|
||||
progressTime.current.innerHTML = ((1 - reload) * shellReload).toFixed(
|
||||
PRECISION,
|
||||
RELOAD_PRECISION,
|
||||
);
|
||||
|
||||
if (state.current >= reloadThreshold) shoot();
|
||||
@@ -117,9 +117,9 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
shells = times(stats.shells, (index) => index > state.current - 1);
|
||||
}
|
||||
|
||||
totalTime.current.innerHTML = interval.toFixed(PRECISION);
|
||||
totalTime.current.innerHTML = interval.toFixed(RELOAD_PRECISION);
|
||||
progressTime.current.innerHTML = ((1 - reload) * interval).toFixed(
|
||||
PRECISION,
|
||||
RELOAD_PRECISION,
|
||||
);
|
||||
|
||||
if (lastState % 1 > state.current % 1) shoot();
|
||||
@@ -262,7 +262,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
transform: 'translate(50%, -50%)',
|
||||
}}
|
||||
>
|
||||
0.00
|
||||
{(0).toFixed(RELOAD_PRECISION)}
|
||||
</Code>
|
||||
<Code
|
||||
ref={totalTime}
|
||||
@@ -277,7 +277,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
0.00
|
||||
{(0).toFixed(RELOAD_PRECISION)}
|
||||
</Code>
|
||||
|
||||
{stats.shells > 1 &&
|
||||
|
||||
+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)",
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { alias, availableProvisions } from "@blitzkit/core";
|
||||
import { ShuffleIcon } from "@radix-ui/react-icons";
|
||||
import { Flex, IconButton, Popover } from "@radix-ui/themes";
|
||||
import { api } from "../../../../../../core/blitzkit/api";
|
||||
import { SPALL_LINER_PROVISION_ID } from "../../../../../../core/blitzkit/spallLiner";
|
||||
import { useLocale } from "../../../../../../hooks/useLocale";
|
||||
import { Duel } from "../../../../../../stores/duel";
|
||||
import { Tankopedia } from "../../../../../../stores/tankopedia";
|
||||
|
||||
const provisionDefinitions = await api.provisionDefinitions();
|
||||
|
||||
export function SpallLinerSwitcher() {
|
||||
const { tank, gun, provisions } = Duel.use((state) => state.protagonist);
|
||||
const { unwrap } = useLocale();
|
||||
const isSpallLinerActive = provisions.includes(SPALL_LINER_PROVISION_ID);
|
||||
const hasSlotAvailable = provisions.length < tank.max_provisions;
|
||||
|
||||
const canEquipSpallLiner = availableProvisions(
|
||||
tank,
|
||||
gun,
|
||||
provisionDefinitions,
|
||||
).some((provision) => provision.id === SPALL_LINER_PROVISION_ID);
|
||||
|
||||
if (!canEquipSpallLiner) return null;
|
||||
|
||||
function equip(replacing?: number) {
|
||||
Duel.mutate((draft) => {
|
||||
if (replacing !== undefined) {
|
||||
const provisions = draft.protagonist.provisions;
|
||||
provisions[provisions.indexOf(replacing)] = SPALL_LINER_PROVISION_ID;
|
||||
} else {
|
||||
draft.protagonist.provisions.push(SPALL_LINER_PROVISION_ID);
|
||||
}
|
||||
});
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function unequip() {
|
||||
Duel.mutate((draft) => {
|
||||
draft.protagonist.provisions = draft.protagonist.provisions.filter(
|
||||
(id) => id !== SPALL_LINER_PROVISION_ID,
|
||||
);
|
||||
});
|
||||
Tankopedia.mutate((draft) => {
|
||||
draft.shot = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
const button = (
|
||||
<IconButton
|
||||
color={isSpallLinerActive ? undefined : "gray"}
|
||||
variant="soft"
|
||||
size={{ initial: "2", sm: "3" }}
|
||||
radius="medium"
|
||||
onClick={() => {
|
||||
if (isSpallLinerActive) {
|
||||
unequip();
|
||||
} else if (hasSlotAvailable) {
|
||||
equip();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={unwrap(
|
||||
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
|
||||
)}
|
||||
src={alias("api", `/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`)}
|
||||
style={{
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
if (isSpallLinerActive || hasSlotAvailable) return button;
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>{button}</Popover.Trigger>
|
||||
|
||||
<Popover.Content>
|
||||
<Flex gap="2" align="center">
|
||||
<img
|
||||
alt={unwrap(
|
||||
provisionDefinitions.provisions[SPALL_LINER_PROVISION_ID].name!,
|
||||
)}
|
||||
src={alias(
|
||||
"api",
|
||||
`/icons/provisions/${SPALL_LINER_PROVISION_ID}.webp`,
|
||||
)}
|
||||
style={{
|
||||
width: "1.25em",
|
||||
height: "1.25em",
|
||||
}}
|
||||
/>
|
||||
|
||||
<ShuffleIcon color="var(--gray-9)" />
|
||||
|
||||
<Flex gap="2">
|
||||
{provisions.map((id) => (
|
||||
<Popover.Close key={id}>
|
||||
<IconButton
|
||||
color="gray"
|
||||
variant="soft"
|
||||
size="3"
|
||||
radius="medium"
|
||||
onClick={() => equip(id)}
|
||||
>
|
||||
<img
|
||||
alt={unwrap(provisionDefinitions.provisions[id].name!)}
|
||||
src={alias("api", `/icons/provisions/${id}.webp`)}
|
||||
style={{
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Popover.Close>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import { TankSearch } from "../../../../TankSearch";
|
||||
import { CustomShellButton } from "./components/CustomShellButton";
|
||||
import { DynamicArmorSwitcher } from "./components/DynamicArmorSwitcher";
|
||||
import { QuickInputs } from "./components/QuickInputs";
|
||||
import { SpallLinerSwitcher } from "./components/SpallLinerSwitcher";
|
||||
import { Thicknesses } from "./components/Thicknesses";
|
||||
|
||||
type OptionsProps = MaybeSkeletonComponentProps & {
|
||||
@@ -76,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);
|
||||
|
||||
@@ -278,9 +280,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<SpallLinerSwitcher />
|
||||
</Flex>
|
||||
|
||||
{!skeleton && (
|
||||
{!skeleton && modelRequested && (
|
||||
<Suspense>
|
||||
<DynamicArmorSwitcher />
|
||||
</Suspense>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ export function Tracker({ fontSize }: Props) {
|
||||
top="50%"
|
||||
width="min(100%, 16rem)"
|
||||
style={{
|
||||
transform: `translate(-50%, calc(${fontSize} / 2 + var(--space-4)))`,
|
||||
transform: `translate(-50%, calc(${fontSize} * 0.75 + var(--space-4)))`,
|
||||
}}
|
||||
>
|
||||
<Progress color="gray" value={progress} />
|
||||
|
||||
+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>
|
||||
);
|
||||
}
|
||||
+1
@@ -82,6 +82,7 @@ export function Lighting({ hasPbr }: Props) {
|
||||
blur={2 ** 1}
|
||||
resolution={2 ** 9}
|
||||
depthWrite={false}
|
||||
renderOrder={1}
|
||||
/>
|
||||
|
||||
{animate && (
|
||||
|
||||
+11
-5
@@ -51,11 +51,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,6 +1,6 @@
|
||||
import { Box, Flex } from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { Suspense, useEffect, useMemo, useRef } from "react";
|
||||
import { api } from "../../../core/blitzkit/api";
|
||||
import { defaultEqualizer } from "../../../core/blitzkit/tankToDuelMember";
|
||||
import { useFullScreen } from "../../../hooks/useFullScreen";
|
||||
@@ -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 &&
|
||||
|
||||
@@ -191,7 +191,7 @@ export function TechTreeSection({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
);
|
||||
})}
|
||||
|
||||
{master.tier < 10 && (
|
||||
{master.successors.length > 0 && (
|
||||
<>
|
||||
<Arrow />
|
||||
{master.successors!.map((id, index) => (
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { times } from "lodash-es";
|
||||
import { api } from "../blitzkit/api";
|
||||
|
||||
export const awaitableTiers = api.tankDefinitions().then((tankDefinitions) => {
|
||||
let max = 0;
|
||||
|
||||
for (const tank of Object.values(tankDefinitions.tanks)) {
|
||||
if (tank.tier > max) max = tank.tier;
|
||||
}
|
||||
|
||||
return times(max, (index) => max - index);
|
||||
});
|
||||
@@ -16,13 +16,15 @@ export async function filterTank(
|
||||
owned: number[] = [],
|
||||
) {
|
||||
return (
|
||||
((filters.showOwned && owned.includes(tank.id)) ||
|
||||
((!filters.showOwned && !filters.showUnowned) ||
|
||||
(filters.showOwned && owned.includes(tank.id)) ||
|
||||
(filters.showUnowned && !owned.includes(tank.id))) &&
|
||||
(filters.tiers.length === 0 || filters.tiers.includes(tank.tier)) &&
|
||||
(filters.nations.length === 0 || filters.nations.includes(tank.nation)) &&
|
||||
(filters.classes.length === 0 || filters.classes.includes(tank.class)) &&
|
||||
(filters.types.length === 0 || filters.types.includes(tank.type)) &&
|
||||
((filters.showTesting && tank.testing) ||
|
||||
((!filters.showTesting && !filters.showNonTesting) ||
|
||||
(filters.showTesting && tank.testing) ||
|
||||
(filters.showNonTesting && !tank.testing)) &&
|
||||
(filters.gunType.length === 0 ||
|
||||
(filters.gunType.includes("regular") &&
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const SPALL_LINER_PROVISION_ID = 101;
|
||||
|
||||
/**
|
||||
* Matches the in-game reduction Spall Liner grants against HE splash
|
||||
* damage. Shared between the characteristics panel and the Dynamic
|
||||
* Armor 3D viewer's damage readout so the figure isn't duplicated.
|
||||
*/
|
||||
export const SPALL_LINER_HE_DAMAGE_DELTA = -0.2;
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "@blitzkit/core";
|
||||
import { coefficient } from "@blitzkit/core/src/blitzkit/coefficient";
|
||||
import type { EquipmentMatrix } from "../../stores/duel";
|
||||
import { SPALL_LINER_HE_DAMAGE_DELTA } from "./spallLiner";
|
||||
import { defaultEqualizer } from "./tankToDuelMember";
|
||||
|
||||
export type TankCharacteristics = ReturnType<typeof tankCharacteristics>;
|
||||
@@ -170,7 +171,10 @@ export function tankCharacteristics(
|
||||
coefficient(
|
||||
[applyReactiveArmor && shell.type !== ShellType.SHELL_TYPE_HE, -0.27],
|
||||
[applyDynamicArmor, -0.1],
|
||||
[applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE, -0.2],
|
||||
[
|
||||
applySpallLiner && shell.type === ShellType.SHELL_TYPE_HE,
|
||||
SPALL_LINER_HE_DAMAGE_DELTA,
|
||||
],
|
||||
);
|
||||
const assaultDamageCoefficient =
|
||||
gun.assault_ranges && gun.assault_ranges.types.includes(shell.type)
|
||||
|
||||
@@ -3,11 +3,13 @@ import { useRef } from "react";
|
||||
|
||||
export function useStableProgress() {
|
||||
const { loaded, total } = useProgress();
|
||||
const baseline = useRef(loaded);
|
||||
const progress = useRef(0);
|
||||
const pending = total - baseline.current;
|
||||
|
||||
progress.current = Math.max(
|
||||
progress.current,
|
||||
total === 0 ? 0 : (loaded / total) * 100
|
||||
pending <= 0 ? 0 : ((loaded - baseline.current) / pending) * 100
|
||||
);
|
||||
|
||||
return progress.current;
|
||||
|
||||
@@ -67,7 +67,7 @@ export const getStaticPaths = mixStaticPaths(_getStaticPaths, () => {
|
||||
|
||||
{
|
||||
locale !== locales.default && (
|
||||
<Flex justify="center" mb="4">
|
||||
<Flex justify="center" pt="4" my="4">
|
||||
<Link
|
||||
href="https://crowdin.com/project/blitzkrieg"
|
||||
target="_blank"
|
||||
|
||||
@@ -17,7 +17,10 @@ export function Page({
|
||||
}: Props & MaybeSkeletonComponentProps) {
|
||||
const generated = Playlist.use((state) => state.list !== undefined);
|
||||
|
||||
if (App.state.logins.wargaming) TankFilters.state.showUnowned = false;
|
||||
if (App.state.logins.wargaming) {
|
||||
TankFilters.state.showOwned = true;
|
||||
TankFilters.state.showUnowned = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
|
||||
@@ -18,6 +18,10 @@ export function Page({ locale }: LocaleAcceptorProps) {
|
||||
|
||||
function Content({ locale }: LocaleAcceptorProps) {
|
||||
const developerMode = App.useDeferred((state) => state.developerMode, false);
|
||||
const autoLoadModels = App.useDeferred(
|
||||
(state) => state.autoLoadModels,
|
||||
true,
|
||||
);
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -36,6 +40,19 @@ function Content({ locale }: LocaleAcceptorProps) {
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="2" justify="between">
|
||||
<Text>{strings.website.settings.auto_load_models}</Text>
|
||||
<Switch
|
||||
variant="classic"
|
||||
checked={autoLoadModels}
|
||||
onCheckedChange={(checked) =>
|
||||
App.mutate((draft) => {
|
||||
draft.autoLoadModels = checked;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex align="center" gap="2" justify="between">
|
||||
<Text>{strings.website.settings.language}</Text>
|
||||
<LocaleSwitcher locale={locale} />
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface WargamingLogin {
|
||||
|
||||
export interface App {
|
||||
developerMode: boolean;
|
||||
autoLoadModels: boolean;
|
||||
policiesAgreementIndex: number;
|
||||
logins: {
|
||||
wargaming?: WargamingLogin;
|
||||
@@ -17,6 +18,7 @@ export interface App {
|
||||
export const App = new Varuna<App>(
|
||||
{
|
||||
developerMode: false,
|
||||
autoLoadModels: true,
|
||||
policiesAgreementIndex: -1,
|
||||
logins: {},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { TankDefinition } from "@blitzkit/core";
|
||||
import { times } from "lodash-es";
|
||||
import { Varuna } from "varuna";
|
||||
import { awaitableTiers } from "../core/awaitables/tiers";
|
||||
|
||||
const tiers = await awaitableTiers;
|
||||
|
||||
export enum GuessState {
|
||||
Correct,
|
||||
@@ -25,5 +27,5 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
||||
correctGuesses: 0,
|
||||
streak: 0,
|
||||
helpingReveal: false,
|
||||
tiers: times(10, (index) => index + 1),
|
||||
tiers,
|
||||
}));
|
||||
@@ -46,8 +46,8 @@ export const TankFilters = new Varuna<TankFilters>({
|
||||
abilities: [],
|
||||
powers: [],
|
||||
|
||||
showTesting: true,
|
||||
showNonTesting: true,
|
||||
showOwned: true,
|
||||
showUnowned: true,
|
||||
showTesting: false,
|
||||
showNonTesting: false,
|
||||
showOwned: false,
|
||||
showUnowned: false,
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import type { ArmorType } from "../components/Armor/components/SpacedArmorScene"
|
||||
import type { ExternalModuleVariant } from "../components/Armor/components/SpacedArmorSceneComponent";
|
||||
import type { XP_MULTIPLIERS } from "../components/Tankopedia/TechTreeSection";
|
||||
import { api } from "../core/blitzkit/api";
|
||||
import { App } from "./app";
|
||||
import { TankopediaDisplay } from "./tankopediaPersistent/constants";
|
||||
|
||||
export interface ShotLayerBase {
|
||||
@@ -79,6 +80,7 @@ export enum TankopediaRelativeAgainst {
|
||||
interface Tankopedia {
|
||||
disturbed: boolean;
|
||||
revealed: boolean;
|
||||
modelRequested: boolean;
|
||||
shot?: Shot;
|
||||
skills: Record<string, number>;
|
||||
relativeAgainst: TankopediaRelativeAgainst;
|
||||
@@ -110,6 +112,7 @@ const skillDefinitions = await api.skillDefinitions();
|
||||
export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
|
||||
disturbed: false,
|
||||
revealed: false,
|
||||
modelRequested: App.state.autoLoadModels,
|
||||
relativeAgainst: TankopediaRelativeAgainst.Class,
|
||||
editStatic: false,
|
||||
skills: createDefaultSkills(skillDefinitions),
|
||||
|
||||
Reference in new issue
Block a user