diff --git a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts
index b30fabdc9..a934a5ad8 100644
--- a/packages/scripts/src/buildAssets/consumableProvisionIcons.ts
+++ b/packages/scripts/src/buildAssets/consumableProvisionIcons.ts
@@ -14,42 +14,6 @@ interface Mappings {
const listItemsPattern = //g;
-async function extractPackedIcon(
- texture: sharp.Sharp,
- sizes: number[],
- context: string,
-) {
- const [left, top, width, height] = sizes;
- 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.clone().extract({ left, top, width, height }).toBuffer();
-}
-
export async function consumableProvisionIcons() {
console.log("Building consumable and provision icons...");
diff --git a/packages/website/src/core/blitz/extractPackedIcon.ts b/packages/website/src/core/blitz/extractPackedIcon.ts
new file mode 100644
index 000000000..b2b1f2ca0
--- /dev/null
+++ b/packages/website/src/core/blitz/extractPackedIcon.ts
@@ -0,0 +1,31 @@
+import type sharp from "sharp";
+
+export async function extractPackedIcon(texture: sharp.Sharp, sizes: number[]) {
+ const [left, top, width, height] = sizes;
+ const bounds = [left, top, width, height];
+
+ if (!bounds.every(Number.isInteger)) {
+ throw new Error(`Invalid RIFF sprite bounds: ${bounds.join(" ")}`);
+ }
+
+ const metadata = await texture.metadata();
+
+ if (metadata.width === undefined || metadata.height === undefined) {
+ throw new Error(`Failed to read image dimensions`);
+ }
+
+ 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: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`,
+ );
+ }
+
+ return await texture.clone().extract({ left, top, width, height }).toBuffer();
+}
diff --git a/packages/scripts/src/core/blitz/parsePackedSpriteRect.ts b/packages/website/src/core/blitz/parsePackedSpriteRect.ts
similarity index 100%
rename from packages/scripts/src/core/blitz/parsePackedSpriteRect.ts
rename to packages/website/src/core/blitz/parsePackedSpriteRect.ts
diff --git a/packages/website/src/pages/[...chunk_api]/icons/consumables/[id].webp.ts b/packages/website/src/pages/[...chunk_api]/icons/consumables/[id].webp.ts
new file mode 100644
index 000000000..8b02390b8
--- /dev/null
+++ b/packages/website/src/pages/[...chunk_api]/icons/consumables/[id].webp.ts
@@ -0,0 +1,112 @@
+import type { ConsumablesCommon } from "@blitzkit/core";
+import type { APIContext, GetStaticPathsItem } from "astro";
+import sharp from "sharp";
+import { extractPackedIcon } from "../../../../core/blitz/extractPackedIcon";
+import { parsePackedSpriteRect } from "../../../../core/blitz/parsePackedSpriteRect";
+import { mixStaticPaths } from "../../../../core/blitzkit/mixStaticPaths";
+import { vfs } from "../../../../core/blitzkit/vfs";
+import { getStaticPaths as _getStaticPaths } from "../../_index";
+
+interface Mappings {
+ Header: { version: number };
+ StyleSheets: {
+ selectors: string[];
+ properties: { "bg-sprite": string };
+ }[];
+}
+
+const listItemsPattern = //g;
+
+export const getStaticPaths = mixStaticPaths(_getStaticPaths, async () => {
+ const paths: GetStaticPathsItem[] = [];
+
+ const styles = [
+ "UI/Styles/Lobby/Inventory/InventoryNormalStyles.yaml",
+ "UI/Styles/Lobby/Inventory/Event/InventoryBigStyles.yaml",
+ "UI/Styles/Lobby/Inventory/Event/InventoryNormalStyles.yaml",
+ "UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml",
+ "UI/Screens/Battle/Styles/BattleEquipmentStyles.yaml",
+ ];
+ const styleSheets = await Promise.all(
+ styles.map(async (path) =>
+ Object.values((await vfs.yaml(`Data/${path}`)).StyleSheets),
+ ),
+ ).then((array) => array.flat());
+
+ for (const match of (
+ await vfs.text(`Data/XML/item_defs/vehicles/common/consumables/list.xml`)
+ ).matchAll(listItemsPattern)) {
+ const consumablesCommon = await vfs.xml<{
+ root: ConsumablesCommon;
+ }>(`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`);
+
+ for (const consumable of Object.values(consumablesCommon.root)) {
+ const styleSheet = styleSheets.find((styleSheet) =>
+ styleSheet.selectors.some((selector) =>
+ selector.includes(`${consumable.icon} `),
+ ),
+ );
+
+ if (!styleSheet) {
+ console.warn(
+ `No style sheet found for consumable ${consumable.icon}; skipping...`,
+ );
+ continue;
+ }
+
+ const path = styleSheet.properties["bg-sprite"]
+ .replace("~res:/", "")
+ .replace(".psd", "")
+ .replace(".txt", "");
+
+ paths.push({
+ props: { path },
+ params: { id: consumable.id },
+ });
+ }
+ }
+
+ return paths;
+});
+
+export async function GET({ props }: APIContext<{ path: string }>) {
+ let buffer: Buffer;
+
+ if (await vfs.resolve(`Data/${props.path}.packed.webp`)) {
+ const packedBuffer = await vfs.file(`Data/${props.path}.packed.webp`);
+ const consumablesTexture = sharp(packedBuffer);
+ const sizes = parsePackedSpriteRect(packedBuffer);
+
+ if (sizes) {
+ buffer = await extractPackedIcon(consumablesTexture, sizes);
+ } else {
+ buffer = await consumablesTexture.trim({ threshold: 80 }).toBuffer();
+ }
+ } else {
+ const consumablesTexture = sharp(
+ await vfs.file(
+ `${`Data/${props.path}`
+ .split("/")
+ .slice(0, -1)
+ .join("/")}/texture0.packed.webp`,
+ ),
+ );
+ const sizes = (await vfs.text(`Data/${props.path}.txt`))
+ .split("\n")[4]
+ .split(" ")
+ .map(Number);
+ buffer = await consumablesTexture
+ .clone()
+ .extract({
+ left: sizes[0],
+ top: sizes[1],
+ width: sizes[2],
+ height: sizes[3],
+ })
+ .toBuffer();
+ }
+
+ const bytes = new Uint8Array(buffer);
+
+ return new Response(bytes);
+}