pixelkiln 0.50.0 → 0.51.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.
package/dist/cli.js CHANGED
@@ -707,7 +707,7 @@ import path2 from "path";
707
707
  // src/types.ts
708
708
  import { z } from "zod";
709
709
  var MediaTypeSchema = z.enum(["image/png", "image/gif"]);
710
- var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character"]);
710
+ var GeneratorSchema = z.enum(["1dir", "map", "pixflux", "tiles", "animation", "frames", "character", "terrain"]);
711
711
  var GridConfidenceSchema = z.enum(["low", "medium", "high"]);
712
712
  var RevisionModeSchema = z.enum(["image-to-image", "inpaint", "outpaint"]);
713
713
  function tileVariationCount(descriptions) {
@@ -740,6 +740,14 @@ function tilesCost(tileSize, variations) {
740
740
  if (px <= 2048) return 25;
741
741
  return 40;
742
742
  }
743
+ function terrainTileCount(transitionSize) {
744
+ return transitionSize === 1 ? 25 : 16;
745
+ }
746
+ function parseTerrainDescriptions(prompt) {
747
+ const parts = prompt.split(/\d+\s*\)\s*\./).map((part) => part.trim()).filter(Boolean);
748
+ if (parts.length < 2) return null;
749
+ return { lower: parts[0], upper: parts[1], transition: parts[2] };
750
+ }
743
751
  var StyleImageSchema = z.object({
744
752
  /** Path to a PNG/JPEG, relative to the manifest; the active provider validates limits. */
745
753
  path: z.string()
@@ -1019,6 +1027,48 @@ var StyleObjectSchema = z.object({
1019
1027
  * rather than inheriting.
1020
1028
  */
1021
1029
  outlineMode: z.enum(["outline", "segmentation"]).optional(),
1030
+ /**
1031
+ * `terrain` generator only. Edge length of one tile in pixels. 16 or 32
1032
+ * work in both modes; 64 needs `terrainMode: "pro"`. The API default is
1033
+ * 16. See `parseTerrainDescriptions` for how an asset's `prompt` becomes
1034
+ * the lower/upper/transition terrain descriptions this endpoint wants.
1035
+ */
1036
+ terrainTileSize: z.union([z.literal(16), z.literal(32), z.literal(64)]).optional(),
1037
+ /**
1038
+ * `terrain` generator only. `standard` is the classic Wang tileset
1039
+ * pipeline; `pro` is a newer corner-pair pipeline with its own shape
1040
+ * controls (`terrainSpreadX`, `terrainSlopeSize`, `terrainRaggedness`)
1041
+ * in place of `terrainShapeStyle`. The API default is `standard`.
1042
+ */
1043
+ terrainMode: z.enum(["standard", "pro"]).optional(),
1044
+ /**
1045
+ * `terrain` generator only, `terrainMode: "standard"`. Procedural
1046
+ * boundary geometry: `square` or `round`, 16px or 32px tiles only.
1047
+ * Rejected together with `terrainMode: "pro"`, whose own shape controls
1048
+ * are `terrainSpreadX`/`terrainSlopeSize`/`terrainRaggedness`.
1049
+ */
1050
+ terrainShapeStyle: z.enum(["square", "round"]).optional(),
1051
+ /** `terrain` generator only, `terrainMode: "pro"`. Boundary spread
1052
+ * between terrains (0 = steep, 1 = gradual). API default 0.5. */
1053
+ terrainSpreadX: z.number().min(0).max(1).optional(),
1054
+ /** `terrain` generator only, `terrainMode: "pro"`. Slope on the N/W/E
1055
+ * sides as a fraction of wall height. API default 0. */
1056
+ terrainSlopeSize: z.number().min(0).max(1).optional(),
1057
+ /** `terrain` generator only, `terrainMode: "pro"`. Terrain boundary
1058
+ * noise (0 = smooth, 1 = rough). API default 0. */
1059
+ terrainRaggedness: z.number().min(0).max(1).optional(),
1060
+ /**
1061
+ * `terrain` generator only. Visual height of the step between lower and
1062
+ * upper terrain. Without `terrainShapeStyle`, only 0, 0.25, 0.5, or 1 are
1063
+ * accepted (1 switches to the 25-tile cliff layout, where corners take a
1064
+ * third "transition" value); with it, any value from 0 to 1 works, but
1065
+ * above 0.5 switches to an extended 32-tile layout this adapter does not
1066
+ * model. API default 0.
1067
+ */
1068
+ terrainTransitionSize: z.number().min(0).max(1).optional(),
1069
+ /** `terrain` generator only. Camera angle; the API default is `high
1070
+ * top-down`. */
1071
+ terrainView: z.enum(["low top-down", "high top-down"]).optional(),
1022
1072
  /**
1023
1073
  * `pixflux` only. Whether to strip the generated background.
1024
1074
  *
@@ -1132,7 +1182,7 @@ var StyleSchema = StyleObjectSchema.refine((s) => !(s.tileFeature && s.styleImag
1132
1182
  message: "tileFeature and styleImages cannot be combined; a connectable set derives its own tile geometry, so remove one or the other",
1133
1183
  path: ["tileFeature"]
1134
1184
  }).superRefine((style, ctx) => {
1135
- if (style.quality && (style.generator === "tiles" || style.generator === "animation")) {
1185
+ if (style.quality && (style.generator === "tiles" || style.generator === "terrain" || style.generator === "animation")) {
1136
1186
  ctx.addIssue({
1137
1187
  code: z.ZodIssueCode.custom,
1138
1188
  message: "quality profiles currently support single-image generators only",
@@ -5157,6 +5207,11 @@ var MapSubmitSchema = z2.object({
5157
5207
  object_id: z2.string().min(1),
5158
5208
  status: z2.string().default("processing")
5159
5209
  }).passthrough();
5210
+ var UsageSchema = z2.object({
5211
+ type: z2.string().optional(),
5212
+ usd: z2.number().nullable().optional(),
5213
+ generations: z2.number().nullable().optional()
5214
+ }).passthrough().nullable().optional();
5160
5215
  var TilesSubmitSchema = z2.object({
5161
5216
  tile_id: z2.string().min(1),
5162
5217
  background_job_id: z2.string().min(1),
@@ -5167,6 +5222,32 @@ var TilesProSchema = z2.object({
5167
5222
  kind: z2.string().nullable().default(null),
5168
5223
  tile_rules: z2.record(z2.unknown()).nullable().optional()
5169
5224
  }).passthrough();
5225
+ var TilesetSubmitSchema = z2.object({
5226
+ tileset_id: z2.string().min(1),
5227
+ background_job_id: z2.string().min(1),
5228
+ status: z2.literal("processing").default("processing")
5229
+ }).passthrough();
5230
+ var TilesetTileSchema = z2.object({
5231
+ id: z2.string().min(1),
5232
+ name: z2.string(),
5233
+ image: z2.object({ base64: z2.string().min(1), format: z2.string().default("png") }).passthrough(),
5234
+ corners: z2.object({ NW: z2.string(), NE: z2.string(), SW: z2.string(), SE: z2.string() }).passthrough(),
5235
+ pattern_4x4: z2.object({
5236
+ row_0: z2.array(z2.number()),
5237
+ row_1: z2.array(z2.number()),
5238
+ row_2: z2.array(z2.number()),
5239
+ row_3: z2.array(z2.number())
5240
+ }).passthrough()
5241
+ }).passthrough();
5242
+ var TilesetGetSchema = z2.object({
5243
+ tileset: z2.object({
5244
+ total_tiles: z2.number().int().min(1),
5245
+ tile_size: z2.object({ width: z2.number().int().positive(), height: z2.number().int().positive() }).passthrough(),
5246
+ terrain_types: z2.array(z2.string()),
5247
+ tiles: z2.array(TilesetTileSchema).min(1)
5248
+ }).passthrough(),
5249
+ usage: UsageSchema
5250
+ }).passthrough();
5170
5251
  var RevisionJobSubmitSchema = z2.object({
5171
5252
  background_job_id: z2.string().min(1),
5172
5253
  status: z2.string().default("processing")
@@ -5198,11 +5279,6 @@ var MapObjectSchema = z2.object({
5198
5279
  var ObjectListSchema = z2.object({ objects: z2.array(PixelLabObjectSchema), total: z2.number().int().min(0) }).passthrough();
5199
5280
  var PixfluxResponseSchema = z2.object({ image: z2.object({ base64: z2.string().min(1) }).passthrough(), usage: z2.unknown().optional() }).passthrough();
5200
5281
  var SelectFramesSchema = z2.object({ created_object_ids: z2.array(z2.string()) }).passthrough();
5201
- var UsageSchema = z2.object({
5202
- type: z2.string().optional(),
5203
- usd: z2.number().nullable().optional(),
5204
- generations: z2.number().nullable().optional()
5205
- }).passthrough().nullable().optional();
5206
5282
  var CharacterSubmitSchema = z2.object({
5207
5283
  background_job_id: z2.string().min(1),
5208
5284
  character_id: z2.string().min(1),
@@ -5418,6 +5494,45 @@ var PixelLabClient = class {
5418
5494
  "get tiles"
5419
5495
  );
5420
5496
  }
5497
+ /**
5498
+ * `/create-tileset`: two named terrain levels (`lower`/`upper`) and the
5499
+ * transition between them, laid out as a Wang corner set. Unlike
5500
+ * `/create-tiles-pro`, the descriptions are separate fields the API itself
5501
+ * places on the terrain vertex grid, not one prompt it splits by number.
5502
+ */
5503
+ async createTileset(args) {
5504
+ const body = {
5505
+ lower_description: args.lowerDescription,
5506
+ upper_description: args.upperDescription
5507
+ };
5508
+ if (args.transitionDescription) body.transition_description = args.transitionDescription;
5509
+ if (args.tileSize != null) body.tile_size = { width: args.tileSize, height: args.tileSize };
5510
+ if (args.mode) body.mode = args.mode;
5511
+ if (args.shapeStyle) body.shape_style = args.shapeStyle;
5512
+ if (args.spreadX != null) body.spread_x = args.spreadX;
5513
+ if (args.slopeSize != null) body.slope_size = args.slopeSize;
5514
+ if (args.raggedness != null) body.raggedness = args.raggedness;
5515
+ if (args.transitionSize != null) body.transition_size = args.transitionSize;
5516
+ if (args.view) body.view = args.view;
5517
+ if (args.outline) body.outline = args.outline;
5518
+ if (args.shading) body.shading = args.shading;
5519
+ if (args.detail) body.detail = args.detail;
5520
+ if (args.seed != null) body.seed = args.seed;
5521
+ return validateResponse(
5522
+ TilesetSubmitSchema,
5523
+ await this.request("/create-tileset", { method: "POST", body: JSON.stringify(body) }),
5524
+ "create tileset"
5525
+ );
5526
+ }
5527
+ /** Throws PixelLabError(423) while the set is still drawing; see Tileset. */
5528
+ async getTileset(tilesetId) {
5529
+ const res = await validateResponse(
5530
+ TilesetGetSchema,
5531
+ await this.request(`/tilesets/${tilesetId}`),
5532
+ "get tileset"
5533
+ );
5534
+ return { tileset: res.tileset, usage: res.usage ?? null };
5535
+ }
5421
5536
  /**
5422
5537
  * Synchronous single-image generation. Returns the PNG inline rather than a
5423
5538
  * job id, and is the only endpoint that honours a forced palette;
@@ -5856,7 +5971,7 @@ var PixelLabProvider = class _PixelLabProvider {
5856
5971
  return new _PixelLabProvider(new PixelLabClient("download-only"));
5857
5972
  }
5858
5973
  supports(generator) {
5859
- return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "character";
5974
+ return generator === "1dir" || generator === "map" || generator === "pixflux" || generator === "tiles" || generator === "terrain" || generator === "character";
5860
5975
  }
5861
5976
  /**
5862
5977
  * `inpaint` (`/inpaint-v3`, a mask) and `image-to-image` (`/edit-images-v2`,
@@ -5892,7 +6007,7 @@ var PixelLabProvider = class _PixelLabProvider {
5892
6007
  const height = spec.revision.sourceHeight ?? spec.height;
5893
6008
  return { unit: "generations", amount: generationCost(width, height, "1dir"), candidates: 1 };
5894
6009
  }
5895
- if (spec.generator === "tiles") {
6010
+ if (spec.generator === "tiles" || spec.generator === "terrain") {
5896
6011
  return { unit: "generations", amount: spec.cost, candidates: spec.candidates };
5897
6012
  }
5898
6013
  if (spec.generator === "character") {
@@ -5936,10 +6051,55 @@ var PixelLabProvider = class _PixelLabProvider {
5936
6051
  "high detail"
5937
6052
  ]);
5938
6053
  }
6054
+ if (spec.generator === "terrain") {
6055
+ if (spec.outline) {
6056
+ requirePixelLabOption("outline", spec.outline, [
6057
+ "single color black outline",
6058
+ "single color outline",
6059
+ "selective outline",
6060
+ "lineless"
6061
+ ]);
6062
+ }
6063
+ if (spec.shading) {
6064
+ requirePixelLabOption("shading", spec.shading, [
6065
+ "flat shading",
6066
+ "basic shading",
6067
+ "medium shading",
6068
+ "detailed shading",
6069
+ "highly detailed shading"
6070
+ ]);
6071
+ }
6072
+ if (spec.detail) {
6073
+ requirePixelLabOption("detail", spec.detail, ["low detail", "medium detail", "highly detailed"]);
6074
+ }
6075
+ if (spec.terrainShapeStyle && spec.terrainMode === "pro") {
6076
+ throw new Error(
6077
+ "PixelLab terrain: terrainShapeStyle is standard-mode only; pro's own shape controls are terrainSpreadX/terrainSlopeSize/terrainRaggedness"
6078
+ );
6079
+ }
6080
+ if (spec.terrainTileSize === 64 && spec.terrainMode !== "pro") {
6081
+ throw new Error('PixelLab terrain: a 64px tile needs terrainMode: "pro"');
6082
+ }
6083
+ if (spec.terrainShapeStyle && spec.terrainTransitionSize != null && spec.terrainTransitionSize > 0.5) {
6084
+ throw new Error(
6085
+ "PixelLab terrain: terrainShapeStyle with terrainTransitionSize above 0.5 uses an extended 32-tile layout pixelkiln does not model; use 0, 0.25, or 0.5"
6086
+ );
6087
+ }
6088
+ if (!spec.terrainShapeStyle && spec.terrainTransitionSize != null && ![0, 0.25, 0.5, 1].includes(spec.terrainTransitionSize)) {
6089
+ throw new Error(
6090
+ "PixelLab terrain: terrainTransitionSize must be 0, 0.25, 0.5, or 1 unless terrainShapeStyle is set"
6091
+ );
6092
+ }
6093
+ }
5939
6094
  if (spec.generator === "character") this.validateCharacter(spec, styleImages);
5940
6095
  if ((spec.generator === "map" || spec.generator === "pixflux") && styleImages.length) {
5941
6096
  throw new Error(`PixelLab ${spec.generator} does not support style images`);
5942
6097
  }
6098
+ if (spec.generator === "terrain" && styleImages.length) {
6099
+ throw new Error(
6100
+ "PixelLab terrain does not support style images yet; use color_image/reference images directly against /create-tileset if this becomes a real need"
6101
+ );
6102
+ }
5943
6103
  for (const image of styleImages) {
5944
6104
  if (image.width > 256 || image.height > 256) {
5945
6105
  throw new Error(
@@ -6302,6 +6462,26 @@ var PixelLabProvider = class _PixelLabProvider {
6302
6462
  });
6303
6463
  return { jobId: res2.tile_id, metadata: { backgroundJobId: res2.background_job_id } };
6304
6464
  }
6465
+ if (spec.generator === "terrain") {
6466
+ const res2 = await this.client.createTileset({
6467
+ lowerDescription: spec.terrainLowerDescription,
6468
+ upperDescription: spec.terrainUpperDescription,
6469
+ transitionDescription: spec.terrainTransitionDescription,
6470
+ tileSize: spec.terrainTileSize,
6471
+ mode: spec.terrainMode,
6472
+ shapeStyle: spec.terrainShapeStyle,
6473
+ spreadX: spec.terrainSpreadX,
6474
+ slopeSize: spec.terrainSlopeSize,
6475
+ raggedness: spec.terrainRaggedness,
6476
+ transitionSize: spec.terrainTransitionSize,
6477
+ view: spec.terrainView,
6478
+ outline: spec.outline,
6479
+ shading: spec.shading,
6480
+ detail: spec.detail,
6481
+ seed: spec.seed
6482
+ });
6483
+ return { jobId: res2.tileset_id, metadata: { backgroundJobId: res2.background_job_id } };
6484
+ }
6305
6485
  if (spec.generator === "1dir") {
6306
6486
  const res2 = await this.client.create1Direction({
6307
6487
  description: spec.prompt,
@@ -6407,6 +6587,7 @@ var PixelLabProvider = class _PixelLabProvider {
6407
6587
  }
6408
6588
  if (generator === "map") return this.pollMap(jobId, context);
6409
6589
  if (generator === "tiles") return this.pollTiles(jobId, Boolean(context?.tileFeature), context);
6590
+ if (generator === "terrain") return this.pollTerrain(jobId, context);
6410
6591
  if (generator === "character") return this.pollCharacter(jobId, context);
6411
6592
  const backgroundJobId = context?.metadata?.backgroundJobId;
6412
6593
  const obj = await this.client.getObject(jobId);
@@ -6545,6 +6726,43 @@ var PixelLabProvider = class _PixelLabProvider {
6545
6726
  throw err;
6546
6727
  }
6547
6728
  }
6729
+ /**
6730
+ * `/create-tileset` reports progress the same way `tiles` does (423 while
6731
+ * drawing, 200 once finished), but each tile's image comes back embedded
6732
+ * as base64 rather than a `storage_urls` link, so it is decoded straight
6733
+ * to a cache file the same way a revision result is (`pollRevision`). A
6734
+ * terrain set is always a connectable Wang tileset, never independent
6735
+ * candidates to review, so this goes straight to "ready" like a connectable
6736
+ * `tiles` set does.
6737
+ */
6738
+ async pollTerrain(tilesetId, context) {
6739
+ try {
6740
+ const { tileset, usage } = await this.client.getTileset(tilesetId);
6741
+ const backgroundJobId = context?.metadata?.backgroundJobId;
6742
+ const billed = billedFromUsage(usage) ?? await this.billedForJob(backgroundJobId);
6743
+ const sources = [];
6744
+ const terrainTiles = [];
6745
+ for (const [index, tile] of tileset.tiles.entries()) {
6746
+ const slug = tile.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "tile";
6747
+ const role = `tile-${String(index).padStart(2, "0")}-${slug}`;
6748
+ const file = path12.join(_PixelLabProvider.cacheDir(), `${tilesetId}-${index}.png`);
6749
+ writeFileSync(file, Buffer.from(tile.image.base64, "base64"));
6750
+ sources.push({ url: `file://${file}`, role });
6751
+ terrainTiles.push({ role, corners: tile.corners, pattern4x4: tile.pattern_4x4 });
6752
+ }
6753
+ return {
6754
+ status: "ready",
6755
+ objectId: tilesetId,
6756
+ sourceUrl: sources[0]?.url ?? null,
6757
+ sources,
6758
+ metadata: { terrainTypes: tileset.terrain_types, terrainTiles },
6759
+ billed
6760
+ };
6761
+ } catch (err) {
6762
+ if (err instanceof PixelLabError && err.status === 423) return { status: "processing" };
6763
+ throw err;
6764
+ }
6765
+ }
6548
6766
  async selectCandidate(jobId, index, commonTag, generator) {
6549
6767
  if (generator === "tiles") {
6550
6768
  const set = await this.client.getTilesPro(jobId);
@@ -7895,6 +8113,10 @@ async function resolveSpecs(loaded, filter) {
7895
8113
  size = asset.state?.canvas ? Math.max(asset.state.canvas.width, asset.state.canvas.height) : asset.size ?? style.size ?? 64;
7896
8114
  width = asset.state?.canvas?.width ?? size;
7897
8115
  height = asset.state?.canvas?.height ?? size;
8116
+ } else if (generator === "terrain") {
8117
+ size = style.terrainTileSize ?? 16;
8118
+ width = size;
8119
+ height = size;
7898
8120
  } else {
7899
8121
  width = asset.width ?? style.size ?? 64;
7900
8122
  height = asset.height ?? style.size ?? 64;
@@ -7907,12 +8129,33 @@ async function resolveSpecs(loaded, filter) {
7907
8129
  }
7908
8130
  const characterKind = asset.animation ? "animation" : asset.state ? "state" : "base";
7909
8131
  const subject = asset.promptByStyle[styleId] ?? asset.prompt ?? "";
7910
- const prompt = generator === "character" && characterKind !== "base" ? subject.trim() : [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8132
+ let terrainLowerDescription;
8133
+ let terrainUpperDescription;
8134
+ let terrainTransitionDescription;
8135
+ let prompt;
8136
+ if (generator === "terrain") {
8137
+ const parsed = parseTerrainDescriptions(subject);
8138
+ if (!parsed) {
8139
+ throw new Error(
8140
+ `assets.${assetId}: terrain needs "1). <lower terrain> 2). <upper terrain>" in its prompt, optionally followed by "3). <transition>"`
8141
+ );
8142
+ }
8143
+ const wrap = (text) => [style.promptPrefix, text, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8144
+ terrainLowerDescription = wrap(parsed.lower);
8145
+ terrainUpperDescription = wrap(parsed.upper);
8146
+ terrainTransitionDescription = parsed.transition ? wrap(parsed.transition) : void 0;
8147
+ prompt = subject.trim();
8148
+ } else if (generator === "character" && characterKind !== "base") {
8149
+ prompt = subject.trim();
8150
+ } else {
8151
+ prompt = [style.promptPrefix, subject, style.promptSuffix].map((p) => p.trim()).filter(Boolean).join(", ");
8152
+ }
7911
8153
  const relFile = asset.file ?? path13.join(asset.category ?? "", `${assetId}.png`);
7912
8154
  const outFile = path13.resolve(root, style.outDir, relFile);
7913
8155
  const qualityOutFile = style.quality ? path13.resolve(root, style.quality.outDir, pngPath(relFile)) : void 0;
7914
8156
  const tileSize = generator === "tiles" ? size : style.tileSize ?? 32;
7915
8157
  const tileVariations = tileFeatureOutputCount(generator === "tiles" ? style.tileFeature : void 0) ?? tileVariationCount(countNumberedDescriptions(prompt));
8158
+ const terrainTiles = generator === "terrain" ? terrainTileCount(style.terrainTransitionSize) : 0;
7916
8159
  const base = {
7917
8160
  styleId,
7918
8161
  assetId,
@@ -7949,10 +8192,21 @@ async function resolveSpecs(loaded, filter) {
7949
8192
  buildingFloor2Description: generator === "tiles" ? style.buildingFloor2Description : void 0,
7950
8193
  buildingWallAngle: generator === "tiles" ? style.buildingWallAngle : void 0,
7951
8194
  outlineMode: generator === "tiles" ? style.outlineMode : void 0,
8195
+ terrainLowerDescription: generator === "terrain" ? terrainLowerDescription : void 0,
8196
+ terrainUpperDescription: generator === "terrain" ? terrainUpperDescription : void 0,
8197
+ terrainTransitionDescription: generator === "terrain" ? terrainTransitionDescription : void 0,
8198
+ terrainTileSize: generator === "terrain" ? size : void 0,
8199
+ terrainMode: generator === "terrain" ? style.terrainMode : void 0,
8200
+ terrainShapeStyle: generator === "terrain" ? style.terrainShapeStyle : void 0,
8201
+ terrainSpreadX: generator === "terrain" ? style.terrainSpreadX : void 0,
8202
+ terrainSlopeSize: generator === "terrain" ? style.terrainSlopeSize : void 0,
8203
+ terrainRaggedness: generator === "terrain" ? style.terrainRaggedness : void 0,
8204
+ terrainTransitionSize: generator === "terrain" ? style.terrainTransitionSize : void 0,
8205
+ terrainView: generator === "terrain" ? style.terrainView : void 0,
7952
8206
  ...generator === "character" ? { character: await resolveCharacterShape(asset, style, characterKind, { root, load: loadStyleImage }) } : {},
7953
- cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generationCost(width, height, generator),
8207
+ cost: generator === "tiles" ? tilesCost(tileSize, tileVariations) : generator === "terrain" ? tilesCost(size, terrainTiles) : generationCost(width, height, generator),
7954
8208
  costUnit: "generations",
7955
- candidates: generator === "tiles" ? tileVariations : generator === "1dir" ? candidateCount(size) : 1
8209
+ candidates: generator === "tiles" ? tileVariations : generator === "terrain" ? terrainTiles : generator === "1dir" ? candidateCount(size) : 1
7956
8210
  };
7957
8211
  const tags = [
7958
8212
  .../* @__PURE__ */ new Set([
@@ -8000,7 +8254,7 @@ async function resolveSpecs(loaded, filter) {
8000
8254
  if (asset.mirror) {
8001
8255
  if (asset.mirror === assetId) throw new Error(`assets.${assetId}: an asset cannot mirror itself`);
8002
8256
  const sourceSpec = await finalize(asset.mirror);
8003
- if (sourceSpec.generator === "tiles") {
8257
+ if (sourceSpec.generator === "tiles" || sourceSpec.generator === "terrain") {
8004
8258
  throw new Error(`assets.${assetId}: a tile set cannot be mirrored; its edges carry meaning`);
8005
8259
  }
8006
8260
  if (sourceSpec.character?.kind === "animation") {