@jgengine/assets 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/README.md +103 -50
  3. package/dist/aliases.d.ts +5 -0
  4. package/dist/aliases.js +28 -9
  5. package/dist/catalogs/build.d.ts +27 -0
  6. package/dist/catalogs/build.js +28 -2
  7. package/dist/catalogs/buildSprites.d.ts +10 -0
  8. package/dist/catalogs/buildSprites.js +15 -0
  9. package/dist/catalogs/starter.d.ts +5 -0
  10. package/dist/catalogs/starter.js +10 -1
  11. package/dist/cli/paths.d.ts +9 -1
  12. package/dist/cli/paths.js +11 -1
  13. package/dist/cli/pull.d.ts +12 -0
  14. package/dist/cli/pull.js +216 -25
  15. package/dist/collisionMeshAssets.d.ts +2 -0
  16. package/dist/collisionMeshAssets.js +6 -0
  17. package/dist/dims.d.ts +7 -0
  18. package/dist/dims.js +154 -0
  19. package/dist/download.d.ts +66 -7
  20. package/dist/download.js +264 -18
  21. package/dist/find.d.ts +20 -1
  22. package/dist/find.js +52 -2
  23. package/dist/generated/index.js +18 -6
  24. package/dist/generated/kaykit-adventurers.json +706 -0
  25. package/dist/generated/kaykit-city-builder.json +904 -0
  26. package/dist/generated/kaykit-dungeon.json +5002 -0
  27. package/dist/generated/kaykit-furniture.json +1168 -0
  28. package/dist/generated/kaykit-skeletons.json +410 -0
  29. package/dist/generated/kaykit-space-base.json +1199 -0
  30. package/dist/generated/quaternius-medieval-village.json +3698 -0
  31. package/dist/generated/quaternius-modular-scifi.json +4013 -0
  32. package/dist/generated/quaternius-stylized-nature.json +1498 -0
  33. package/dist/generated-sprites/index.d.ts +5 -0
  34. package/dist/generated-sprites/index.js +4 -0
  35. package/dist/index.d.ts +9 -2
  36. package/dist/index.js +9 -2
  37. package/dist/indexGen.d.ts +2 -2
  38. package/dist/indexGen.js +34 -10
  39. package/dist/manifest.d.ts +16 -1
  40. package/dist/materials.d.ts +61 -0
  41. package/dist/materials.js +98 -0
  42. package/dist/packs/starter.d.ts +46 -0
  43. package/dist/packs/starter.js +248 -0
  44. package/dist/provisioning.d.ts +88 -0
  45. package/dist/provisioning.js +74 -0
  46. package/dist/registry-catalog.json +35 -0
  47. package/dist/snippet.d.ts +4 -0
  48. package/dist/snippet.js +27 -0
  49. package/dist/sources/ambientcg.d.ts +5 -0
  50. package/dist/sources/ambientcg.js +64 -0
  51. package/dist/sources/gameicons.d.ts +10 -0
  52. package/dist/sources/gameicons.js +21 -0
  53. package/dist/sources/index.d.ts +9 -2
  54. package/dist/sources/index.js +11 -3
  55. package/dist/sources/kaykit.js +55 -6
  56. package/dist/sources/quaternius.js +60 -9
  57. package/dist/spriteIndexGen.d.ts +17 -0
  58. package/dist/spriteIndexGen.js +108 -0
  59. package/dist/verify.d.ts +1 -0
  60. package/dist/verify.js +38 -1
  61. package/package.json +4 -4
  62. package/skills/jgengine/SKILL.md +54 -0
  63. package/skills/jgengine/api.md +527 -0
  64. package/skills/jgengine/capabilities.md +131 -0
  65. package/skills/jgengine/recipes/minimal-game.md +88 -0
  66. package/skills/jgengine-assets/SKILL.md +45 -0
  67. package/skills/jgengine-assets/api.md +282 -0
  68. package/skills/jgengine-assets/capabilities.md +13 -0
  69. package/dist/generated/kenney-blaster.json +0 -362
  70. package/dist/generated/kenney-nature.json +0 -3292
  71. package/dist/generated/kenney-space.json +0 -1532
  72. package/dist/sources/kenney.d.ts +0 -2
  73. package/dist/sources/kenney.js +0 -36
  74. package/llms.txt +0 -1010
package/dist/download.js CHANGED
@@ -1,5 +1,65 @@
1
1
  import { unzipSync } from "fflate";
2
2
  import { isScrapeDownload } from "./manifest.js";
3
+ /** Max size of a downloaded (still-compressed) archive, in bytes. Provider zips run tens of MB; this leaves headroom without buffering an unbounded response. */
4
+ export const MAX_ARCHIVE_DOWNLOAD_BYTES = 256 * 1024 * 1024;
5
+ /** Max total uncompressed size this module will inflate out of one archive, in bytes. */
6
+ export const MAX_ARCHIVE_UNCOMPRESSED_BYTES = 512 * 1024 * 1024;
7
+ /** Max number of entries this module will extract out of one archive. */
8
+ export const MAX_ARCHIVE_ENTRY_COUNT = 20_000;
9
+ /** Max allowed originalSize/size ratio for a single archive entry — past this it's treated as a zip bomb. */
10
+ export const MAX_ARCHIVE_COMPRESSION_RATIO = 100;
11
+ /** @internal */
12
+ export function boundedExtractFilter(matches) {
13
+ let totalUncompressedBytes = 0;
14
+ let entryCount = 0;
15
+ return (file) => {
16
+ if (!matches(file))
17
+ return false;
18
+ if (file.size > 0 && file.originalSize / file.size > MAX_ARCHIVE_COMPRESSION_RATIO) {
19
+ throw new Error(`archive entry "${file.name}" has a ${(file.originalSize / file.size).toFixed(0)}x compression ratio, exceeds the ${MAX_ARCHIVE_COMPRESSION_RATIO}x zip-bomb guard`);
20
+ }
21
+ totalUncompressedBytes += file.originalSize;
22
+ if (totalUncompressedBytes > MAX_ARCHIVE_UNCOMPRESSED_BYTES) {
23
+ throw new Error(`archive would extract more than the ${MAX_ARCHIVE_UNCOMPRESSED_BYTES} byte uncompressed-size cap`);
24
+ }
25
+ entryCount += 1;
26
+ if (entryCount > MAX_ARCHIVE_ENTRY_COUNT) {
27
+ throw new Error(`archive has more than the ${MAX_ARCHIVE_ENTRY_COUNT} entry cap`);
28
+ }
29
+ return true;
30
+ };
31
+ }
32
+ const PACK_IMAGE_EXT = /\.(png|jpe?g|webp)$/i;
33
+ /** Flatten a glTF image URI ("../Textures/color%20map.png") to its decoded basename. */
34
+ function imageUriBasename(uri) {
35
+ const decoded = (() => {
36
+ try {
37
+ return decodeURIComponent(uri);
38
+ }
39
+ catch {
40
+ return uri;
41
+ }
42
+ })();
43
+ const clean = decoded.replace(/\\/g, "/");
44
+ return clean.split("/").pop() ?? clean;
45
+ }
46
+ /**
47
+ * Rewrite external image URIs to flat basenames so a model always finds its
48
+ * texture beside itself in the pack dir, no matter how the source archive
49
+ * nested it ("Textures/x.png", "../texture/x.png", "x.png" all become "x.png").
50
+ * Returns the basenames of every external image the model references.
51
+ */
52
+ function normalizeGltfImageUris(json) {
53
+ const referenced = [];
54
+ for (const image of json.images ?? []) {
55
+ if (image.uri === undefined || image.uri.startsWith("data:"))
56
+ continue;
57
+ const base = imageUriBasename(image.uri);
58
+ image.uri = base;
59
+ referenced.push(base);
60
+ }
61
+ return referenced;
62
+ }
3
63
  function resolveRelative(path, pageUrl) {
4
64
  try {
5
65
  return new URL(path, pageUrl).toString();
@@ -8,6 +68,7 @@ function resolveRelative(path, pageUrl) {
8
68
  return path;
9
69
  }
10
70
  }
71
+ /** @internal */
11
72
  export function findArchiveUrl(html, pageUrl) {
12
73
  const matches = html.match(/[^\s"'()<>]+\.zip/gi);
13
74
  if (matches === null)
@@ -26,6 +87,7 @@ function score(candidate) {
26
87
  value += 1;
27
88
  return value;
28
89
  }
90
+ /** @internal */
29
91
  export async function resolveArchiveUrl(source, fetchImpl = fetch) {
30
92
  const download = source.download;
31
93
  if (!isScrapeDownload(download))
@@ -40,17 +102,30 @@ export async function resolveArchiveUrl(source, fetchImpl = fetch) {
40
102
  }
41
103
  return url;
42
104
  }
105
+ /** @internal */
43
106
  export async function downloadArchive(url, fetchImpl = fetch) {
44
107
  const response = await fetchImpl(url, { redirect: "follow" });
45
108
  if (!response.ok)
46
109
  throw new Error(`download ${url} -> HTTP ${response.status}`);
47
- return new Uint8Array(await response.arrayBuffer());
110
+ const declaredLength = response.headers.get("content-length");
111
+ if (declaredLength !== null) {
112
+ const declared = Number(declaredLength);
113
+ if (Number.isFinite(declared) && declared > MAX_ARCHIVE_DOWNLOAD_BYTES) {
114
+ throw new Error(`download ${url} declares ${declared} bytes, exceeds the ${MAX_ARCHIVE_DOWNLOAD_BYTES} byte cap`);
115
+ }
116
+ }
117
+ const bytes = new Uint8Array(await response.arrayBuffer());
118
+ if (bytes.byteLength > MAX_ARCHIVE_DOWNLOAD_BYTES) {
119
+ throw new Error(`download ${url} is ${bytes.byteLength} bytes, exceeds the ${MAX_ARCHIVE_DOWNLOAD_BYTES} byte cap`);
120
+ }
121
+ return bytes;
48
122
  }
49
123
  /**
50
124
  * Layout for the `--mirror` / `JGENGINE_ASSETS_MIRROR` base URL override: the
51
125
  * archive for a pack is expected at `<baseUrl>/<provider>/<packId>.zip`, e.g.
52
126
  * `https://my-mirror.example.com/kenney/kenney-nature.zip`.
53
- */
127
+ * @internal
128
+ */
54
129
  export function mirrorOverrideUrl(baseUrl, source) {
55
130
  const base = baseUrl.replace(/\/+$/, "");
56
131
  return `${base}/${source.provider}/${source.id}.zip`;
@@ -66,7 +141,9 @@ export function mirrorOverrideUrl(baseUrl, source) {
66
141
  * `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
67
142
  */
68
143
  export const DEFAULT_RELEASE_BASE = "https://github.com/Noisemaker111/jgengine/releases/download/packs";
69
- /** URL of `source`'s archive on the default GitHub-release mirror. */
144
+ /** URL of `source`'s archive on the default GitHub-release mirror.
145
+ * @internal
146
+ */
70
147
  export function defaultReleaseUrl(source) {
71
148
  return `${DEFAULT_RELEASE_BASE}/${source.provider}-${source.id}.zip`;
72
149
  }
@@ -89,7 +166,8 @@ async function verifyPinnedSha(source, archive) {
89
166
  * source supplied the bytes; a mismatch is treated as a failed attempt so the
90
167
  * next source in the chain is tried. Throws with every attempted URL and its
91
168
  * failure reason when all sources fail.
92
- */
169
+ * @internal
170
+ */
93
171
  export async function downloadPackArchive(source, options = {}) {
94
172
  const fetchImpl = options.fetchImpl ?? fetch;
95
173
  const attempted = [];
@@ -135,10 +213,8 @@ export async function downloadPackArchive(source, options = {}) {
135
213
  throw new Error(`failed to download ${source.id} from all ${attempted.length} source(s):\n` +
136
214
  errors.map((line) => ` - ${line}`).join("\n"));
137
215
  }
138
- export function extractGlbs(archive) {
139
- const entries = unzipSync(archive, {
140
- filter: (file) => /\.glb$/i.test(file.name),
141
- });
216
+ function dedupeByBasename(archive, pattern) {
217
+ const entries = unzipSync(archive, { filter: boundedExtractFilter((file) => pattern.test(file.name)) });
142
218
  const byName = new Map();
143
219
  for (const [path, bytes] of Object.entries(entries)) {
144
220
  const base = path.split("/").pop();
@@ -149,22 +225,192 @@ export function extractGlbs(archive) {
149
225
  }
150
226
  return Array.from(byName, ([file, bytes]) => ({ file, bytes })).sort((a, b) => a.file.localeCompare(b.file));
151
227
  }
152
- const TEXTURE_ENTRY = /(?:^|\/)(Textures\/[^/]+)$/i;
153
- export function extractTextures(archive) {
228
+ function buildGlb(jsonText, binBytes) {
229
+ const jsonPad = (4 - (jsonText.length % 4)) % 4;
230
+ const jsonChunk = new Uint8Array(jsonText.length + jsonPad);
231
+ new TextEncoder().encodeInto(jsonText, jsonChunk);
232
+ for (let i = jsonText.length; i < jsonChunk.length; i++)
233
+ jsonChunk[i] = 0x20;
234
+ const binPad = binBytes === undefined ? 0 : (4 - (binBytes.byteLength % 4)) % 4;
235
+ const binChunkLen = binBytes === undefined ? 0 : binBytes.byteLength + binPad;
236
+ const total = 12 +
237
+ 8 +
238
+ jsonChunk.byteLength +
239
+ (binBytes === undefined ? 0 : 8 + binChunkLen);
240
+ const out = new Uint8Array(total);
241
+ const view = new DataView(out.buffer);
242
+ view.setUint32(0, 0x46546c67, true); // glTF
243
+ view.setUint32(4, 2, true);
244
+ view.setUint32(8, total, true);
245
+ let o = 12;
246
+ view.setUint32(o, jsonChunk.byteLength, true);
247
+ view.setUint32(o + 4, 0x4e4f534a, true); // JSON
248
+ out.set(jsonChunk, o + 8);
249
+ o += 8 + jsonChunk.byteLength;
250
+ if (binBytes !== undefined) {
251
+ view.setUint32(o, binChunkLen, true);
252
+ view.setUint32(o + 4, 0x004e4942, true); // BIN
253
+ out.set(binBytes, o + 8);
254
+ }
255
+ return out;
256
+ }
257
+ /** Parse a `.glb` container into its JSON document and optional BIN chunk. Returns null for non-glb bytes. */
258
+ function parseGlb(bytes) {
259
+ if (bytes.byteLength < 20)
260
+ return null;
261
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
262
+ if (view.getUint32(0, true) !== 0x46546c67)
263
+ return null;
264
+ let offset = 12;
265
+ let json;
266
+ let bin;
267
+ while (offset + 8 <= bytes.byteLength) {
268
+ const length = view.getUint32(offset, true);
269
+ const kind = view.getUint32(offset + 4, true);
270
+ const body = bytes.subarray(offset + 8, offset + 8 + length);
271
+ if (kind === 0x4e4f534a) {
272
+ try {
273
+ json = JSON.parse(new TextDecoder().decode(body));
274
+ }
275
+ catch {
276
+ return null;
277
+ }
278
+ }
279
+ else if (kind === 0x004e4942) {
280
+ bin = body;
281
+ }
282
+ offset += 8 + length;
283
+ }
284
+ if (json === undefined)
285
+ return null;
286
+ return bin === undefined ? { json } : { json, bin };
287
+ }
288
+ /**
289
+ * External image basenames a packed `.glb` still references (its textures must
290
+ * sit beside it in the pack dir). Empty for self-contained models.
291
+ * @internal
292
+ */
293
+ export function externalGlbImages(bytes) {
294
+ const chunks = parseGlb(bytes);
295
+ if (chunks === null)
296
+ return [];
297
+ return (chunks.json.images ?? [])
298
+ .map((image) => image.uri)
299
+ .filter((uri) => uri !== undefined && !uri.startsWith("data:"))
300
+ .map((uri) => imageUriBasename(uri));
301
+ }
302
+ /** Rewrite a native `.glb`'s external image URIs to flat basenames; returns the input untouched when nothing changes. */
303
+ function normalizeGlbImageUris(bytes) {
304
+ const chunks = parseGlb(bytes);
305
+ if (chunks === null)
306
+ return { bytes, referenced: [] };
307
+ const before = JSON.stringify(chunks.json.images ?? []);
308
+ const referenced = normalizeGltfImageUris(chunks.json);
309
+ if (referenced.length === 0 || JSON.stringify(chunks.json.images ?? []) === before) {
310
+ return { bytes, referenced };
311
+ }
312
+ return { bytes: buildGlb(JSON.stringify(chunks.json), chunks.bin), referenced };
313
+ }
314
+ /**
315
+ * Pack a `.gltf` + optional external `.bin` into a single `.glb` so reindex /
316
+ * the shell only ever deal in one-file models. Strips buffer `uri` fields so
317
+ * the binary chunk is self-contained; external image URIs are flattened to
318
+ * basenames so the pack ships each referenced texture beside the models
319
+ * (loaders resolve the relative URI against the model's own URL).
320
+ * @internal
321
+ */
322
+ export function packGltfToGlb(gltfBytes, binBytes) {
323
+ const json = JSON.parse(new TextDecoder().decode(gltfBytes));
324
+ if (Array.isArray(json.buffers)) {
325
+ for (const buffer of json.buffers) {
326
+ delete buffer.uri;
327
+ if (binBytes !== undefined)
328
+ buffer.byteLength = binBytes.byteLength;
329
+ }
330
+ }
331
+ normalizeGltfImageUris(json);
332
+ return buildGlb(JSON.stringify(json), binBytes);
333
+ }
334
+ /**
335
+ * Pull every GLB out of an archive, plus the textures those models reference.
336
+ * Converts co-located `.gltf` + `.bin` pairs (Quaternius Standard packs on
337
+ * OpenGameArt, KayKit bits packs) into `.glb` so the catalog stays
338
+ * one-file-per-model; GLB wins when both formats share a basename. External
339
+ * image URIs are flattened to basenames and the matching archive images are
340
+ * returned for writing beside the models — a pulled pack renders textured, not
341
+ * as white clay (#1005).
342
+ * @internal
343
+ */
344
+ export function extractGlbs(archive) {
154
345
  const entries = unzipSync(archive, {
155
- filter: (file) => TEXTURE_ENTRY.test(file.name),
346
+ filter: boundedExtractFilter((file) => /\.(glb|gltf|bin)$/i.test(file.name) || PACK_IMAGE_EXT.test(file.name)),
156
347
  });
157
- const byRel = new Map();
348
+ const byDirBase = new Map();
349
+ const imagesByBase = new Map();
158
350
  for (const [path, bytes] of Object.entries(entries)) {
159
- const match = path.match(TEXTURE_ENTRY);
160
- if (match === null)
351
+ const parts = path.replace(/\\/g, "/").split("/");
352
+ const file = parts.pop();
353
+ if (file === undefined || file.length === 0)
354
+ continue;
355
+ if (PACK_IMAGE_EXT.test(file)) {
356
+ if (!imagesByBase.has(file))
357
+ imagesByBase.set(file, bytes);
358
+ continue;
359
+ }
360
+ const dir = parts.join("/");
361
+ // KayKit ships some files as `name.gltf.glb` — strip both suffixes for a clean id.
362
+ const base = file.replace(/\.gltf\.glb$/i, "").replace(/\.(glb|gltf|bin)$/i, "");
363
+ if (base.length === 0 || base === file)
364
+ continue;
365
+ const extMatch = file.match(/\.(glb|gltf|bin)$/i);
366
+ if (extMatch === null)
367
+ continue;
368
+ const ext = extMatch[1].toLowerCase();
369
+ const key = `${dir}\0${base}`;
370
+ const slot = byDirBase.get(key) ?? {};
371
+ if (ext === "glb")
372
+ slot.glb = bytes;
373
+ else if (ext === "gltf")
374
+ slot.gltf = bytes;
375
+ else if (ext === "bin")
376
+ slot.bin = bytes;
377
+ byDirBase.set(key, slot);
378
+ }
379
+ const byName = new Map();
380
+ const referenced = new Set();
381
+ for (const [key, slot] of byDirBase) {
382
+ const base = key.split("\0")[1];
383
+ const file = `${base}.glb`;
384
+ if (byName.has(file))
161
385
  continue;
162
- const rel = match[1];
163
- if (!byRel.has(rel))
164
- byRel.set(rel, bytes);
386
+ if (slot.glb !== undefined) {
387
+ const normalized = normalizeGlbImageUris(slot.glb);
388
+ byName.set(file, normalized.bytes);
389
+ for (const image of normalized.referenced)
390
+ referenced.add(image);
391
+ continue;
392
+ }
393
+ if (slot.gltf !== undefined) {
394
+ const packed = packGltfToGlb(slot.gltf, slot.bin);
395
+ byName.set(file, packed);
396
+ for (const image of externalGlbImages(packed))
397
+ referenced.add(image);
398
+ }
165
399
  }
166
- return Array.from(byRel, ([file, bytes]) => ({ file, bytes })).sort((a, b) => a.file.localeCompare(b.file));
400
+ const models = Array.from(byName, ([file, bytes]) => ({ file, bytes })).sort((a, b) => a.file.localeCompare(b.file));
401
+ const images = Array.from(referenced)
402
+ .filter((file) => imagesByBase.has(file))
403
+ .map((file) => ({ file, bytes: imagesByBase.get(file) }))
404
+ .sort((a, b) => a.file.localeCompare(b.file));
405
+ return { models, images };
406
+ }
407
+ /** Pulls every SVG/PNG out of a sprite/icon-pack archive, deduped by basename regardless of nesting depth.
408
+ * @internal
409
+ */
410
+ export function extractSpriteFiles(archive) {
411
+ return dedupeByBasename(archive, /\.(svg|png)$/i);
167
412
  }
413
+ /** @internal */
168
414
  export async function sha256Hex(bytes) {
169
415
  const buffer = new ArrayBuffer(bytes.byteLength);
170
416
  new Uint8Array(buffer).set(bytes);
package/dist/find.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type AssetKind = "model" | "pack" | "component" | "icon";
1
+ export type AssetKind = "model" | "pack" | "material" | "component" | "icon" | "sprite" | "spritePack";
2
2
  export type AssetMatch = {
3
3
  kind: "model";
4
4
  id: string;
@@ -10,6 +10,11 @@ export type AssetMatch = {
10
10
  source: string;
11
11
  title: string;
12
12
  categories: readonly string[];
13
+ } | {
14
+ kind: "material";
15
+ id: string;
16
+ title: string;
17
+ categories: readonly string[];
13
18
  } | {
14
19
  kind: "component";
15
20
  name: string;
@@ -18,6 +23,20 @@ export type AssetMatch = {
18
23
  } | {
19
24
  kind: "icon";
20
25
  name: string;
26
+ }
27
+ /** One already-indexed file inside a pulled sprite/icon pack, e.g. `gameicons-icons/sword`. */
28
+ | {
29
+ kind: "sprite";
30
+ id: string;
31
+ source: string;
32
+ via: "index";
33
+ }
34
+ /** A whole sprite/icon pack, not yet resolved to individual files — same role as `pack` for models. */
35
+ | {
36
+ kind: "spritePack";
37
+ source: string;
38
+ title: string;
39
+ categories: readonly string[];
21
40
  };
22
41
  export interface FindOptions {
23
42
  /** Restrict results to one kind. */
package/dist/find.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { aliases } from "./aliases.js";
2
2
  import { generatedIndex } from "./generated/index.js";
3
+ import { generatedSpriteIndex } from "./generated-sprites/index.js";
3
4
  import { keyFromFile } from "./indexGen.js";
5
+ import { materialAliases } from "./materials.js";
4
6
  import { registryCatalog } from "./registry.js";
5
7
  import { singles } from "./singles.js";
6
- import { sources } from "./sources/index.js";
8
+ import { materialSources, modelSources, spriteSources } from "./sources/index.js";
9
+ import { keyFromSpriteFile } from "./spriteIndexGen.js";
7
10
  function normalize(value) {
8
11
  return value.toLowerCase().replace(/[\s_/-]+/g, " ").trim();
9
12
  }
@@ -92,7 +95,7 @@ export function rankAssets(query, options = {}) {
92
95
  }
93
96
  }
94
97
  if (options.kind === undefined || options.kind === "pack") {
95
- for (const source of sources) {
98
+ for (const source of modelSources) {
96
99
  push(best(q, source.id, source.title, source.provider, ...source.categories), {
97
100
  kind: "pack",
98
101
  source: source.id,
@@ -101,6 +104,27 @@ export function rankAssets(query, options = {}) {
101
104
  });
102
105
  }
103
106
  }
107
+ if (options.kind === undefined || options.kind === "material") {
108
+ for (const source of materialSources) {
109
+ push(best(q, source.id, source.title, ...source.categories), {
110
+ kind: "material",
111
+ id: source.id,
112
+ title: source.title,
113
+ categories: source.categories,
114
+ });
115
+ }
116
+ for (const alias of materialAliases) {
117
+ const target = materialSources.find((source) => source.id === alias.target);
118
+ if (target === undefined)
119
+ continue;
120
+ push(best(q, alias.key, alias.target), {
121
+ kind: "material",
122
+ id: target.id,
123
+ title: target.title,
124
+ categories: target.categories,
125
+ });
126
+ }
127
+ }
104
128
  if (options.kind === undefined || options.kind === "component") {
105
129
  for (const component of registryCatalog.components) {
106
130
  push(best(q, component.name, component.title, component.description), {
@@ -116,6 +140,26 @@ export function rankAssets(query, options = {}) {
116
140
  push(best(q, icon), { kind: "icon", name: icon });
117
141
  }
118
142
  }
143
+ if (options.kind === undefined || options.kind === "sprite") {
144
+ for (const entry of generatedSpriteIndex) {
145
+ push(best(q, entry.id, keyFromSpriteFile(entry.file), ...entry.categories), {
146
+ kind: "sprite",
147
+ id: entry.id,
148
+ source: entry.source,
149
+ via: "index",
150
+ });
151
+ }
152
+ }
153
+ if (options.kind === undefined || options.kind === "spritePack") {
154
+ for (const source of spriteSources) {
155
+ push(best(q, source.id, source.title, source.provider, ...source.categories), {
156
+ kind: "spritePack",
157
+ source: source.id,
158
+ title: source.title,
159
+ categories: source.categories,
160
+ });
161
+ }
162
+ }
119
163
  ranked.sort((a, b) => b.score - a.score || matchKey(a.match).localeCompare(matchKey(b.match)));
120
164
  return dedupe(ranked).slice(0, options.limit ?? 12);
121
165
  }
@@ -125,6 +169,12 @@ function matchKey(match) {
125
169
  return `model:${match.id}`;
126
170
  case "pack":
127
171
  return `pack:${match.source}`;
172
+ case "material":
173
+ return `material:${match.id}`;
174
+ case "sprite":
175
+ return `sprite:${match.id}`;
176
+ case "spritePack":
177
+ return `spritePack:${match.source}`;
128
178
  default:
129
179
  return `${match.kind}:${match.name}`;
130
180
  }
@@ -1,9 +1,21 @@
1
- import s_kenney_blaster from "./kenney-blaster.json" with { type: "json" };
2
- import s_kenney_nature from "./kenney-nature.json" with { type: "json" };
3
- import s_kenney_space from "./kenney-space.json" with { type: "json" };
1
+ import s_kaykit_adventurers from "./kaykit-adventurers.json" with { type: "json" };
2
+ import s_kaykit_city_builder from "./kaykit-city-builder.json" with { type: "json" };
3
+ import s_kaykit_dungeon from "./kaykit-dungeon.json" with { type: "json" };
4
+ import s_kaykit_furniture from "./kaykit-furniture.json" with { type: "json" };
5
+ import s_kaykit_skeletons from "./kaykit-skeletons.json" with { type: "json" };
6
+ import s_kaykit_space_base from "./kaykit-space-base.json" with { type: "json" };
7
+ import s_quaternius_medieval_village from "./quaternius-medieval-village.json" with { type: "json" };
8
+ import s_quaternius_modular_scifi from "./quaternius-modular-scifi.json" with { type: "json" };
9
+ import s_quaternius_stylized_nature from "./quaternius-stylized-nature.json" with { type: "json" };
4
10
  export const generatedBySource = {
5
- "kenney-blaster": s_kenney_blaster,
6
- "kenney-nature": s_kenney_nature,
7
- "kenney-space": s_kenney_space,
11
+ "kaykit-adventurers": s_kaykit_adventurers,
12
+ "kaykit-city-builder": s_kaykit_city_builder,
13
+ "kaykit-dungeon": s_kaykit_dungeon,
14
+ "kaykit-furniture": s_kaykit_furniture,
15
+ "kaykit-skeletons": s_kaykit_skeletons,
16
+ "kaykit-space-base": s_kaykit_space_base,
17
+ "quaternius-medieval-village": s_quaternius_medieval_village,
18
+ "quaternius-modular-scifi": s_quaternius_modular_scifi,
19
+ "quaternius-stylized-nature": s_quaternius_stylized_nature,
8
20
  };
9
21
  export const generatedIndex = Object.values(generatedBySource).flat();