diff --git a/apps/game/public/assets/PROVENANCE.md b/apps/game/public/assets/PROVENANCE.md index c6d2604..a39c38f 100644 --- a/apps/game/public/assets/PROVENANCE.md +++ b/apps/game/public/assets/PROVENANCE.md @@ -76,10 +76,10 @@ worse textures. Not everything here is generated. These are licensed, and their provenance is the licence rather than a generator script. -| File | Source | Licence | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------- | -| `models/fighter_swat.glb` | [Quaternius — Ultimate Modular Characters](https://quaternius.com/packs/ultimatemodularcharacters.html) (`Swat`) | CC0 1.0 | -| `models/fighter_worker.glb` | [Quaternius — Ultimate Modular Characters](https://quaternius.com/packs/ultimatemodularcharacters.html) (`Worker`) | CC0 1.0 | +| File | Source | Licence | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------- | +| `models/fighter_swat.glb` | [Quaternius — Ultimate Modular Characters](https://quaternius.com/packs/ultimatemodularcharacters.html) (`Swat`) | CC0 1.0 | +| `models/fighter_adventurer.glb` | [Quaternius — Ultimate Modular Characters](https://quaternius.com/packs/ultimatemodularcharacters.html) (`Adventurer`) | CC0 1.0 | **CC0 1.0** (https://creativecommons.org/publicdomain/zero/1.0/) is a public domain dedication: unrestricted commercial use, modification and diff --git a/apps/game/public/assets/manifest.json b/apps/game/public/assets/manifest.json index 86254cc..da000be 100644 --- a/apps/game/public/assets/manifest.json +++ b/apps/game/public/assets/manifest.json @@ -1,7 +1,7 @@ { "generator": "tools/art/build-assets.mjs", "blender": "Blender 4.5.12 LTS", - "commit": "bfc24e463e97c8c28389df2ba795a458561313d5", + "commit": "ad926825e31ded8e6197881e6d68cf00f368ed65", "license": "Original work, © NIGHTCELL 7. No third-party assets.", "source": "Generated procedurally from the scripts in tools/art. No asset is downloaded, photographed, traced, or derived from another game.", "textureSize": 1024, @@ -11,8 +11,8 @@ "character.glb", "container.glb", "deck.glb", + "fighter_adventurer.glb", "fighter_swat.glb", - "fighter_worker.glb", "hardpoint.glb", "lamp_mast.glb", "pipe_rack.glb", @@ -72,12 +72,14 @@ "music_frost-on-the-oar.mp3", "music_ironwood-oath.mp3", "music_runes-on-ice.mp3", - "music_storm-crown-oath.mp3" + "music_storm-crown-oath.mp3", + "music_the-wolf-called-want-part-2.mp3", + "music_the-wolf-called-want.mp3" ], "bytes": { - "models": 3668192, + "models": 4206012, "textures": 1971598, - "audio": 326680, - "total": 5966470 + "audio": 323166, + "total": 6500776 } } diff --git a/apps/game/public/assets/models/fighter_adventurer.glb b/apps/game/public/assets/models/fighter_adventurer.glb new file mode 100644 index 0000000..e281be1 Binary files /dev/null and b/apps/game/public/assets/models/fighter_adventurer.glb differ diff --git a/apps/game/public/assets/models/fighter_worker.glb b/apps/game/public/assets/models/fighter_worker.glb deleted file mode 100644 index 1d1edc4..0000000 Binary files a/apps/game/public/assets/models/fighter_worker.glb and /dev/null differ diff --git a/apps/game/public/audio/The Wolf Called Want (Part 2).mp3 b/apps/game/public/audio/music_the-wolf-called-want-part-2.mp3 similarity index 100% rename from apps/game/public/audio/The Wolf Called Want (Part 2).mp3 rename to apps/game/public/audio/music_the-wolf-called-want-part-2.mp3 diff --git a/apps/game/public/audio/The Wolf Called Want.mp3 b/apps/game/public/audio/music_the-wolf-called-want.mp3 similarity index 100% rename from apps/game/public/audio/The Wolf Called Want.mp3 rename to apps/game/public/audio/music_the-wolf-called-want.mp3 diff --git a/apps/game/src/assets.test.ts b/apps/game/src/assets.test.ts index a6fdec0..946ba56 100644 --- a/apps/game/src/assets.test.ts +++ b/apps/game/src/assets.test.ts @@ -28,10 +28,18 @@ const TEXTURES_DIR = join(ASSETS, "textures"); * * The hard limit is the 15 MB shell download budget (PRD §30, * `DOWNLOAD_BUDGET_BYTES`), which also has to fit the engine, the code and the - * fonts. 6 MB leaves room for all of that and is roughly double what the - * current set uses, so it catches a runaway without failing on every addition. + * fonts. + * + * Raised from 6 MB to 9 MB when the two licensed characters landed: they are + * 2.8 MB between them and pushed the set to 6.2 MB legitimately, rather than + * through any runaway. 9 MB still leaves real headroom under the hard limit + * and would catch a genuine mistake — an uncompressed texture set or a + * forgotten master would blow straight past it. + * + * Streamed music is deliberately outside this: it is fetched on demand and the + * game is playable before a note arrives. */ -const ASSET_BUDGET_BYTES = 6 * 1024 * 1024; +const ASSET_BUDGET_BYTES = 9 * 1024 * 1024; /** Read the JSON chunk out of a GLB container. */ function glbJson(file: string): { @@ -70,7 +78,7 @@ describe("generated models", () => { * apply. Their provenance is a licence recorded in PROVENANCE.md rather than * a generator script. */ - const LICENSED = new Set(["fighter_swat", "fighter_worker"]); + const LICENSED = new Set(["fighter_swat", "fighter_adventurer"]); it("only uses material slots the engine can bind", () => { // A slot the engine does not know about is not an error at load time: the diff --git a/apps/game/src/assets.ts b/apps/game/src/assets.ts index 5aff2b0..5498cf1 100644 --- a/apps/game/src/assets.ts +++ b/apps/game/src/assets.ts @@ -59,7 +59,7 @@ export const MODELS = [ // animations and materials, so they bypass the material-slot convention the // generated props follow — see apps/game/public/assets/PROVENANCE.md. "fighter_swat", - "fighter_worker", + "fighter_adventurer", ] as const; export type ModelName = (typeof MODELS)[number]; diff --git a/apps/game/src/audio.ts b/apps/game/src/audio.ts index 3a04fac..8138374 100644 --- a/apps/game/src/audio.ts +++ b/apps/game/src/audio.ts @@ -48,6 +48,8 @@ const MUSIC = [ "music_ironwood-oath", "music_runes-on-ice", "music_storm-crown-oath", + "music_the-wolf-called-want", + "music_the-wolf-called-want-part-2", ] as const; type VariedName = keyof typeof VARIED; diff --git a/apps/game/src/opponents.ts b/apps/game/src/opponents.ts index 0dcf68b..2517767 100644 --- a/apps/game/src/opponents.ts +++ b/apps/game/src/opponents.ts @@ -136,8 +136,11 @@ export class Opponents { private readonly deadUntil = new Map(); constructor(scene: Scene, assets: AssetSet) { - // One model per faction. The Directorate is equipped and uniformed; the - // Nightcell side are irregulars in civilian clothing. That difference is + // One model per faction. + // + // Nightcell are irregulars: olive drab, boots, a pack — someone fighting + // with what they own. The first attempt used the pack's `Worker`, which is + // a hi-vis construction labourer and read exactly like one. That difference is // the fastest target ID a player gets, and it is carried by the whole // silhouette rather than by a colour swatch. // Back on the generated character for now. @@ -153,7 +156,7 @@ export class Opponents { // licensed ones are diagnosed. Swapping back is a one-line change: // assets.models.get("fighter_swat") / ("fighter_worker") const enemyModel = assets.models.get("fighter_swat") ?? assets.models.get("character"); - const friendlyModel = assets.models.get("fighter_worker") ?? assets.models.get("character"); + const friendlyModel = assets.models.get("fighter_adventurer") ?? assets.models.get("character"); const character = enemyModel; const carbine = assets.models.get("carbine"); if (!character) throw new Error("no character model loaded"); diff --git a/tools/art/build-assets.mjs b/tools/art/build-assets.mjs index 8caa831..db8686c 100644 --- a/tools/art/build-assets.mjs +++ b/tools/art/build-assets.mjs @@ -132,8 +132,9 @@ function toWebp(pngDir, outDir) { */ function directoryBytes(dir, skip) { if (!existsSync(dir)) return 0; + const drop = typeof skip === "function" ? skip : (f) => Boolean(skip?.test(f)); return readdirSync(dir) - .filter((f) => !skip || !skip.test(f)) + .filter((f) => !skip || !drop(f)) .reduce((sum, f) => sum + statSync(join(dir, f)).size, 0); } @@ -267,10 +268,15 @@ function main() { const modelBytes = directoryBytes(MODELS_OUT); const textureBytes = directoryBytes(TEXTURES_OUT); // Skip streamed music and any video the directory happens to hold. - const audioBytesTotal = directoryBytes(AUDIO_OUT, /^music_|\.mp4$/); + // Allowlist the generated effects rather than trying to pattern-match the + // music. Tracks arrive with whatever names they were given, so an exclusion + // pattern silently under-counts the moment one does not match — which is how + // 22 MB of songs ended up inside a 6 MB budget guard. + const SFX = /^(fire_|step_|impact_|reload\.|ui_|ambience_yard\.)/; + const audioBytesTotal = directoryBytes(AUDIO_OUT, (f) => !SFX.test(f)); const audio = existsSync(AUDIO_OUT) ? readdirSync(AUDIO_OUT) - .filter((f) => f.endsWith(".mp3") && !f.startsWith("music_")) + .filter((f) => f.endsWith(".mp3") && SFX.test(f)) .sort() : []; @@ -301,7 +307,7 @@ function main() { // Streamed on demand, so outside the shell budget entirely. music: existsSync(AUDIO_OUT) ? readdirSync(AUDIO_OUT) - .filter((f) => f.startsWith("music_") && f.endsWith(".mp3")) + .filter((f) => f.endsWith(".mp3") && !SFX.test(f)) .sort() : [], bytes: {