Compare commits

...
19 Commits
Author SHA1 Message Date
tresabhi cb2d795461 fix url 2026-07-11 18:14:22 -05:00
tresabhi 6e934e0ff1 logs 2026-07-11 18:13:02 -05:00
tresabhi 375adadbe5 Merge branch 'main' into dev 2026-07-11 18:04:36 -05:00
tresabhi 082a756f1b auto scroll on drag in tier list 2026-07-11 18:02:49 -05:00
tresabhi 9fc8a8e2a2 more generous pitch transition zones 2026-07-11 17:41:14 -05:00
tresabhi 1413475679 bigger floor and more aggressive light decay 2026-07-11 17:38:11 -05:00
tresabhi 86504e685e softer shadows 2026-07-11 17:33:37 -05:00
tresabhi 9df81044d1 increasing lighting angle to cover larger tanks 2026-07-11 17:29:44 -05:00
tresabhi c5fd7591ea complete lighting animation even if canvas is unfocused 2026-07-11 17:29:15 -05:00
TrèsAbhi cd8e43f945 Merge pull request #256 from Hitjack007/mixer-crashing 2026-07-11 17:17:11 -05:00
tresabhi 8df112aab4 type-safe disposal 2026-07-11 17:16:52 -05:00
TrèsAbhi 94c04ce102 Merge pull request #255 from Hitjack007/external-ricochets 2026-07-11 17:14:10 -05:00
TrèsAbhi 3a170202ca Merge pull request #254 from Hitjack007/he-crash-fix 2026-07-11 17:10:49 -05:00
tresabhi 46937693a0 implement nullable refs in a type-safe manner 2026-07-11 17:09:38 -05:00
TrèsAbhi b391d0b036 Merge pull request #253 from Hitjack007/static-armor-uncapitalised-fix 2026-07-11 17:06:29 -05:00
Mark Greene 1061e003f7 Fix Safari crash on mixer texture disposal 2026-07-10 21:30:21 +12:00
Mark Greene a2b573abca fix ricochets on external modules 2026-07-10 17:51:32 +12:00
Mark Greene 4571d141e6 Fix crash on repeated HE shell clicks in Tankopedia armor viewer 2026-07-10 16:03:05 +12:00
Mark Greene d70225b03c fix static armor plate title to use localized strings
Was rendering the raw lowercase layerTypeNames key instead of resolving it through strings.website.tools.tankopedia.sandbox.dynamic.shot_card.element, same pattern already used in ShotDisplayCard.
2026-07-10 15:41:46 +12:00
11 changed files with 143 additions and 65 deletions

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
# BlitzKit 1.30.7
Thank you to [Mark Greene](https://github.com/Hitjack007) for your contributions this update.
## Features
- Dragging items in the Tier List now automatically scrolls the screen if you reach the top/bottom edge of the screen
## Fixes
- [Mark Greene] Clicking on the armor multiple times with HE no longer crashes BlitzKit
- [Mark Greene] Ricochets that later intersect with external modules no longer falsely claim a successful damaging penetration
- The lighting animation no longer stops prematurely when BlitzKit is unfocused
- The spot light angle has been increased to more evenly light up larger tanks
- BlitzKit once again uses soft shadows, reducing the pixelated shadows
- Fixed the weird patterns that formed on the tracks of tanks under self-shadows
- Increased the floor size and light decay to create the illusion that the floor is infinite
- [Mark Greene] Safari no longer crashes when unloading textures for unloaded tanks
- The gun depression transition between two constraints (like often behind tanks) now uses a gentler zoned and can never be 0
- [Mark Greene] Armor plate names in Static Armor are now properly capitalized in English and translated to other languages
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "blitzkit", "name": "blitzkit",
"version": "1.30.6", "version": "1.30.7",
"license": "BlitzKit License", "license": "BlitzKit License",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -22,7 +22,7 @@ export function ArmorPlateDisplay() {
const highlightArmor = Tankopedia.use((state) => state.highlightArmor); const highlightArmor = Tankopedia.use((state) => state.highlightArmor);
const developerMode = App.useDeferred((state) => state.developerMode, false); const developerMode = App.useDeferred((state) => state.developerMode, false);
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const { locale } = useLocale(); const { locale, strings } = useLocale();
if (highlightArmor === undefined) return null; if (highlightArmor === undefined) return null;
@@ -44,7 +44,10 @@ export function ArmorPlateDisplay() {
> >
<Flex direction="column"> <Flex direction="column">
<Text weight="bold"> <Text weight="bold">
{layerTypeNames[highlightArmor.type]}{" "} {
strings.website.tools.tankopedia.sandbox.dynamic.shot_card
.element[layerTypeNames[highlightArmor.type]]
}{" "}
{highlightArmor.thickness.toFixed(0)} {highlightArmor.thickness.toFixed(0)}
<Text size="1" weight="regular"> <Text size="1" weight="regular">
mm mm
@@ -42,10 +42,10 @@ const TRACER_THIN = TRACER_THICK / 2;
export function ShotDisplay() { export function ShotDisplay() {
const shot = Tankopedia.use((state) => state.shot); const shot = Tankopedia.use((state) => state.shot);
const inTracer = useRef<Mesh>(null!); const inTracer = useRef<Mesh>(null);
const outTracer = useRef<Mesh>(null!); const outTracer = useRef<Mesh>(null);
const splashRadiusWrapper = useRef<Group>(null!); const splashRadiusWrapper = useRef<Group>(null);
const splashRadiusMaterial = useRef<MeshBasicMaterial>(null!); const splashRadiusMaterial = useRef<MeshBasicMaterial>(null);
const { locale } = useLocale(); const { locale } = useLocale();
useEffect(() => { useEffect(() => {
@@ -63,10 +63,10 @@ export function ShotDisplay() {
outlineMaterial, outlineMaterial,
).computeLineDistances(); ).computeLineDistances();
splashRadiusWrapper.current.add(outline); splashRadiusWrapper.current?.add(outline);
return () => { return () => {
splashRadiusWrapper.current.remove(outline); splashRadiusWrapper.current?.remove(outline);
}; };
} }
}); });
@@ -255,11 +255,11 @@ export function ShotDisplay() {
} }
interface AnimatorProps { interface AnimatorProps {
inTracer: RefObject<Mesh>; inTracer: RefObject<Mesh | null>;
outTracer: RefObject<Mesh>; outTracer: RefObject<Mesh | null>;
splashRadiusWrapper: RefObject<Group>; splashRadiusWrapper: RefObject<Group | null>;
splashRadiusMaterial: RefObject<MeshBasicMaterial>; splashRadiusMaterial: RefObject<MeshBasicMaterial | null>;
inLength: number; inLength: number;
outLength: number; outLength: number;
@@ -283,17 +283,23 @@ function Animator({
const tracerT1 = t % 1; const tracerT1 = t % 1;
const tracerT2 = (t + 0.5) % 1; const tracerT2 = (t + 0.5) % 1;
inTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT1 - 0.5), 1); if (inTracer.current) {
inTracer.current.position.set(0, (1 - tracerT1) * inLength, 0); inTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT1 - 0.5), 1);
inTracer.current.position.set(0, (1 - tracerT1) * inLength, 0);
if (outTracer.current) { if (outTracer.current) {
outTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT2 - 0.5), 1); outTracer.current.scale.set(1, 1 - 2 * Math.abs(tracerT2 - 0.5), 1);
outTracer.current.position.set(0, tracerT2 * outLength, 0); outTracer.current.position.set(0, tracerT2 * outLength, 0);
}
invalidate();
} }
invalidate(); if (
shot.splashRadius !== undefined &&
if (shot.splashRadius !== undefined) { splashRadiusWrapper.current &&
splashRadiusMaterial.current
) {
if (animationStartTime === null) animationStartTime = t; if (animationStartTime === null) animationStartTime = t;
const scale = const scale =
@@ -161,9 +161,10 @@ export function SpacedArmorSceneComponent({
if ( if (
!allowRicochet && !allowRicochet &&
noDuplicateIntersections.every((intersection) => { noDuplicateIntersections.every(
intersection.object.userData.type !== ArmorType.Primary; (intersection) =>
}) intersection.object.userData.type !== ArmorType.Primary,
)
) { ) {
return null; return null;
} }
@@ -1,14 +1,20 @@
import { useFrame } from "@react-three/fiber"; import { useFrame } from "@react-three/fiber";
import { clamp, times } from "lodash-es"; import { clamp, times } from "lodash-es";
import { Quicklime } from "quicklime"; import { Quicklime } from "quicklime";
import { useEffect, useRef, useState, type RefObject } from "react"; import {
useCallback,
useEffect,
useRef,
useState,
type RefObject,
} from "react";
import { HemisphereLight, SpotLight, type Group } from "three"; import { HemisphereLight, SpotLight, type Group } from "three";
import { degToRad, lerp } from "three/src/math/MathUtils.js"; import { degToRad, lerp } from "three/src/math/MathUtils.js";
import { Tankopedia } from "../../../../../../stores/tankopedia"; import { Tankopedia } from "../../../../../../stores/tankopedia";
import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent"; import { TankopediaPersistent } from "../../../../../../stores/tankopediaPersistent";
import { HelpingSpotLight } from "../../../../../HelpingSpotLight"; import { HelpingSpotLight } from "../../../../../HelpingSpotLight";
const ANGLE = degToRad(10); const ANGLE = degToRad(12);
const REVEAL_ANIMATION_TIME = 3; const REVEAL_ANIMATION_TIME = 3;
const TRANSITION_ANIMATION_TIME = 0.5; const TRANSITION_ANIMATION_TIME = 0.5;
@@ -17,8 +23,8 @@ const THETA_OFFSET = degToRad(180 - 45);
const LIGHT_DISTANCE = 20; const LIGHT_DISTANCE = 20;
const LIGHT_HEIGHT_0 = 4; const LIGHT_HEIGHT_0 = 4;
const LIGHT_HEIGHT_1 = 6; const LIGHT_HEIGHT_1 = 6;
const INTENSITY_0 = 6; const INTENSITY_0 = 6 * 9;
const INTENSITY_1 = 3; const INTENSITY_1 = 3 * 9;
const HEMISPHERE_INTENSITY = 2; const HEMISPHERE_INTENSITY = 2;
export const transitionEvent = new Quicklime<number>(0); export const transitionEvent = new Quicklime<number>(0);
@@ -36,8 +42,6 @@ export function Lighting() {
const [animate, setAnimate] = useState(true); const [animate, setAnimate] = useState(true);
console.log(animate);
useEffect(() => { useEffect(() => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
draft.revealed = true; draft.revealed = true;
@@ -89,7 +93,7 @@ export function Lighting() {
intensity={intensity} intensity={intensity}
penumbra={1} penumbra={1}
castShadow={highGraphics} castShadow={highGraphics}
decay={0} decay={1}
color="#ffffff" color="#ffffff"
angle={0} angle={0}
/> />
@@ -111,17 +115,7 @@ interface AnimatorProps {
function Animator({ stop, t0, animationTime, wrapper }: AnimatorProps) { function Animator({ stop, t0, animationTime, wrapper }: AnimatorProps) {
const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay); const requestedDisplay = Tankopedia.use((state) => state.requestedDisplay);
useFrame(({ invalidate }) => { const apply = useCallback((t: number) => {
const dt = performance.now() / 1e3 - t0.current;
const x = clamp(dt / animationTime.current, 0, 2);
if (x === 2) {
stop();
return;
}
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
for (const child of wrapper.current.children) { for (const child of wrapper.current.children) {
if (child instanceof SpotLight) { if (child instanceof SpotLight) {
child.angle = ANGLE * t; child.angle = ANGLE * t;
@@ -131,6 +125,22 @@ function Animator({ stop, t0, animationTime, wrapper }: AnimatorProps) {
} }
transitionEvent.dispatch(t); transitionEvent.dispatch(t);
}, []);
useFrame(({ invalidate }) => {
const dt = performance.now() / 1e3 - t0.current;
const x = clamp(dt / animationTime.current, 0, 2);
if (x === 2) {
apply(1);
stop();
return;
}
// https://www.desmos.com/calculator/awsbahxjku
const t = (0.5 * Math.sin(Math.PI * (x + 0.5)) + 0.5) ** 2;
apply(t);
invalidate(); invalidate();
if (x >= 1 && Tankopedia.state.display !== requestedDisplay) { if (x >= 1 && Tankopedia.state.display !== requestedDisplay) {
@@ -3,7 +3,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 = 14; const SIZE = 2 ** 4;
export const screenshotReadyEvent = new Quicklime(false); export const screenshotReadyEvent = new Quicklime(false);
@@ -156,7 +156,7 @@ export const TankSandbox = forwardRef<HTMLCanvasElement, TankSandboxProps>(
localClippingEnabled: true, localClippingEnabled: true,
preserveDrawingBuffer: true, preserveDrawingBuffer: true,
}} }}
shadows="basic" shadows="soft"
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
onPointerMissed={() => { onPointerMissed={() => {
Tankopedia.mutate((draft) => { Tankopedia.mutate((draft) => {
@@ -1,14 +1,16 @@
import type { TankDefinition } from '@blitzkit/core'; import type { TankDefinition } from "@blitzkit/core";
import { Box } from '@radix-ui/themes'; import { Box } from "@radix-ui/themes";
import { import {
useCallback, useCallback,
useEffect, useEffect,
useRef, useRef,
type PointerEvent as ReactPointerEvent, type PointerEvent as ReactPointerEvent,
} from 'react'; } from "react";
import { TierList } from '../../stores/tierList'; import { TierList } from "../../stores/tierList";
import { TankCard } from '../TankCard'; import { TankCard } from "../TankCard";
import { tierListCardElements, tierListRowElements } from './Table/constants'; import { tierListCardElements, tierListRowElements } from "./Table/constants";
const SCROLL_ZONE = 1 / 3;
type TierListTileProps = { type TierListTileProps = {
tank: TankDefinition; tank: TankDefinition;
@@ -26,6 +28,33 @@ type TierListTileProps = {
export function TierListTile(props: TierListTileProps) { export function TierListTile(props: TierListTileProps) {
const card = useRef<HTMLDivElement>(null); const card = useRef<HTMLDivElement>(null);
const lastPosition = useRef({ x: 0, y: 0 }); const lastPosition = useRef({ x: 0, y: 0 });
const autoScroll = useRef(false);
const lastT = useRef(0);
const autoScrollLoop = useCallback(() => {
if (!autoScroll.current) return;
let scrollSpeed = 0;
const v = lastPosition.current.y / window.innerHeight;
if (v <= SCROLL_ZONE) {
scrollSpeed = v / SCROLL_ZONE - 1;
} else if (v >= 1 - SCROLL_ZONE) {
scrollSpeed = 1 - (1 - v) / SCROLL_ZONE;
}
const t = performance.now();
const dt = t - lastT.current;
lastT.current = t;
window.scrollBy({
behavior: "instant",
top: scrollSpeed * dt,
});
requestAnimationFrame(autoScrollLoop);
}, []);
const handlePointerDown = useCallback( const handlePointerDown = useCallback(
(event: ReactPointerEvent<HTMLSpanElement>) => { (event: ReactPointerEvent<HTMLSpanElement>) => {
if (!card.current) return; if (!card.current) return;
@@ -34,22 +63,26 @@ export function TierListTile(props: TierListTileProps) {
const rect = card.current.getBoundingClientRect(); const rect = card.current.getBoundingClientRect();
card.current.style.position = 'fixed'; card.current.style.position = "fixed";
card.current.style.cursor = 'grabbing'; card.current.style.cursor = "grabbing";
card.current.style.zIndex = '1'; card.current.style.zIndex = "1";
card.current.style.left = `${rect.left}px`; card.current.style.left = `${rect.left}px`;
card.current.style.top = `${rect.top}px`; card.current.style.top = `${rect.top}px`;
lastPosition.current = { x: event.clientX, y: event.clientY }; lastPosition.current = { x: event.clientX, y: event.clientY };
window.scrollTo({ behavior: 'smooth', top: 124 }); window.scrollTo({ behavior: "instant", top: 124 });
window.addEventListener('pointermove', handlePointerMove); window.addEventListener("pointermove", handlePointerMove);
window.addEventListener('pointerup', handlePointerUp); window.addEventListener("pointerup", handlePointerUp);
TierList.mutate((draft) => { TierList.mutate((draft) => {
draft.dragging = true; draft.dragging = true;
}); });
autoScroll.current = true;
lastT.current = performance.now();
autoScrollLoop();
}, },
[], [],
); );
@@ -141,12 +174,14 @@ export function TierListTile(props: TierListTileProps) {
}); });
} }
card.current.style.position = 'static'; card.current.style.position = "static";
card.current.style.zIndex = 'unset'; card.current.style.zIndex = "unset";
card.current.style.cursor = 'grab'; card.current.style.cursor = "grab";
window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener("pointermove", handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener("pointerup", handlePointerUp);
autoScroll.current = false;
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -168,7 +203,7 @@ export function TierListTile(props: TierListTileProps) {
data-tile-index={props.isPlaced ? props.tileIndex : undefined} data-tile-index={props.isPlaced ? props.tileIndex : undefined}
data-tile-id={props.tank.id} data-tile-id={props.tank.id}
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
style={{ cursor: 'grab', touchAction: 'none' }} style={{ cursor: "grab", touchAction: "none" }}
> >
<TankCard noLink tank={props.tank} /> <TankCard noLink tank={props.tank} />
</Box> </Box>
@@ -6,7 +6,7 @@ import {
import { clamp } from "lodash-es"; import { clamp } from "lodash-es";
import { degToRad } from "three/src/math/MathUtils.js"; import { degToRad } from "three/src/math/MathUtils.js";
export const DEFAULT_PITCH_TRANSITION = 15; export const DEFAULT_PITCH_TRANSITION = 20;
export function applyPitchYawLimits( export function applyPitchYawLimits(
pitch: number, // rad pitch: number, // rad
@@ -31,7 +31,7 @@ export function applyPitchYawLimits(
let lowerPitch = -degToRad(pitchLimits.max) - depressionBuff; let lowerPitch = -degToRad(pitchLimits.max) - depressionBuff;
let upperPitch = -degToRad(pitchLimits.min) + elevationBuff; let upperPitch = -degToRad(pitchLimits.min) + elevationBuff;
const transition = degToRad( const transition = degToRad(
pitchLimits.transition ?? DEFAULT_PITCH_TRANSITION, pitchLimits.transition || DEFAULT_PITCH_TRANSITION,
); );
if (pitchLimits.back) { if (pitchLimits.back) {
+4 -1
View File
@@ -36,7 +36,10 @@ export function useDispose(gltf: GLTF & ObjectMap, path: string) {
for (const map of maps) { for (const map of maps) {
if (!map) continue; if (!map) continue;
map.source.data.close(); if (map.source.data instanceof ImageBitmap) {
map.source.data.close();
}
map.dispose(); map.dispose();
} }