---
name: pixi-vn-assets
description: Use when adding, organizing, registering, or loading non-code files (images, audio, video) in a Pixi'VN project — local assets processed by PixiJS AssetPack vs. online/remote-hosted assets, the AssetsManifest/bundle/alias system, where to edit the manifest for each case, and when to load bundles (at startup, in the background, per-label, per-route). Load this before writing any code that references an image/sound/video path directly, or before adding entries to a manifest.
---

# Pixi'VN Assets: local vs. online, manifests, bundles, and loading strategy

Official docs: [pixi-vn.com/start/assets](https://pixi-vn.com/start/assets) (storage options) and
[pixi-vn.com/start/assets-management](https://pixi-vn.com/start/assets-management) (loading
strategy).

## When to use this skill

Use this skill whenever a task involves adding a new image/audio/video file to a project, deciding
whether an asset should live locally or be hosted online, editing the assets manifest, or choosing
when a bundle of assets should load (blocking at startup, in the background, per-label, per-route).
This skill does not cover _how_ an image/sound is displayed/played once loaded — that's
`pixi-vn-canvas` / `pixi-vn-sound` — only how it gets registered and made available by alias.

**The one rule that matters most: always reference assets by their manifest `alias`, never by raw
`src` path/URL, in game code.** Loading by path directly is strongly discouraged — it couples code
to a specific file location/host, so moving a file or switching hosting providers silently breaks
every reference. `Assets.load("bg01-hallway")`, not `Assets.load("https://.../bg01-hallway.webp")`.

## Local vs. online assets

Assets can be stored **locally** in the project or hosted **online** (a CDN, GitHub raw, cloud
storage). Frequently used assets (character sprites, backgrounds, background music) are best kept
local; assets used once, or large optional files, are better hosted online to keep the project
small and the initial download fast.

|                      | Local                                                     | Online                                |
| -------------------- | --------------------------------------------------------- | ------------------------------------- |
| Bundled into the app | Yes — ships in `public/assets`                            | No — fetched at runtime from a URL    |
| Best for             | Frequently used assets (backgrounds, sprites, core music) | Large/optional/rarely-used assets     |
| Requires             | PixiJS AssetPack (pre-configured in official templates)   | A CORS-enabled host with a public URL |

## Local assets: the `src/assets` folder + PixiJS AssetPack

Local assets go under `src/assets` (any folder works, but this is the convention every official
template uses), organized into subfolders however makes sense for the project. **PixiJS AssetPack**
— pre-wired into the Vite config of every official template — watches this folder and, on every
change (`vite dev`) or once before a production build (`vite build`), optimizes the files
(compression, WebP/AVIF conversion, texture-atlas packing) into `public/assets`, and **auto-generates
`src/assets/manifest.gen.json`** describing every resulting bundle/alias.

```ts
// vite.config.ts (already present in official templates — no changes usually needed)
import { AssetPack } from "@assetpack/core";
import assetPackConfig from "./.assetpack.ts";
// a Vite plugin runs `new AssetPack(assetPackConfig).watch()` in dev,
// `.run()` once during `vite build`
```

```ts
// .assetpack.ts (already present in official templates)
import { pixiPipes } from "@assetpack/core/pixi";

export default {
  entry: "./src/assets",
  output: "./public/assets",
  ignore: ["**/*.ts", "**/*.js", "**/*.gen.*"],
  pipes: [
    pixiPipes({ manifest: { output: "src/assets/manifest.gen.json" } }),
    // official templates add two extra pipes here: one bundle per top-level
    // folder (instead of everything in a single "default" bundle), and
    // stripping file extensions from generated aliases
  ],
};
```

**`src/assets/manifest.gen.json` is generated — never hand-edit it.** Any manual change is
overwritten the next time AssetPack runs. If a local asset isn't showing up with the alias you
expect, check the actual file's path/name under `src/assets` and let AssetPack regenerate the file,
rather than editing the JSON directly.

```json
// src/assets/manifest.gen.json (excerpt — auto-generated, read-only)
{
  "bundles": [
    {
      "name": "mc",
      "assets": [{ "alias": "mc-neutral", "src": "./assets/mc/mc-neutral.png" }]
    }
  ]
}
```

## Online assets: the exact place to register them — `src/assets/index.ts`

For assets you host online (or any manifest entries beyond what AssetPack generates), **the one
place to add/edit them is `src/assets/index.ts`**, present in every official template. It imports
the auto-generated local manifest and appends hand-written bundles for remote URLs:

```ts
// src/assets/index.ts — the exact, single point to add/modify online assets
import generatedManifestJson from "@/assets/manifest.gen.json";
import type { AssetsManifest } from "@drincs/pixi-vn";

export const manifest: AssetsManifest = {
  bundles: [
    ...generatedManifestJson.bundles, // everything AssetPack generated locally
    {
      name: "audio", // pick a bundle name (see "Organizing into bundles" below)
      assets: [
        {
          alias: "bgm_cheerful",
          src: "https://your-cdn.example.com/audio/bgm_cheerful.wav",
        },
        {
          alias: "sfx_whoosh",
          src: "https://your-cdn.example.com/audio/sfx_whoosh.wav",
        },
      ],
    },
  ],
};
```

Popular CORS-friendly hosting options (see [pixi-vn.com/start/assets#assets-hosting](https://pixi-vn.com/start/assets#assets-hosting) for the full comparison of price/limits/editability):
GitHub raw URLs (`https://raw.githubusercontent.com/[repo]/refs/heads/main/[path]`, free, 100MB/file
limit), image-hosting services (imgbb/imgur, images only), or cloud storage (Cloudflare R2, Firebase
Storage, S3, Supabase) for anything larger or non-image. Whichever host is used, it must allow
**CORS requests** or the browser will block loading the asset.

## Organizing assets into bundles

Group related assets into **bundles** instead of registering each one loosely — a bundle loads (or
defers loading) as a single unit. Name each bundle after **where it's used**:

- a label's id (e.g. `startLabel.id`) for assets only that label needs
- a route's path (e.g. `"/"`, `"/game/narration"`) for assets a specific screen needs
- a character's id for that character's sprite/expression assets

This convention matters most for **online** assets, where it directly controls what the player
waits for; for local assets (which load near-instantly) precise bundle boundaries matter far less.

```ts
// src/assets/index.ts
import { startLabel } from "@/content/labels/start.label";

export const manifest: AssetsManifest = {
  bundles: [
    { name: "/", assets: [{ alias: "background_main_menu", src: "..." }] }, // main menu route
    { name: startLabel.id, assets: [{ alias: "bg01-hallway", src: "..." }] }, // one label
    { name: "mc", assets: [{ alias: "mc-neutral", src: "..." }] }, // one character
  ],
};
```

## Initializing and loading

Register the manifest once, near app startup, before relying on any alias:

```ts
import { manifest } from "@/assets";
import { Assets } from "@drincs/pixi-vn";

let assetsInitialized = false;
export async function defineAssets() {
  if (!assetsInitialized) {
    const origin = `${location.protocol}//${location.host}/`;
    Assets.resolver.rootPath = origin;
    await Assets.init({ manifest, basePath: `${origin}assets/` });
    assetsInitialized = true;
  }
  await Assets.loadBundle("/"); // block on what the first screen needs
  Assets.backgroundLoadBundle("audio"); // load the rest quietly in the background
}
```

Then reference assets purely by alias, anywhere (`pixi-vn-canvas` for images/video,
`pixi-vn-sound` for audio):

```ts
const texture = await Assets.load("mc-neutral");
```

By default assets load on demand, but waiting until the exact moment an asset is needed can cause a
visible stall. Common, recommended loading moments, least-to-most eager:

- **At label start (background)** — in a label's `onLoadingLabel` hook, call
  `Assets.backgroundLoadBundle(label.id)` (fire-and-forget): the label starts immediately, its
  bundle streams in without blocking. Prefer this over the blocking variant below unless a step
  genuinely cannot proceed without the asset.
- **At label start (blocking)** — same hook, but `await Assets.loadBundle(label.id)`: the label
  waits for its bundle before its first step runs. Only use this when the label truly can't start
  without those assets.
- **At project start** — `await Assets.loadBundle(...)` for anything used on the very first screen
  (recommended limit: under 100MB, so the initial wait stays short).
- **In the background at project start** — `Assets.backgroundLoadBundle(...)` for larger,
  non-blocking preloading (recommended limit: under 2GB).
- **Before a route/screen renders** — inside a router's `loader` (e.g. TanStack Router), call
  `await Assets.loadBundle(routePath)`, paired with a `pendingComponent` so the player sees a
  loading state instead of a blank screen.

## Real-world project convention (official React template)

`src/lib/utils/assets-utility.ts`'s `defineAssets()` combines several of the strategies above: guards
`Assets.init` with an `assetsInitialized` flag (safe to call more than once), **blocks** on the
current route's bundle (`await Assets.loadBundle("/")`) so the first screen never shows missing
assets, then kicks off the audio bundle in the **background** (`sound.backgroundLoadBundle(...)` —
the sound module's own wrapper, not `Assets.backgroundLoadBundle`, since it also has to wire the
loaded media into a channel). Separately, `main.tsx` wires `Game.onLoadingLabel((_stepId, { id }) =>
Assets.backgroundLoadBundle(id))` **once, globally** — so every label's own bundle (named after its
id, per the convention above) is background-loaded automatically the moment that label starts,
without every label needing its own `onLoadingLabel` hook. This is the template's convention, not a
library requirement — a project is free to load more/less eagerly.

For caching **online** assets so returning players don't re-download them: official templates
pre-configure [VitePWA](https://vite-pwa-org.netlify.app/)'s service worker to cache responses from
specific hostnames (a `CACHED_EXTERNAL_HOSTNAMES` list in `vite.config.ts`) for 7 days by default —
add any CDN/host actually used to that list. See
[pixi-vn.com/start/assets#caching-assets](https://pixi-vn.com/start/assets#caching-assets).

## Related skills

- **pixi-vn-getting-started** — the `@drincs/pixi-vn/vite` plugin's `assetsManifest` option bridges
  a manifest (often this skill's `src/assets/index.ts`) into the plugin's generated type file and
  dev-server endpoint; `Game.onLoadingLabel` is where label-scoped bundle loading is wired.
- **pixi-vn-canvas** — displaying images/video once their alias is loaded.
- **pixi-vn-sound** — playing audio once its alias is loaded, and the sound module's own
  `backgroundLoadBundle`/`loadBundle` wrappers.
- **pixi-vn-narration** — labels are a natural bundle-naming unit (`label.id`).
