mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
9 files changed
+144
-31
No files matched your search
@@ -28,7 +28,7 @@ jobs:
|
|||||||
run: bun run build:bot
|
run: bun run build:bot
|
||||||
|
|
||||||
- name: Archive build
|
- name: Archive build
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: bot
|
name: bot
|
||||||
path: dist/bot
|
path: dist/bot
|
||||||
@@ -46,14 +46,14 @@ jobs:
|
|||||||
- uses: arduino/setup-protoc@v3
|
- uses: arduino/setup-protoc@v3
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.GH_TOKEN }}
|
||||||
- uses: actions/configure-pages@v5
|
- uses: actions/configure-pages@v6
|
||||||
- run: bun run setup
|
- run: bun run setup
|
||||||
|
|
||||||
- name: Build website
|
- name: Build website
|
||||||
run: bun run build:website
|
run: bun run build:website
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v4
|
uses: actions/upload-pages-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: dist/website
|
path: dist/website
|
||||||
|
|
||||||
@@ -69,4 +69,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@v5
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# BlitzKit 1.30.3
|
||||||
|
|
||||||
|
Thank you to [GonnaHetzMe](https://github.com/karelpak1) for your contributions this update.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [GonnaHetzMe] Added tooltips to the module selector in Tankopedia
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
- Added aggressive re-rendering for the Guess mini-game's score readout to make inspect-element harder
|
||||||
|
- Removed tank visual and collision model caching
|
||||||
|
- Guesser once again breaks streaks if not all tiers are selected
|
||||||
|
- BlitzKit now properly unloads tank models from memory when visually hidden in Guesser
|
||||||
|
- [GonnaHetzMe] Tanks with complex transparency like the Herdbreaker once again handle opacity correctly
|
||||||
|
- [GonnaHetzMe] Pieces of tank models that have transparency now handle culling properly (they're visible from both sides)
|
||||||
|
- [GonnaHetzMe] Tanks with "cardboard" textures have been fixed, thanks to better PVR file parsing
|
||||||
|
- [GonnaHetzMe] Tank icons are now scaled correctly so small tanks are now properly zoomed out in contrast to bigger ones
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"version": "1.30.2",
|
"version": "1.30.3",
|
||||||
"license": "BlitzKit License",
|
"license": "BlitzKit License",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { clamp, isEqual, times } from 'lodash-es';
|
import { clamp, isEqual, times } from "lodash-es";
|
||||||
import { ReadStream } from './buffer';
|
import { ReadStream } from "./buffer";
|
||||||
|
|
||||||
export enum PvrFlags {
|
export enum PvrFlags {
|
||||||
NoFlag = 0,
|
NoFlag = 0,
|
||||||
@@ -96,7 +96,7 @@ export class PvrReadStream extends ReadStream {
|
|||||||
const header = this.header();
|
const header = this.header();
|
||||||
this.metadata();
|
this.metadata();
|
||||||
|
|
||||||
if (typeof header.pixelFormat === 'number') {
|
if (typeof header.pixelFormat === "number") {
|
||||||
switch (header.pixelFormat) {
|
switch (header.pixelFormat) {
|
||||||
default:
|
default:
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
@@ -142,12 +142,12 @@ export class PvrReadStream extends ReadStream {
|
|||||||
|
|
||||||
times(pixelCount, (index) => {
|
times(pixelCount, (index) => {
|
||||||
const buffer = this.consumeUint8Array(2);
|
const buffer = this.consumeUint8Array(2);
|
||||||
const bufferIndex = index * 4;
|
const bufferIndex = index * 3;
|
||||||
const r = ((buffer[1] & 0b11111000) >>> 3) / 31;
|
const r = ((buffer[1] & 0b11111000) >>> 3) / 31;
|
||||||
const g =
|
const g =
|
||||||
((buffer[1] & 0b111) << 3) |
|
(((buffer[1] & 0b111) << 3) | ((buffer[0] & 0b11100000) >>> 5)) /
|
||||||
(((buffer[0] & 0b11100000) >>> 5) / 31);
|
63;
|
||||||
const b = buffer[0] & 0b11111;
|
const b = (buffer[0] & 0b11111) / 31;
|
||||||
|
|
||||||
data[bufferIndex] = Math.round(clamp(r * 255, 0, 255));
|
data[bufferIndex] = Math.round(clamp(r * 255, 0, 255));
|
||||||
data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255));
|
data[bufferIndex + 1] = Math.round(clamp(g * 255, 0, 255));
|
||||||
@@ -186,7 +186,7 @@ export class PvrReadStream extends ReadStream {
|
|||||||
metadataSize: this.uint32(),
|
metadataSize: this.uint32(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (header.version !== 'PVR\x03') throw new TypeError('Endian mismatch');
|
if (header.version !== "PVR\x03") throw new TypeError("Endian mismatch");
|
||||||
|
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
@@ -205,19 +205,19 @@ export class PvrReadStream extends ReadStream {
|
|||||||
resolveBitRatePixelFormat(
|
resolveBitRatePixelFormat(
|
||||||
pixelFormat: Exclude<ReturnType<typeof this.pixelFormat>, number>,
|
pixelFormat: Exclude<ReturnType<typeof this.pixelFormat>, number>,
|
||||||
) {
|
) {
|
||||||
if (pixelFormat.order === 'rgba') {
|
if (pixelFormat.order === "rgba") {
|
||||||
if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) {
|
if (isEqual(pixelFormat.bitRates, [4, 4, 4, 4])) {
|
||||||
return ResolvedBitRatePixelFormat.R4G4B4A4;
|
return ResolvedBitRatePixelFormat.R4G4B4A4;
|
||||||
} else
|
} else
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Unhandled rgba bit rate ${pixelFormat.bitRates.join(', ')}`,
|
`Unhandled rgba bit rate ${pixelFormat.bitRates.join(", ")}`,
|
||||||
);
|
);
|
||||||
} else if (pixelFormat.order === 'rgb\0') {
|
} else if (pixelFormat.order === "rgb\0") {
|
||||||
if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) {
|
if (isEqual(pixelFormat.bitRates, [5, 6, 5, 0])) {
|
||||||
return ResolvedBitRatePixelFormat.R5G6B5A0;
|
return ResolvedBitRatePixelFormat.R5G6B5A0;
|
||||||
} else
|
} else
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Unhandled rgb bit rate ${pixelFormat.bitRates.join(', ')}`,
|
`Unhandled rgb bit rate ${pixelFormat.bitRates.join(", ")}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError(`Unhandled pixel order ${pixelFormat.order}`);
|
throw new TypeError(`Unhandled pixel order ${pixelFormat.order}`);
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ type DataNode = {
|
|||||||
qualityGroup?: string;
|
qualityGroup?: string;
|
||||||
fxName?: string;
|
fxName?: string;
|
||||||
textures?: Textures;
|
textures?: Textures;
|
||||||
|
customCullMode?: number;
|
||||||
|
enabledPresets?: EnabledPresets;
|
||||||
|
properties?: Properties;
|
||||||
} & (
|
} & (
|
||||||
| ({
|
| ({
|
||||||
configCount: number;
|
configCount: number;
|
||||||
@@ -24,7 +27,7 @@ type DataNode = {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ConfigArchive {
|
export interface ConfigArchive {
|
||||||
configName: string;
|
configName: string;
|
||||||
fxName: string;
|
fxName: string;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NATION_IDS } from "@blitzkit/core";
|
import { NATION_IDS } from "@blitzkit/core";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import type { Vector3Tuple } from "three";
|
import type { Vector3Tuple } from "three";
|
||||||
|
import { parsePackedSpriteRect } from "../core/blitz/parsePackedSpriteRect";
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
import { AssetUploader } from "../core/github/assetUploader";
|
||||||
import { vfs } from "./constants";
|
import { vfs } from "./constants";
|
||||||
import type { VehicleDefinitionList } from "./definitions";
|
import type { VehicleDefinitionList } from "./definitions";
|
||||||
@@ -37,6 +38,45 @@ export interface TankParameters {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function extractPackedTankIcon(packedBuffer: Buffer, context: string) {
|
||||||
|
const texture = sharp(packedBuffer);
|
||||||
|
const spriteRect = parsePackedSpriteRect(packedBuffer);
|
||||||
|
|
||||||
|
if (!spriteRect) {
|
||||||
|
throw new Error(`Missing RIFF sprite bounds for ${context}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [left, top, width, height] = spriteRect;
|
||||||
|
const bounds = [left, top, width, height];
|
||||||
|
|
||||||
|
if (!bounds.every(Number.isInteger)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid RIFF sprite bounds for ${context}: ${bounds.join(" ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = await texture.metadata();
|
||||||
|
|
||||||
|
if (metadata.width === undefined || metadata.height === undefined) {
|
||||||
|
throw new Error(`Failed to read image dimensions for ${context}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
left < 0 ||
|
||||||
|
top < 0 ||
|
||||||
|
width <= 0 ||
|
||||||
|
height <= 0 ||
|
||||||
|
left + width > metadata.width ||
|
||||||
|
top + height > metadata.height
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Out-of-bounds RIFF sprite for ${context}: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await texture.extract({ left, top, width, height }).toBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
export async function tankIcons() {
|
export async function tankIcons() {
|
||||||
console.log("Building tank icons...");
|
console.log("Building tank icons...");
|
||||||
|
|
||||||
@@ -67,12 +107,14 @@ export async function tankIcons() {
|
|||||||
const bigPath = `Data/${parameters.resourcesPath.bigIconPath
|
const bigPath = `Data/${parameters.resourcesPath.bigIconPath
|
||||||
.replace(/~res:\//, "")
|
.replace(/~res:\//, "")
|
||||||
.replace(/\..+/, "")}.packed.webp`;
|
.replace(/\..+/, "")}.packed.webp`;
|
||||||
const big = await sharp(await vfs.file(bigPath))
|
const big = await extractPackedTankIcon(
|
||||||
.trim()
|
await vfs.file(bigPath),
|
||||||
.toBuffer();
|
`tank ${tankKey} big`,
|
||||||
const small = await sharp(await vfs.file(smallPath))
|
);
|
||||||
.trim()
|
const small = await extractPackedTankIcon(
|
||||||
.toBuffer();
|
await vfs.file(smallPath),
|
||||||
|
`tank ${tankKey} small`,
|
||||||
|
);
|
||||||
|
|
||||||
if (big) {
|
if (big) {
|
||||||
await uploader.add({
|
await uploader.add({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
bufferToBigInt,
|
bufferToBigInt,
|
||||||
|
ConfigArchive,
|
||||||
Hierarchy,
|
Hierarchy,
|
||||||
Sc2ReadStream,
|
Sc2ReadStream,
|
||||||
ScgReadStream,
|
ScgReadStream,
|
||||||
@@ -75,6 +76,42 @@ export async function extractModel(vfs: AbstractVFS, path: string) {
|
|||||||
.setImage(baseColor),
|
.setImage(baseColor),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let defaultConfigArchive: ConfigArchive | undefined = undefined;
|
||||||
|
|
||||||
|
if (node.configCount) {
|
||||||
|
const names = times(
|
||||||
|
node.configCount,
|
||||||
|
(index) => node[`configArchive_${index}`],
|
||||||
|
);
|
||||||
|
|
||||||
|
defaultConfigArchive = names.find(
|
||||||
|
(archive) => archive.configName === "Default",
|
||||||
|
);
|
||||||
|
defaultConfigArchive ??= node.configArchive_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customCullMode =
|
||||||
|
node.customCullMode ?? defaultConfigArchive?.customCullMode;
|
||||||
|
|
||||||
|
if (customCullMode === 0) {
|
||||||
|
material.setDoubleSided(true);
|
||||||
|
} else if (customCullMode !== undefined) {
|
||||||
|
material.setDoubleSided(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
node.enabledPresets?.AlphaTest &&
|
||||||
|
node.properties?.alphatestThreshold
|
||||||
|
) {
|
||||||
|
const view = new DataView(node.properties.alphatestThreshold);
|
||||||
|
const alphaCutoff =
|
||||||
|
view.byteLength < 4
|
||||||
|
? 0.5
|
||||||
|
: view.getFloat32(view.byteLength - 4, true);
|
||||||
|
|
||||||
|
material.setAlphaMode("MASK").setAlphaCutoff(alphaCutoff);
|
||||||
|
}
|
||||||
|
|
||||||
if (node.configCount) {
|
if (node.configCount) {
|
||||||
for (
|
for (
|
||||||
let configIndex = 0;
|
let configIndex = 0;
|
||||||
@@ -85,14 +122,17 @@ export async function extractModel(vfs: AbstractVFS, path: string) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
archive.configName !== "Default" ||
|
archive.configName !== "Default" ||
|
||||||
!archive.enabledPresets?.AlphaTest
|
!archive.enabledPresets?.AlphaTest ||
|
||||||
|
!archive.properties?.alphatestThreshold
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = new DataView(archive.properties.alphatestThreshold);
|
const view = new DataView(archive.properties.alphatestThreshold);
|
||||||
const _ = view.getUint8(0); // only god knows what these flags are
|
const alphaCutoff =
|
||||||
const alphaCutoff = view.getFloat32(1, true);
|
view.byteLength < 4
|
||||||
|
? 0.5
|
||||||
|
: view.getFloat32(view.byteLength - 4, true);
|
||||||
|
|
||||||
material.setAlphaMode("MASK").setAlphaCutoff(alphaCutoff);
|
material.setAlphaMode("MASK").setAlphaCutoff(alphaCutoff);
|
||||||
}
|
}
|
||||||
@@ -171,8 +211,8 @@ export async function extractModel(vfs: AbstractVFS, path: string) {
|
|||||||
|
|
||||||
case "TransformComponent": {
|
case "TransformComponent": {
|
||||||
const localTranslation = component["tc.localTranslation"];
|
const localTranslation = component["tc.localTranslation"];
|
||||||
// The game resets top-level node translation to [0, 0, 0] on load.
|
// The game resets top-level node translation to [0, 0, 0] on load.
|
||||||
// Child nodes must keep their authored local offsets.
|
// Child nodes must keep their authored local offsets.
|
||||||
node.setTranslation(
|
node.setTranslation(
|
||||||
parent instanceof Scene ? [0, 0, 0] : localTranslation,
|
parent instanceof Scene ? [0, 0, 0] : localTranslation,
|
||||||
);
|
);
|
||||||
|
|||||||
+12
-2
@@ -18,6 +18,7 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
IconButton,
|
IconButton,
|
||||||
Text,
|
Text,
|
||||||
|
Tooltip,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
|
import { awaitableTankDefinitions } from "../../../../core/awaitables/tankDefinitions";
|
||||||
import { useLocale } from "../../../../hooks/useLocale";
|
import { useLocale } from "../../../../hooks/useLocale";
|
||||||
@@ -55,12 +56,14 @@ function ModuleButton({
|
|||||||
selected,
|
selected,
|
||||||
unlock,
|
unlock,
|
||||||
top,
|
top,
|
||||||
|
tooltip,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
unlock: Unlock;
|
unlock: Unlock;
|
||||||
tier: number;
|
tier: number;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
top: boolean;
|
top: boolean;
|
||||||
|
tooltip?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
const isTank = unlock.type === ModuleType.MODULE_TYPE_VEHICLE;
|
const isTank = unlock.type === ModuleType.MODULE_TYPE_VEHICLE;
|
||||||
@@ -134,7 +137,7 @@ function ModuleButton({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
return isTank ? (
|
const node = isTank ? (
|
||||||
<LinkI18n
|
<LinkI18n
|
||||||
locale={locale}
|
locale={locale}
|
||||||
href={`/tanks/${tankDefinitions.tanks[unlock.id].slug}`}
|
href={`/tanks/${tankDefinitions.tanks[unlock.id].slug}`}
|
||||||
@@ -144,6 +147,10 @@ function ModuleButton({
|
|||||||
) : (
|
) : (
|
||||||
button
|
button
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!tooltip) return node;
|
||||||
|
|
||||||
|
return <Tooltip content={tooltip}>{node}</Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tankDefinitions = await awaitableTankDefinitions;
|
const tankDefinitions = await awaitableTankDefinitions;
|
||||||
@@ -167,7 +174,7 @@ export function Modules() {
|
|||||||
const topGun = topTurret.guns.at(-1)!;
|
const topGun = topTurret.guns.at(-1)!;
|
||||||
const topEngine = tank.engines.at(-1)!;
|
const topEngine = tank.engines.at(-1)!;
|
||||||
const topTrack = tank.tracks.at(-1)!;
|
const topTrack = tank.tracks.at(-1)!;
|
||||||
const { strings } = useLocale();
|
const { strings, unwrap } = useLocale();
|
||||||
|
|
||||||
function setByUnlock(unlock: Unlock) {
|
function setByUnlock(unlock: Unlock) {
|
||||||
Duel.mutate((draft) => {
|
Duel.mutate((draft) => {
|
||||||
@@ -249,6 +256,8 @@ export function Modules() {
|
|||||||
|
|
||||||
if (module === undefined) return null;
|
if (module === undefined) return null;
|
||||||
|
|
||||||
|
const tooltip = unwrap(module.name!);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
key={unlock.id}
|
key={unlock.id}
|
||||||
@@ -301,6 +310,7 @@ export function Modules() {
|
|||||||
<ModuleButton
|
<ModuleButton
|
||||||
unlock={unlock}
|
unlock={unlock}
|
||||||
tier={extractModule(module).tier}
|
tier={extractModule(module).tier}
|
||||||
|
tooltip={tooltip}
|
||||||
selected={
|
selected={
|
||||||
(unlock.type === ModuleType.MODULE_TYPE_TURRET
|
(unlock.type === ModuleType.MODULE_TYPE_TURRET
|
||||||
? turret.id
|
? turret.id
|
||||||
|
|||||||
Reference in new issue
Block a user