Merge branch 'main' into dev

This commit is contained in:
TrèsAbhi committed 2025-11-27 17:16:12 -06:00
commit 54b73d11ce
27 files changed
+203 -199

No files matched your search

+2 -8
View File
@@ -69,7 +69,6 @@
"@types/bun": "^1.3.1", "@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"crc": "^4.3.2", "crc": "^4.3.2",
@@ -79,7 +78,6 @@
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"pickleparser": "^0.2.1", "pickleparser": "^0.2.1",
"protobufjs": "^7.5.4", "protobufjs": "^7.5.4",
"steam-user": "^5.2.3",
"three": "^0.180.0", "three": "^0.180.0",
"ts-proto": "^2.7.7", "ts-proto": "^2.7.7",
"yaml": "^2.8.1", "yaml": "^2.8.1",
@@ -107,6 +105,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/progress": "^2.0.7", "@types/progress": "^2.0.7",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"crc": "^4.3.2", "crc": "^4.3.2",
"csv-parse": "^6.1.0", "csv-parse": "^6.1.0",
@@ -120,6 +119,7 @@
"progress": "^2.0.3", "progress": "^2.0.3",
"puppeteer": "^24.22.3", "puppeteer": "^24.22.3",
"sharp": "^0.34.4", "sharp": "^0.34.4",
"steam-user": "^5.2.3",
"three": "^0.180.0", "three": "^0.180.0",
"ts-proto": "^2.7.7", "ts-proto": "^2.7.7",
"yaml": "^2.8.1", "yaml": "^2.8.1",
@@ -2613,10 +2613,6 @@
"@blitzkit/bot/prisma": ["[email protected]", "", { "dependencies": { "@prisma/config": "6.17.1", "@prisma/engines": "6.17.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g=="], "@blitzkit/bot/prisma": ["[email protected]", "", { "dependencies": { "@prisma/config": "6.17.1", "@prisma/engines": "6.17.1" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g=="],
"@blitzkit/scripts/@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
"@blitzkit/scripts/fast-xml-parser": ["[email protected]", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
"@blitzkit/website/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="], "@blitzkit/website/vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg=="],
"@discordjs/rest/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], "@discordjs/rest/@discordjs/collection": ["@discordjs/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
@@ -2785,8 +2781,6 @@
"@blitzkit/bot/prisma/@prisma/engines": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "6.17.1", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", "@prisma/fetch-engine": "6.17.1", "@prisma/get-platform": "6.17.1" } }, "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw=="], "@blitzkit/bot/prisma/@prisma/engines": ["@prisma/[email protected]", "", { "dependencies": { "@prisma/debug": "6.17.1", "@prisma/engines-version": "6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac", "@prisma/fetch-engine": "6.17.1", "@prisma/get-platform": "6.17.1" } }, "sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw=="],
"@blitzkit/scripts/@types/bun/bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="], "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="], "@octokit/graphql/@octokit/types/@octokit/openapi-types": ["@octokit/[email protected]", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
+11
View File
@@ -0,0 +1,11 @@
# BlitzKit 1.29.5
## Features
- Added new lighting to be a bit softer on tanks
- Added a button with the sun icon to toggle high graphics
- Added a new reveal and transition animation between tabs
## Fixes
- Track wheels on some tanks no longer look translucent
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"version": "1.29.4", "version": "1.29.5",
"license": "BlitzKit License", "license": "BlitzKit License",
"private": true, "private": true,
"scripts": { "scripts": {
-2
View File
@@ -13,7 +13,6 @@
"@types/bun": "^1.3.1", "@types/bun": "^1.3.1",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"crc": "^4.3.2", "crc": "^4.3.2",
@@ -23,7 +22,6 @@
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"pickleparser": "^0.2.1", "pickleparser": "^0.2.1",
"protobufjs": "^7.5.4", "protobufjs": "^7.5.4",
"steam-user": "^5.2.3",
"three": "^0.180.0", "three": "^0.180.0",
"ts-proto": "^2.7.7", "ts-proto": "^2.7.7",
"yaml": "^2.8.1" "yaml": "^2.8.1"
-1
View File
@@ -6,6 +6,5 @@ export * from "./patreon";
export * from "./protobuf"; export * from "./protobuf";
export * from "./protos"; export * from "./protos";
export * from "./statistics"; export * from "./statistics";
export * from "./steam";
export * from "./streams"; export * from "./streams";
export * from "./types"; export * from "./types";
-1
View File
@@ -1 +0,0 @@
export * from "./vfs";
+2
View File
@@ -14,6 +14,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/lz4js": "^0.2.1", "@types/lz4js": "^0.2.1",
"@types/progress": "^2.0.7", "@types/progress": "^2.0.7",
"@types/steam-user": "^5.1.0",
"@types/three": "^0.180.0", "@types/three": "^0.180.0",
"crc": "^4.3.2", "crc": "^4.3.2",
"csv-parse": "^6.1.0", "csv-parse": "^6.1.0",
@@ -27,6 +28,7 @@
"progress": "^2.0.3", "progress": "^2.0.3",
"puppeteer": "^24.22.3", "puppeteer": "^24.22.3",
"sharp": "^0.34.4", "sharp": "^0.34.4",
"steam-user": "^5.2.3",
"three": "^0.180.0", "three": "^0.180.0",
"ts-proto": "^2.7.7", "ts-proto": "^2.7.7",
"yaml": "^2.8.1" "yaml": "^2.8.1"
@@ -1,4 +1,5 @@
import { assertSecret, SteamVFS } from "@blitzkit/core"; import { assertSecret } from "@blitzkit/core";
import { SteamVFS } from "../core/steam/vfs";
const APP = 444200; const APP = 444200;
const DEPOT = 444202; const DEPOT = 444202;
@@ -2,11 +2,11 @@ import {
Hierarchy, Hierarchy,
Sc2ReadStream, Sc2ReadStream,
ScgReadStream, ScgReadStream,
SteamVFS,
VertexAttribute, VertexAttribute,
} from "@blitzkit/core"; } from "@blitzkit/core";
import { Accessor, Document, Node, Scene } from "@gltf-transform/core"; import { Accessor, Document, Node, Scene } from "@gltf-transform/core";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { SteamVFS } from "../steam/vfs";
import { import {
vertexAttributeGLTFName, vertexAttributeGLTFName,
vertexAttributeGltfVectorSizes, vertexAttributeGltfVectorSizes,
@@ -2,7 +2,6 @@ import {
Hierarchy, Hierarchy,
Sc2ReadStream, Sc2ReadStream,
ScgReadStream, ScgReadStream,
SteamVFS,
Textures, Textures,
VertexAttribute, VertexAttribute,
} from "@blitzkit/core"; } from "@blitzkit/core";
@@ -10,6 +9,7 @@ import { Document, Material, Node, Scene } from "@gltf-transform/core";
import { dedup, prune } from "@gltf-transform/functions"; import { dedup, prune } from "@gltf-transform/functions";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { dirname } from "path"; import { dirname } from "path";
import { SteamVFS } from "../../steam/vfs";
import { readBaseColor } from "../readBaseColor"; import { readBaseColor } from "../readBaseColor";
import { readNormal } from "../readNormal"; import { readNormal } from "../readNormal";
import { readRoughnessMetallic } from "../readRoughnessMetallic"; import { readRoughnessMetallic } from "../readRoughnessMetallic";
@@ -1,8 +1,8 @@
import { readDVPL } from "@blitzkit/core/src/blitz";
import { XMLParser } from "fast-xml-parser"; import { XMLParser } from "fast-xml-parser";
import { normalize } from "path/posix"; import { normalize } from "path/posix";
import SteamUser, { EConnectionProtocol } from "steam-user"; import SteamUser, { EConnectionProtocol } from "steam-user";
import { parse } from "yaml"; import { parse } from "yaml";
import { readDVPL } from "../blitz";
interface SteamManifestFile { interface SteamManifestFile {
chunks: { chunks: {
Binary file not shown.
@@ -67,7 +67,9 @@ export const PrimaryArmorScene = memo(() => {
<group ref={turretContainer}> <group ref={turretContainer}>
{armorNodes.map((node) => { {armorNodes.map((node) => {
const isCurrentTurret = node.name.startsWith( const isCurrentTurret = node.name.startsWith(
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}` `turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`
); );
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
const armorId = nameToArmorId(node.name); const armorId = nameToArmorId(node.name);
@@ -4,6 +4,7 @@ import {
resolvePenetrationCoefficient, resolvePenetrationCoefficient,
} from "@blitzkit/core"; } from "@blitzkit/core";
import { invalidate, useFrame } from "@react-three/fiber"; import { invalidate, useFrame } from "@react-three/fiber";
import type { QuicklimeEvent } from "quicklime";
import { useEffect } from "react"; import { useEffect } from "react";
import { import {
MeshBasicMaterial, MeshBasicMaterial,
@@ -21,6 +22,7 @@ import { jsxTree } from "../../../../core/blitzkit/jsxTree";
import { Duel, type EquipmentMatrix } from "../../../../stores/duel"; import { Duel, type EquipmentMatrix } from "../../../../stores/duel";
import { Tankopedia } from "../../../../stores/tankopedia"; import { Tankopedia } from "../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent"; import { TankopediaPersistent } from "../../../../stores/tankopediaPersistent";
import { transitionEvent } from "../../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import fragmentShader from "./shaders/fragment.glsl?raw"; import fragmentShader from "./shaders/fragment.glsl?raw";
import vertexShader from "./shaders/vertex.glsl?raw"; import vertexShader from "./shaders/vertex.glsl?raw";
import { spacedArmorRenderTarget } from "./target"; import { spacedArmorRenderTarget } from "./target";
@@ -72,6 +74,8 @@ export function PrimaryArmorSceneComponent({
resolution: { value: new Vector2() }, resolution: { value: new Vector2() },
spacedArmorBuffer: { value: null }, spacedArmorBuffer: { value: null },
spacedArmorDepth: { value: null }, spacedArmorDepth: { value: null },
opacity: { value: 0 },
}, },
]), ]),
}); });
@@ -154,6 +158,10 @@ export function PrimaryArmorSceneComponent({
if (!noInvalidate) invalidate(); if (!noInvalidate) invalidate();
} }
function handleTransitionEvent(event: QuicklimeEvent<number>) {
material.uniforms.opacity.value = event.data;
}
handleShellChange(); handleShellChange();
handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration); handleGreenPenetrationChange(TankopediaPersistent.state.greenPenetration);
handleAdvancedHighlightingChange( handleAdvancedHighlightingChange(
@@ -164,6 +172,8 @@ export function PrimaryArmorSceneComponent({
handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix); handleProtagonistEquipmentChange(Duel.state.protagonist.equipmentMatrix);
handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix); handleAntagonistEquipmentChange(Duel.state.antagonist.equipmentMatrix);
transitionEvent.on(handleTransitionEvent);
const unsubscribes = [ const unsubscribes = [
Duel.on((state) => state.antagonist.shell, handleShellChange), Duel.on((state) => state.antagonist.shell, handleShellChange),
Tankopedia.on((state) => state.customShell, handleShellChange), Tankopedia.on((state) => state.customShell, handleShellChange),
@@ -184,12 +194,13 @@ export function PrimaryArmorSceneComponent({
(state) => state.antagonist.equipmentMatrix, (state) => state.antagonist.equipmentMatrix,
(equipment) => handleAntagonistEquipmentChange(equipment) (equipment) => handleAntagonistEquipmentChange(equipment)
), ),
() => transitionEvent.off(handleTransitionEvent),
]; ];
return () => { return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe()); unsubscribes.forEach((unsubscribe) => unsubscribe());
}; };
}); }, []);
useFrame(({ gl, camera }) => { useFrame(({ gl, camera }) => {
gl.getSize(material.uniforms.resolution.value).multiplyScalar( gl.getSize(material.uniforms.resolution.value).multiplyScalar(
@@ -1,12 +1,9 @@
precision mediump float; precision mediump float;
#define USE_FOG
#include <fog_pars_fragment>
varying vec3 vNormal; varying vec3 vNormal;
varying vec3 vViewPosition; varying vec3 vViewPosition;
uniform float opacity;
uniform float thickness; uniform float thickness;
uniform float penetration; uniform float penetration;
uniform float caliber; uniform float caliber;
@@ -119,5 +116,5 @@ void main() {
gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha); gl_FragColor = vec4(baseColor, (1.0 - penetrationChance) * alpha);
} }
#include <fog_fragment> gl_FragColor.a *= opacity;
} }
@@ -1,9 +1,5 @@
precision mediump float; precision mediump float;
#define USE_FOG
#include <fog_pars_vertex>
varying vec3 vNormal; varying vec3 vNormal;
varying vec3 vViewPosition; varying vec3 vViewPosition;
@@ -14,6 +10,4 @@ void main() {
vNormal = normalMatrix * normal; vNormal = normalMatrix * normal;
gl_Position = projectionMatrix * mvPosition; gl_Position = projectionMatrix * mvPosition;
#include <fog_vertex>
} }
@@ -24,6 +24,7 @@ import {
} from "../../../core/blitzkit/modelTransform"; } from "../../../core/blitzkit/modelTransform";
import { discardClippingPlane } from "../../../core/three/discardClippingPlane"; import { discardClippingPlane } from "../../../core/three/discardClippingPlane";
import { Tankopedia } from "../../../stores/tankopedia"; import { Tankopedia } from "../../../stores/tankopedia";
import { transitionEvent } from "../../Tankopedia/HeroSection/components/TankSandbox/components/Lighting";
import { ArmorType } from "./SpacedArmorScene"; import { ArmorType } from "./SpacedArmorScene";
import type { import type {
ArmorUserData, ArmorUserData,
@@ -108,10 +109,11 @@ export function StaticArmorSceneComponent({
() => () =>
new MeshBasicMaterial({ new MeshBasicMaterial({
color, color,
opacity, transparent: true,
transparent: opacity < 1,
depthWrite, depthWrite,
...(props.clip ? { clippingPlanes: [props.clip] } : {}), ...(props.clip ? { clippingPlanes: [props.clip] } : {}),
opacity: 0,
userData: { opacity0: opacity },
}), }),
[thickness] [thickness]
); );
@@ -121,6 +123,8 @@ export function StaticArmorSceneComponent({
color: color color: color
.clone() .clone()
.multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1), .multiplyScalar(props.type === ArmorType.Spaced ? 2 ** 2 : 2 ** -1),
opacity: 0,
transparent: true,
}), }),
[thickness] [thickness]
); );
@@ -163,11 +167,22 @@ export function StaticArmorSceneComponent({
surfaceMaterial.needsUpdate = true; surfaceMaterial.needsUpdate = true;
} }
function handleTransitionEvent(event: QuicklimeEvent<number>) {
surfaceMaterial.opacity = surfaceMaterial.userData.opacity0 * event.data;
outlineMaterial.opacity = event.data;
surfaceMaterial.transparent = surfaceMaterial.opacity < 1;
outlineMaterial.transparent = outlineMaterial.opacity < 1;
}
transitionEvent.on(handleTransitionEvent);
const unsubscribes = [ const unsubscribes = [
Tankopedia.on( Tankopedia.on(
(state) => state.highlightArmor?.name, (state) => state.highlightArmor?.name,
handleHighlightArmor handleHighlightArmor
), ),
() => transitionEvent.off(handleTransitionEvent),
]; ];
if (props.clip) { if (props.clip) {
@@ -1,3 +1,5 @@
import { Box, Text } from "@radix-ui/themes";
import { Html } from "@react-three/drei";
import { memo, type ComponentProps } from "react"; import { memo, type ComponentProps } from "react";
interface Props extends ComponentProps<"spotLight"> { interface Props extends ComponentProps<"spotLight"> {
@@ -8,10 +10,20 @@ export const HelpingSpotLight = memo<Props>(({ debug, ...props }) => {
return ( return (
<> <>
{debug && ( {debug && (
<mesh position={props.position}> <group position={props.position}>
<icosahedronGeometry args={[0.1, 2]} /> <mesh>
<meshBasicMaterial color={props.color} /> <icosahedronGeometry args={[0.1, 2]} />
</mesh> <meshBasicMaterial color={props.color} />
</mesh>
<Html center>
<Box pb="9">
<Text wrap="nowrap">
{props.intensity} @ {JSON.stringify(props.userData)}
</Text>
</Box>
</Html>
</group>
)} )}
<spotLight {...props} /> <spotLight {...props} />
@@ -12,6 +12,7 @@ import {
EyeOpenIcon, EyeOpenIcon,
ShadowInnerIcon, ShadowInnerIcon,
ShadowNoneIcon, ShadowNoneIcon,
SunIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { import {
Button, Button,
@@ -55,7 +56,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const hasCustomShell = Tankopedia.use( const hasCustomShell = Tankopedia.use(
(state) => state.customShell !== undefined (state) => state.customShell !== undefined
); );
const display = Tankopedia.use((state) => state.display); const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const advancedHighlighting = TankopediaPersistent.use( const advancedHighlighting = TankopediaPersistent.use(
(state) => state.advancedHighlighting (state) => state.advancedHighlighting
@@ -73,6 +74,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
const { strings, unwrap } = useLocale(); const { strings, unwrap } = useLocale();
const revealed = Tankopedia.use((state) => state.revealed); const revealed = Tankopedia.use((state) => state.revealed);
const disturbed = Tankopedia.use((state) => state.disturbed); const disturbed = Tankopedia.use((state) => state.disturbed);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
return ( return (
<> <>
@@ -84,7 +86,9 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
gap="2" gap="2"
direction="column" direction="column"
top="50%" top="50%"
right={display === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"} right={
requestedDisplay === TankopediaDisplay.DynamicArmor ? "3" : "-4rem"
}
style={{ style={{
position: "absolute", position: "absolute",
transform: "translateY(-50%)", transform: "translateY(-50%)",
@@ -296,7 +300,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
> >
{disturbed && ( {disturbed && (
<> <>
{display === TankopediaDisplay.DynamicArmor && ( {requestedDisplay === TankopediaDisplay.DynamicArmor && (
<IconButton <IconButton
size="2" size="2"
highContrast highContrast
@@ -349,6 +353,20 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
<IconButton
size="2"
highContrast
onClick={() => {
TankopediaPersistent.mutate((draft) => {
draft.highGraphics = !draft.highGraphics;
});
}}
color={highGraphics ? undefined : "gray"}
variant={highGraphics ? "solid" : "surface"}
>
<SunIcon />
</IconButton>
<ScreenshotButton <ScreenshotButton
color="gray" color="gray"
size="2" size="2"
@@ -384,11 +402,11 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
<Flex gap="3" align="center" mt="2"> <Flex gap="3" align="center" mt="2">
<SegmentedControl.Root <SegmentedControl.Root
variant="classic" variant="classic"
value={`${disturbed ? display : -1}`} value={`${disturbed ? requestedDisplay : -1}`}
onValueChange={(value) => { onValueChange={(value) => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.disturbed = true; draft.disturbed = true;
draft.display = Number(value); draft.requestedDisplay = Number(value);
}); });
}} }}
> >
@@ -430,7 +448,7 @@ export function Options({ thicknessRange, canvas, skeleton }: OptionsProps) {
</SegmentedControl.Item> </SegmentedControl.Item>
</SegmentedControl.Root> </SegmentedControl.Root>
{display === TankopediaDisplay.DynamicArmor && ( {requestedDisplay === TankopediaDisplay.DynamicArmor && (
<Dialog.Root <Dialog.Root
open={antagonistSelectorOpen} open={antagonistSelectorOpen}
onOpenChange={setAntagonistSelectorOpen} onOpenChange={setAntagonistSelectorOpen}
@@ -1,40 +0,0 @@
import { invalidate, useThree } from "@react-three/fiber";
import { useEffect } from "react";
import type { Fog } from "three";
import { fogAnimationTime, fogFar0, fogFar1, forNear0, forNear1 } from "..";
import { Tankopedia } from "../../../../../../stores/tankopedia";
export function InitialFogReveal() {
const fog = useThree((state) => state.scene.fog as Fog | null);
useEffect(() => {
Tankopedia.mutate((draft) => {
draft.revealed = true;
});
if (!fog) return;
const t0 = Date.now();
const interval = setInterval(() => {
const t = (Date.now() - t0) / 1000;
const x = t / fogAnimationTime;
const near = forNear1 + (forNear0 - forNear1) * x;
const far = fogFar1 + (fogFar0 - fogFar1) * x;
fog.near = near;
fog.far = far;
invalidate();
if (x >= 1) {
fog.near = forNear0;
fog.far = fogFar0;
clearInterval(interval);
}
}, 1000 / 60);
return () => clearInterval(interval);
}, []);
return null;
}
@@ -1,82 +1,118 @@
import { useHelper } from "@react-three/drei"; import { useFrame, useThree } from "@react-three/fiber";
import { useFrame } from "@react-three/fiber";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { useRef, useState } from "react"; import { Quicklime } from "quicklime";
import { Color, SpotLight, SpotLightHelper } from "three"; import { useEffect, useRef } from "react";
import { degToRad } from "three/src/math/MathUtils.js"; import { HemisphereLight, SpotLight, type Group } from "three";
import { isHalloween } from "../../../../../../core/blitzkit/isHalloween"; import { clamp, degToRad, lerp } from "three/src/math/MathUtils.js";
import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { HelpingSpotLight } from "../../../../../HelpingSpotLight"; import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
const ANGLE = Math.PI * 2 ** -2;
const REVEAL_ANIMATION_TIME = 3;
const TRANSITION_ANIMATION_TIME = 0.5;
const LIGHTS_COUNT = 5; const LIGHTS_COUNT = 5;
const THETA_OFFSET = degToRad(-152);
const LIGHT_DISTANCE = 13; const LIGHT_DISTANCE = 13;
const LIGHT_HEIGHT_0 = 4; const LIGHT_HEIGHT_0 = 4;
const LIGHT_HEIGHT_1 = 2; const LIGHT_HEIGHT_1 = 2;
const INTENSITY = 50; const INTENSITY_0 = 3.5;
const INTENSITY_1 = 1.5;
const HEMISPHERE_INTENSITY = 2;
const N = 128; export const transitionEvent = new Quicklime<number>(0);
const TARGET_FRAME_RATE = 30;
export function Lighting() { export function Lighting() {
const [cast, setCast] = useState(true); const wrapper = useRef<Group>(null!);
const t0 = useRef(0); const t0 = useRef(0);
const topLight = useRef<SpotLight>(null!); const transition = useRef(false);
const frontLight = useRef<SpotLight>(null!); const clock = useThree((state) => state.clock);
const display = Tankopedia.use((state) => state.display);
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
const animationTime = useRef(REVEAL_ANIMATION_TIME);
const highGraphics = TankopediaPersistent.use((state) => state.highGraphics);
useHelper(topLight, SpotLightHelper, "red"); useEffect(() => {
useHelper(frontLight, SpotLightHelper, "green"); Tankopedia.mutate((draft) => {
draft.revealed = true;
});
const last = useRef<number>(0); t0.current = clock.elapsedTime - REVEAL_ANIMATION_TIME;
const samples = useRef<number[]>([]); }, []);
useFrame(({ clock, frameloop }) => { useFrame(({ invalidate }) => {
if (frameloop === "never") { if (transition.current) {
transition.current = false;
t0.current = clock.elapsedTime; t0.current = clock.elapsedTime;
samples.current = [];
return;
}
if (!cast || clock.elapsedTime - t0.current < TARGET_FRAME_RATE * N) {
return;
} }
const dt = clock.elapsedTime - last.current; const x = clamp(
last.current = clock.elapsedTime; (clock.elapsedTime - t0.current) / animationTime.current,
0,
2
);
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
const fps = 1 / dt; if (t !== transitionEvent.last) {
samples.current.push(fps); transitionEvent.dispatch(t);
}
if (samples.current.length > N) samples.current.shift(); for (const child of wrapper.current.children) {
if (child instanceof SpotLight) {
child.angle = ANGLE * t;
} else if (child instanceof HemisphereLight) {
child.intensity = HEMISPHERE_INTENSITY * t;
}
}
const avg = if (x < 2) invalidate();
samples.current.reduce((a, b) => a + b, 0) / samples.current.length;
if (avg < TARGET_FRAME_RATE) setCast(false); if (
x > 1 &&
Tankopedia.state.display !== Tankopedia.state.requestedDisplay
) {
Tankopedia.mutate((draft) => {
draft.display = requestedDisplay;
});
}
}); });
useEffect(() => {
if (requestedDisplay === display) return;
transition.current = true;
animationTime.current = TRANSITION_ANIMATION_TIME;
}, [requestedDisplay]);
return ( return (
<> <group ref={wrapper}>
<hemisphereLight intensity={0} color="#ffffff" groundColor="#afafaf" />
{times(LIGHTS_COUNT, (index) => { {times(LIGHTS_COUNT, (index) => {
const x = index / (LIGHTS_COUNT - 1); const x = index / (LIGHTS_COUNT - 1);
const theta = 2 * Math.PI * (index / LIGHTS_COUNT) - degToRad(152); const theta = 2 * Math.PI * (index / LIGHTS_COUNT) + THETA_OFFSET;
const position = [ const position = [
LIGHT_DISTANCE * Math.sin(theta), LIGHT_DISTANCE * Math.sin(theta),
LIGHT_HEIGHT_0 * (1 - x) + LIGHT_HEIGHT_1 * x, lerp(LIGHT_HEIGHT_0, LIGHT_HEIGHT_1, x),
LIGHT_DISTANCE * Math.cos(theta), LIGHT_DISTANCE * Math.cos(theta),
] as const; ] as const;
const color = new Color(); const intensity = lerp(INTENSITY_0, INTENSITY_1, x);
return ( return (
<HelpingSpotLight <HelpingSpotLight
userData={{ index }}
key={index} key={index}
position={position} position={position}
intensity={INTENSITY} intensity={intensity}
penumbra={1} penumbra={1}
castShadow={cast} castShadow={highGraphics}
decay={1} decay={0}
color={color} color="#ffffff"
angle={0}
/> />
); );
})} })}
</> </group>
); );
} }
@@ -4,7 +4,7 @@ import { Quicklime, type QuicklimeEvent } from "quicklime";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Mesh, MeshStandardMaterial } from "three"; import { Mesh, MeshStandardMaterial } from "three";
const SIZE = 2 ** 5; const SIZE = 2 ** 6;
export const screenshotReadyEvent = new Quicklime(false); export const screenshotReadyEvent = new Quicklime(false);
@@ -36,6 +36,7 @@ export function SceneProps() {
receiveShadow receiveShadow
visible={!screenshotReadyEvent.last!} visible={!screenshotReadyEvent.last!}
ref={mesh} ref={mesh}
renderOrder={-1}
> >
<planeGeometry args={[SIZE, SIZE]} /> <planeGeometry args={[SIZE, SIZE]} />
<meshStandardMaterial <meshStandardMaterial
@@ -43,7 +44,7 @@ export function SceneProps() {
ref={material} ref={material}
color="black" color="black"
roughness={1} roughness={1}
metalness={1} metalness={0}
/> />
</mesh> </mesh>
); );
@@ -106,7 +106,9 @@ export function TankModel() {
const isTurret = node.name.startsWith("turret_"); const isTurret = node.name.startsWith("turret_");
const isCurrentTurret = const isCurrentTurret =
node.name === node.name ===
`turret_${turretModelDefinition.model_id.toString().padStart(2, "0")}`; `turret_${turretModelDefinition.model_id
.toString()
.padStart(2, "0")}`;
const isVisible = isCurrentTurret; const isVisible = isCurrentTurret;
const position = new Vector2(); const position = new Vector2();
const delta = new Vector2(); const delta = new Vector2();
@@ -1,12 +1,10 @@
import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core"; import { BLITZKIT_TANK_ICON_SIZE } from "@blitzkit/core";
import { invalidate } from "@react-three/fiber";
import { import {
forwardRef, forwardRef,
Suspense, Suspense,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useRef, useRef,
useState,
} from "react"; } from "react";
import { Fog } from "three"; import { Fog } from "three";
import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits"; import { applyPitchYawLimits } from "../../../../../core/blitz/applyPitchYawLimits";
@@ -29,26 +27,17 @@ import { SmartCanvas } from "../../../../SmartCanvas";
import { AutoClear } from "./components/AutoClear"; import { AutoClear } from "./components/AutoClear";
import { Controls } from "./components/Control"; import { Controls } from "./components/Control";
import { InitialAligner } from "./components/InitialAligner"; import { InitialAligner } from "./components/InitialAligner";
import { InitialFogReveal } from "./components/InitialFogReveal";
import { Lighting } from "./components/Lighting"; import { Lighting } from "./components/Lighting";
import { SceneProps } from "./components/SceneProps"; import { SceneProps } from "./components/SceneProps";
import { TankModel } from "./components/TankModel"; import { TankModel } from "./components/TankModel";
import { TransitionSkeleton } from "./components/TransitionSkeleton";
interface TankSandboxProps { interface TankSandboxProps {
thicknessRange: ThicknessRange; thicknessRange: ThicknessRange;
naked?: boolean; naked?: boolean;
} }
export const forNear0 = 30;
export const fogFar0 = 50;
export const forNear1 = 0;
export const fogFar1 = 0;
export const fogAnimationTime = 2.5;
export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>( export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
({ thicknessRange, naked }, ref) => { ({ thicknessRange, naked }, ref) => {
const fog = useRef(new Fog("black", forNear1, fogFar1));
const canvas = useRef<HTMLCanvasElement>(null); const canvas = useRef<HTMLCanvasElement>(null);
const hasImprovedVerticalStabilizer = useEquipment(122); const hasImprovedVerticalStabilizer = useEquipment(122);
const hasDownImprovedVerticalStabilizer = useEquipment(124); const hasDownImprovedVerticalStabilizer = useEquipment(124);
@@ -58,40 +47,11 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
const turretModelDefinition = const turretModelDefinition =
tankModelDefinition.turrets[protagonistTurret.id]; tankModelDefinition.turrets[protagonistTurret.id];
const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id]; const gunModelDefinition = turretModelDefinition.guns[protagonistGun.id];
const rawDisplay = Tankopedia.use((state) => state.display); const display = Tankopedia.use((state) => state.display);
const [display, setDisplay] = useState(rawDisplay);
const hideTankModelUnderArmor = TankopediaPersistent.use( const hideTankModelUnderArmor = TankopediaPersistent.use(
(state) => state.hideTankModelUnderArmor (state) => state.hideTankModelUnderArmor
); );
useEffect(() => {
if (rawDisplay === display) return;
const t0 = Date.now();
const interval = setInterval(() => {
const t = (Date.now() - t0) / 1000;
const x = t / fogAnimationTime;
const y = Math.sin(Math.PI * x);
const near = forNear0 + (forNear1 - forNear0) * y;
const far = fogFar0 + (fogFar1 - fogFar0) * y;
fog.current.near = near;
fog.current.far = far;
invalidate();
if (rawDisplay !== display && x >= 0.5) setDisplay(rawDisplay);
if (x >= 1) {
fog.current.near = forNear0;
fog.current.far = fogFar0;
clearInterval(interval);
}
}, 1000 / 60);
return () => clearInterval(interval);
}, [rawDisplay]);
useImperativeHandle(ref, () => canvas.current!, []); useImperativeHandle(ref, () => canvas.current!, []);
function handlePointerDown() { function handlePointerDown() {
@@ -188,10 +148,12 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
} }
}, [display]); }, [display]);
console.log(TankopediaDisplay[display]);
return ( return (
<SmartCanvas <SmartCanvas
ref={canvas} ref={canvas}
scene={{ fog: naked ? undefined : fog.current }} scene={{ fog: naked ? undefined : new Fog("black", 30, 50) }}
gl={{ gl={{
clippingPlanes: [], clippingPlanes: [],
localClippingEnabled: true, localClippingEnabled: true,
@@ -212,11 +174,12 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
outline: naked ? "1rem red solid" : undefined, outline: naked ? "1rem red solid" : undefined,
}} }}
> >
<Lighting />
{!naked && <SceneProps />} {!naked && <SceneProps />}
{(display === TankopediaDisplay.Model || {(display === TankopediaDisplay.Model ||
(display === TankopediaDisplay.DynamicArmor && (display === TankopediaDisplay.DynamicArmor &&
!hideTankModelUnderArmor)) && <TankModel />} !hideTankModelUnderArmor)) && <TankModel />}
<TransitionSkeleton />
<ShotDisplay /> <ShotDisplay />
<ArmorPlateDisplay /> <ArmorPlateDisplay />
<AutoClear /> <AutoClear />
@@ -224,13 +187,11 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
<Suspense> <Suspense>
{/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */} {/* Controls within Suspense to allow for frame-perfect start of camera auto-rotate */}
<Controls naked={naked} /> <Controls naked={naked} />
<InitialFogReveal />
{display === TankopediaDisplay.DynamicArmor && <Armor />} {display === TankopediaDisplay.DynamicArmor && <Armor />}
{display === TankopediaDisplay.StaticArmor && ( {display === TankopediaDisplay.StaticArmor && (
<StaticArmor thicknessRange={thicknessRange} /> <StaticArmor thicknessRange={thicknessRange} />
)} )}
<Lighting />
</Suspense> </Suspense>
<InitialAligner /> <InitialAligner />
@@ -1,6 +1,6 @@
import { Box, Flex } from "@radix-ui/themes"; import { Box, Flex } from "@radix-ui/themes";
import { times } from "lodash-es"; import { times } from "lodash-es";
import { Suspense, useEffect, useMemo, useRef } from "react"; import { useEffect, useMemo, useRef } from "react";
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions"; import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
import { useFullScreen } from "../../../hooks/useFullScreen"; import { useFullScreen } from "../../../hooks/useFullScreen";
import { Duel } from "../../../stores/duel"; import { Duel } from "../../../stores/duel";
@@ -99,11 +99,11 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
isFullScreen isFullScreen
? "100vh" ? "100vh"
: disturbed : disturbed
? "calc(100svh - 8rem)" ? "calc(100svh - 8rem)"
: { : {
initial: "28rem", initial: "28rem",
md: "calc(100svh - 14rem)", md: "calc(100svh - 14rem)",
} }
} }
maxWidth={isFullScreen ? undefined : "120rem"} maxWidth={isFullScreen ? undefined : "120rem"}
flexGrow="1" flexGrow="1"
@@ -120,30 +120,17 @@ export function HeroSection({ skeleton }: MaybeSkeletonComponentProps) {
position="relative" position="relative"
> >
<Box position="absolute" width="100%" height="100%" overflow="hidden"> <Box position="absolute" width="100%" height="100%" overflow="hidden">
<Box <Box width="100%" height="100%" position="relative">
width="100%"
height="100%"
position="relative"
style={{
transitionDuration: "1s",
}}
>
<Box <Box
style={{ position="absolute"
width: "100%", width="100%"
height: "100%", height="100%"
transitionDuration: "2s", top="0"
opacity: revealed ? 1 : 0, left="0"
filter: disturbed
? undefined
: `drop-shadow(0 1rem 1rem black)`,
}}
> >
<Suspense> {!skeleton && (
{!skeleton && ( <TankSandbox ref={canvas} thicknessRange={thicknessRange} />
<TankSandbox ref={canvas} thicknessRange={thicknessRange} /> )}
)}
</Suspense>
</Box> </Box>
</Box> </Box>
@@ -100,6 +100,7 @@ interface Tankopedia {
); );
xpMultiplier: (typeof XP_MULTIPLIERS)[number]; xpMultiplier: (typeof XP_MULTIPLIERS)[number];
customShell?: ShellDefinition; customShell?: ShellDefinition;
requestedDisplay: TankopediaDisplay;
display: TankopediaDisplay; display: TankopediaDisplay;
statSearch?: string; statSearch?: string;
} }
@@ -114,5 +115,6 @@ export const Tankopedia = new Varuna<Tankopedia, ModelDefinition>((model) => ({
skills: createDefaultSkills(skillDefinitions), skills: createDefaultSkills(skillDefinitions),
model, model,
xpMultiplier: 1, xpMultiplier: 1,
requestedDisplay: TankopediaDisplay.Model,
display: TankopediaDisplay.Model, display: TankopediaDisplay.Model,
})); }));
@@ -15,6 +15,7 @@ export interface TankopediaPersistent {
showPrimaryArmor: boolean; showPrimaryArmor: boolean;
recentlyViewed: number[]; recentlyViewed: number[];
hideTankModelUnderArmor: boolean; hideTankModelUnderArmor: boolean;
highGraphics: boolean;
sort: { sort: {
by: TankopediaSortBy; by: TankopediaSortBy;
@@ -33,6 +34,7 @@ export const TankopediaPersistent = new Varuna<TankopediaPersistent>(
showExternalModules: true, showExternalModules: true,
showPrimaryArmor: true, showPrimaryArmor: true,
recentlyViewed: [], recentlyViewed: [],
highGraphics: true,
sort: { sort: {
by: "meta.none", by: "meta.none",
direction: "ascending", direction: "ascending",