Compare commits

...
13 Commits
Author SHA1 Message Date
TrèsAbhi abfb3f8ddb fix: typo 2025-08-29 23:58:47 -05:00
TrèsAbhi 9cb5113f6e feat: haha 2025-08-29 23:58:14 -05:00
TrèsAbhi 7dd1c0c32a feat: logs 2025-08-29 23:57:33 -05:00
TrèsAbhi 03233f3fdc fix: align antagonist selector filters correctly 2025-08-29 23:46:57 -05:00
TrèsAbhi 377159c298 fix: consider tank models 2025-08-29 23:41:35 -05:00
TrèsAbhi d8431fb620 fix: gap alignemnt 2025-08-29 23:28:49 -05:00
TrèsAbhi 0546ddefbd fix: better shot card alignment 2025-08-29 23:24:12 -05:00
TrèsAbhi 26f6889012 feat: revert subtler advanced highlighting coloring 2025-08-29 23:10:20 -05:00
TrèsAbhi 3ad6088565 feat: skip button for reload visualizer 2025-08-29 23:08:44 -05:00
TrèsAbhi b6fd568cc6 fix: screenshot props 2025-08-29 23:00:02 -05:00
TrèsAbhi 3861b07aef fix: remove "show grid" 2025-08-29 22:53:04 -05:00
TrèsAbhi 2bf57e2c72 feat: better framing 2025-08-29 22:51:24 -05:00
TrèsAbhi 744c6654cf feat: cooler initial position 2025-08-29 22:39:34 -05:00
17 changed files with 187 additions and 198 deletions

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
# BlitzKit 1.27.19
Two updates in a day? I just got a little excited by the new Tankopedia lighting.
## Features
- The camera is now lower down in the Tankopedia, giving the tank a more heroic feel
- Tanks are better framed across all devices, minimizing whitespace
- Added a skip button for the reload visualizer to skip to the next event
## Fixes
- Removed the useless "show grid" setting
- Taking screenshots now no longer includes the scene props like the floor
- The subtler color contrast for the advanced highlighting has now been reverted back to the higher contrast
- The shot display card is now properly aligned as a proper table again
- Tank modules are once again considered in the penetration tester
- Tank filters in the antagonist selector in the Tankopedia sandbox are now properly aligned
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "blitzkit",
"private": true,
"version": "1.27.18",
"version": "1.27.19",
"description": "🎉 Tools for everything World of Tanks Blitz",
"type": "module",
"types": "./types.d.ts",
+5 -3
View File
@@ -894,6 +894,10 @@
"tank_seo_description": "%s1 armor, statistics, equipment, provisions, and more. %s2 DPM, %s3hp alpha, %s4mm penetration, %s5kph top speed, %s6hp health.",
"visualizers": {
"reload": {
"skip": "Skip"
},
"assault": {
"title": "Assault shells"
},
@@ -942,9 +946,7 @@
"green_penetration": "Green penetration",
"hide_model_under_armor": "Hide model under armor",
"opaque": "Opaque",
"dev_wireframe": "DEV: wireframe",
"environment": "Environment",
"show_grid": "Show grid"
"dev_wireframe": "DEV: wireframe"
},
"screenshot": {
@@ -14,7 +14,6 @@ export const PrimaryArmorScene = memo(() => {
const wrapper = useRef<Group>(null);
const turretContainer = useRef<Group>(null);
const gunContainer = useRef<Group>(null);
// const protagonist = Duel.use((draft) => draft.protagonist);
const tank = Duel.use((state) => state.protagonist.tank);
const track = Duel.use((state) => state.protagonist.track);
const turret = Duel.use((state) => state.protagonist.turret);
@@ -37,7 +37,7 @@ float getDistance(vec2 coord, float depth) {
vec3 getPenetrationColor(bool isThreeCalibersRule, bool couldHaveRicochet) {
if (advancedHighlighting && couldHaveRicochet) {
return vec3(0.0, 1.0, isThreeCalibersRule ? 0.5 : 0.0);
return vec3(0.0, 1.0, isThreeCalibersRule ? 1.0 : 0.0);
}
return vec3(0.0, 1.0, 0.0);
@@ -107,7 +107,7 @@ void main() {
vec3 baseColor = vec3(1.0, splashChance * GREEN_VALUE, 0.0);
if (advancedHighlighting && didRicochet) {
baseColor = vec3(1.0, baseColor.g, 0.5);
baseColor = vec3(1.0, baseColor.g, 1.0);
}
if (greenPenetration || advancedHighlighting) {
@@ -37,44 +37,38 @@ function LayerEntry({
const { locale, strings } = useLocale();
return (
<Flex align="center" gap="2">
<Text size="1" color="gray" style={{ width: 12 }}>
{layerIndex}
</Text>
<Text size="2" color={shotStatusColor} style={{ width: 48 }}>
{layerName}
</Text>
<tr>
<td>
<Text size="1" color="gray">
{layerIndex}
</Text>
</td>
{layer.type === null && (
<>
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
<td>
<Text size="2" color={shotStatusColor}>
{layerName}
</Text>
</td>
<td>
{layer.type === null && (
<Text size="2" color={shotStatusColor}>
{literals(strings.common.units.mm, [
Math.round(layer.distance * 1000).toLocaleString(locale),
])}
</Text>
)}
<Text size="2" color="gray">
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.ricochet_loss,
[Math.max(-100, -50 * layer.distance).toFixed(0)],
)}
{layer.type === ArmorType.External && (
<Text size="2" color={shotStatusColor}>
{literals(strings.common.units.mm, [
Math.round(layer.thickness).toLocaleString(locale),
])}
</Text>
</>
)}
{layer.type === ArmorType.External && (
<Text size="2" color={shotStatusColor} style={{ width: 64 }}>
{literals(strings.common.units.mm, [
Math.round(layer.thickness).toLocaleString(locale),
])}
</Text>
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
<>
<Text size="2" color={shotStatusColor} style={{ width: 80 }}>
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
<Text size="2" color={shotStatusColor}>
{literals(
angle === undefined
? strings.common.units.mm
@@ -86,7 +80,22 @@ function LayerEntry({
],
)}
</Text>
)}
</td>
<td>
{layer.type === null && (
<Text size="2" color="gray">
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
.ricochet_loss,
[Math.max(-100, -50 * layer.distance).toFixed(0)],
)}
</Text>
)}
{(layer.type === ArmorType.Primary ||
layer.type === ArmorType.Spaced) && (
<Text size="2" color="gray">
{literals(
strings.website.tools.tankopedia.sandbox.dynamic.shot_card.stats
@@ -94,9 +103,9 @@ function LayerEntry({
[Math.round(layer.thickness).toLocaleString(locale)],
)}
</Text>
</>
)}
</Flex>
)}
</td>
</tr>
);
}
@@ -115,7 +124,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
const inTitleColor = shotStatusColors[shot.in.status];
return (
<Card variant="classic" style={{ width: 300 }} {...props}>
<Card variant="classic" {...props}>
<Flex direction="column" gap="2">
<Flex direction="column" gap="1">
<Text color={inTitleColor} weight="bold">
@@ -127,7 +136,12 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
)}
</Text>
<Flex direction="column">
<table
style={{
whiteSpace: 'nowrap',
borderSpacing: 'var(--space-1) 0',
}}
>
{shot.in.layers.map((layer, index) => {
const layerName =
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
@@ -161,7 +175,7 @@ export function ShotDisplayCard({ shot, ...props }: ShotDisplayCardProps) {
/>
);
})}
</Flex>
</table>
</Flex>
{shot.out && shot.out.layers.length > 0 && (
@@ -123,6 +123,7 @@ export function SpacedArmorSubExternal({
return (
<mesh
{...props}
onClick={() => {}}
key={`${key}-spaced-sub-external-exclude`}
renderOrder={3}
material={
@@ -207,7 +207,6 @@ export const StaticArmor = memo<ArmorSceneProps>(({ thicknessRange }) => {
<StaticArmorSceneComponent
name={node.name}
thicknessRange={thicknessRange}
key={node.uuid}
type={spaced ? ArmorType.Spaced : ArmorType.Primary}
thickness={thickness}
node={node}
@@ -1,17 +1,11 @@
import type { SpotLightProps } from '@react-three/fiber';
import { memo } from 'react';
import { Color } from 'three';
import { isHalloween } from '../core/blitzkit/isHalloween';
interface Props extends SpotLightProps {
debug?: boolean;
}
export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
const color = new Color();
if (isHalloween()) color.set(1, 0.25, 0.25);
return (
<>
{debug && (
@@ -21,7 +15,7 @@ export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
</mesh>
)}
<spotLight color={color} {...props} />
<spotLight {...props} />
</>
);
});
@@ -307,7 +307,7 @@ export function FilterControl({ compact }: FilterControlProps) {
</Flex>
<Flex
direction={{ initial: 'row', md: 'column' }}
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
align="start"
gap={{ initial: '2', md: '1' }}
>
@@ -1,8 +1,10 @@
import { Box, Code } from '@radix-ui/themes';
import { DoubleArrowRightIcon } from '@radix-ui/react-icons';
import { Box, Button, Code } from '@radix-ui/themes';
import { times } from 'lodash-es';
import { Quicklime, type QuicklimeCallback } from 'quicklime';
import { useCallback, useEffect, useRef } from 'react';
import { Var } from '../../../../../../../core/radix/var';
import { useLocale } from '../../../../../../../hooks/useLocale';
import type { StatsAcceptorProps } from '../TraverseVisualizer';
import { VisualizerCard } from '../VisualizerCard';
import { Shell } from './components/Shell';
@@ -19,6 +21,8 @@ export interface ReloadUpdateData {
export const reloadUpdate = new Quicklime<ReloadUpdateData>();
export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
const { strings } = useLocale();
const reloadCircle = useRef<HTMLDivElement>(null);
const reloadCore = useRef<HTMLDivElement>(null);
const reloadGlow = useRef<HTMLDivElement>(null);
@@ -246,7 +250,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
<Code
ref={progressTime}
size="1"
size="2"
variant="ghost"
weight="bold"
color="gray"
@@ -255,14 +259,14 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
position: 'absolute',
top: '50%',
right: 'calc(50% + var(--space-8))',
transform: 'translateX(50%)',
transform: 'translate(50%, -50%)',
}}
>
0.00
</Code>
<Code
ref={totalTime}
size="1"
size="2"
variant="ghost"
weight="bold"
color="gray"
@@ -270,7 +274,7 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
position: 'absolute',
top: '50%',
left: 'calc(50% + var(--space-8))',
transform: 'translateX(-50%)',
transform: 'translate(-50%, -50%)',
}}
>
0.00
@@ -290,6 +294,25 @@ export function ReloadVisualizer({ stats }: StatsAcceptorProps) {
width="100%"
height="100%"
/>
<Button
size="1"
variant="outline"
color="gray"
highContrast
style={{
position: 'absolute',
top: '68%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
onClick={() => {
state.current = Math.floor(state.current + 1);
}}
>
{strings.website.tools.tankopedia.visualizers.reload.skip}
<DoubleArrowRightIcon />
</Button>
</VisualizerCard>
);
}
@@ -12,7 +12,6 @@ import {
EnterFullScreenIcon,
ExitFullScreenIcon,
EyeOpenIcon,
GearIcon,
} from '@radix-ui/react-icons';
import {
Button,
@@ -33,7 +32,6 @@ import { useEquipment } from '../../../../../hooks/useEquipment';
import { useFullScreen } from '../../../../../hooks/useFullScreen';
import { useFullscreenAvailability } from '../../../../../hooks/useFullscreenAvailability';
import { useLocale } from '../../../../../hooks/useLocale';
import { App } from '../../../../../stores/app';
import { Duel } from '../../../../../stores/duel';
import { TankopediaEphemeral } from '../../../../../stores/tankopediaEphemeral';
import { TankopediaPersistent } from '../../../../../stores/tankopediaPersistent';
@@ -43,6 +41,7 @@ import type { ThicknessRange } from '../../../../Armor/components/StaticArmor';
import { ModuleButton } from '../../../../ModuleButtons/ModuleButton';
import { SmallTankIcon } from '../../../../SmallTankIcon';
import { TankSearch } from '../../../../TankSearch';
import { screenshotReadyEvent } from '../TankSandbox/components/SceneProps';
import { CustomShellButton } from './components/CustomShellButton';
import { DynamicArmorSwitcher } from './components/DynamicArmorSwitcher';
import { QuickInputs } from './components/QuickInputs';
@@ -59,20 +58,10 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
);
const display = TankopediaEphemeral.use((state) => state.display);
const isFullScreen = useFullScreen();
const showGrid = TankopediaPersistent.use((state) => state.showGrid);
const greenPenetration = TankopediaPersistent.use(
(state) => state.greenPenetration,
);
const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor,
);
const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting,
);
const wireframe = TankopediaPersistent.use((state) => state.wireframe);
const opaque = TankopediaPersistent.use((state) => state.opaque);
const fullScreenAvailable = useFullscreenAvailability(true);
const developerMode = App.use((state) => state.developerMode);
const protagonistTank = Duel.use((state) => state.protagonist.tank);
const antagonistGun = Duel.use((state) => state.antagonist.gun);
const antagonistShell = Duel.use((state) => state.antagonist.shell);
@@ -330,6 +319,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Text color="gray" size="2">
{strings.website.tools.tankopedia.sandbox.dynamic.shooter}
</Text>
<Dialog.Root
open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorOpen}
@@ -451,106 +441,6 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</DropdownMenu.Content>
</DropdownMenu.Root>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton variant="ghost" color="gray">
<GearIcon />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{display === TankopediaDisplay.DynamicArmor && (
<>
<DropdownMenu.Label>
{
strings.website.tools.tankopedia.sandbox.settings
.armor
}
</DropdownMenu.Label>
<DropdownMenu.CheckboxItem
checked={greenPenetration}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.greenPenetration = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.green_penetration
}
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={hideTankModelUnderArmor}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.hideTankModelUnderArmor = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.hide_model_under_armor
}
</DropdownMenu.CheckboxItem>
<DropdownMenu.CheckboxItem
checked={opaque}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.opaque = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.opaque
}
</DropdownMenu.CheckboxItem>
{developerMode && (
<DropdownMenu.CheckboxItem
checked={wireframe}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.wireframe = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.dev_wireframe
}
</DropdownMenu.CheckboxItem>
)}
</>
)}
<DropdownMenu.Label>
{
strings.website.tools.tankopedia.sandbox.settings
.environment
}
</DropdownMenu.Label>
<DropdownMenu.CheckboxItem
checked={showGrid}
onCheckedChange={(checked) => {
mutateTankopediaPersistent((draft) => {
draft.showGrid = checked;
});
}}
>
{
strings.website.tools.tankopedia.sandbox.settings
.show_grid
}
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Popover.Root>
<Popover.Trigger>
<IconButton color="gray" variant="ghost">
@@ -563,19 +453,25 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Popover.Close>
<Button
onClick={() => {
if (!canvas.current) return;
screenshotReadyEvent.dispatch(true);
const anchor = document.createElement('a');
requestAnimationFrame(() => {
if (!canvas.current) return;
anchor.setAttribute(
'download',
`${protagonistTank.name}.png`,
);
anchor.setAttribute(
'href',
canvas.current.toDataURL('image/png'),
);
anchor.click();
const anchor = document.createElement('a');
anchor.setAttribute(
'download',
`${protagonistTank.name}.png`,
);
anchor.setAttribute(
'href',
canvas.current.toDataURL('image/png'),
);
anchor.click();
screenshotReadyEvent.dispatch(false);
});
}}
>
<DownloadIcon />
@@ -589,14 +485,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Button
variant="outline"
onClick={() => {
if (!canvas.current) return;
screenshotReadyEvent.dispatch(true);
canvas.current.toBlob((blob) => {
if (!blob) return;
requestAnimationFrame(() => {
if (!canvas.current) return;
navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob }),
]);
canvas.current.toBlob((blob) => {
if (!blob) return;
navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob }),
]);
});
screenshotReadyEvent.dispatch(false);
});
}}
>
@@ -82,7 +82,7 @@ export function Controls({
protagonistGunOrigin.y;
const inspectModeInitialPosition = new Vector3(
-8,
gunHeight + (naked ? 10 : 2),
naked ? gunHeight + 10 : 2,
-13,
);
@@ -238,7 +238,11 @@ export function Controls({
orbitControls.current.rotateSpeed = 0.25;
camera.position.copy(inspectModeInitialPosition);
orbitControls.current.target.set(0, gunHeight / (naked ? 4 : 2), 0);
orbitControls.current.target.set(
0,
gunHeight * (naked ? 1 / 4 : 3 / 4),
0,
);
orbitControls.current.enablePan = true;
orbitControls.current.enableZoom = true;
camera.fov = naked ? 20 : 25;
@@ -1,8 +1,9 @@
import { useHelper } from '@react-three/drei';
import { times } from 'lodash-es';
import { useRef } from 'react';
import { SpotLight, SpotLightHelper } from 'three';
import { Color, SpotLight, SpotLightHelper } from 'three';
import { degToRad } from 'three/src/math/MathUtils.js';
import { isHalloween } from '../../../../../../../core/blitzkit/isHalloween';
import { HelpingSpotLight } from '../../../../../../HelpingSpotLight';
const LIGHTS_COUNT = 5;
@@ -28,6 +29,9 @@ export function Lighting() {
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x,
LIGHT_DISTANCE * Math.cos(theta),
] as const;
const color = new Color();
if (isHalloween()) color.set(1, 0.25, 0.25);
return (
<HelpingSpotLight
@@ -36,6 +40,7 @@ export function Lighting() {
penumbra={1}
castShadow
decay={1}
color={color}
/>
);
})}
@@ -1,11 +1,15 @@
import { useFrame } from '@react-three/fiber';
import { invalidate, useFrame } from '@react-three/fiber';
import { clamp } from 'lodash-es';
import { useRef } from 'react';
import { MeshStandardMaterial } from 'three';
import { Quicklime, type QuicklimeEvent } from 'quicklime';
import { useEffect, useRef } from 'react';
import { Mesh, MeshStandardMaterial } from 'three';
const SIZE = 2 ** 5;
export const screenshotReadyEvent = new Quicklime(false);
export function SceneProps() {
const mesh = useRef<Mesh>(null!);
const material = useRef<MeshStandardMaterial>(null);
useFrame(({ camera }) => {
@@ -13,8 +17,26 @@ export function SceneProps() {
material.current.opacity = clamp(0.5 * camera.position.y, 0, 1);
});
useEffect(() => {
function handleScreenshotReady(event: QuicklimeEvent<boolean>) {
mesh.current.visible = !event.data;
invalidate();
}
screenshotReadyEvent.on(handleScreenshotReady);
return () => {
screenshotReadyEvent.off(handleScreenshotReady);
};
}, []);
return (
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<mesh
rotation={[-Math.PI / 2, 0, 0]}
receiveShadow
visible={!screenshotReadyEvent.last!}
ref={mesh}
>
<planeGeometry args={[SIZE, SIZE]} />
<meshStandardMaterial
transparent
@@ -97,8 +97,16 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
transitionDuration: '1s',
background: isFullScreen ? 'black' : undefined,
}}
height={!isFullScreen ? 'calc(100svh - 6rem)' : '100vh'}
maxHeight={isFullScreen ? undefined : '60rem'}
height={
isFullScreen
? '100vh'
: disturbed
? 'calc(100svh - 6rem)'
: {
initial: '28rem',
md: 'calc(100svh - 12rem)',
}
}
maxWidth={isFullScreen ? undefined : '120rem'}
flexGrow="1"
width={isFullScreen ? '100vw' : undefined}
@@ -12,7 +12,6 @@ export interface TankopediaPersistentStore {
wireframe: boolean;
opaque: boolean;
advancedHighlighting: boolean;
showGrid: boolean;
greenPenetration: boolean;
showSpacedArmor: boolean;
showExternalModules: boolean;
@@ -34,7 +33,6 @@ export const TankopediaPersistent = createContextualStore(() =>
wireframe: false,
opaque: false,
advancedHighlighting: false,
showGrid: true,
greenPenetration: false,
showSpacedArmor: true,
showExternalModules: true,