@ai-game-assets/phaser 0.6.0 → 0.7.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 (44) hide show
  1. package/dist/animations.d.ts +2 -0
  2. package/dist/animations.d.ts.map +1 -1
  3. package/dist/animations.js +4 -3
  4. package/dist/animations.js.map +1 -1
  5. package/dist/debug-client.d.ts +52 -2
  6. package/dist/debug-client.d.ts.map +1 -1
  7. package/dist/debug-client.js +66 -0
  8. package/dist/debug-client.js.map +1 -1
  9. package/dist/designer-support.d.ts +28 -3
  10. package/dist/designer-support.d.ts.map +1 -1
  11. package/dist/designer-support.js +552 -29
  12. package/dist/designer-support.js.map +1 -1
  13. package/dist/designer.d.ts +19 -2
  14. package/dist/designer.d.ts.map +1 -1
  15. package/dist/designer.js +1546 -134
  16. package/dist/designer.js.map +1 -1
  17. package/dist/first-drafts.js +21 -9
  18. package/dist/first-drafts.js.map +1 -1
  19. package/dist/index.d.ts +7 -3
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/keys.d.ts +2 -0
  24. package/dist/keys.d.ts.map +1 -1
  25. package/dist/keys.js +6 -0
  26. package/dist/keys.js.map +1 -1
  27. package/dist/loader.d.ts.map +1 -1
  28. package/dist/loader.js +50 -3
  29. package/dist/loader.js.map +1 -1
  30. package/dist/phaser-types.d.ts +1 -0
  31. package/dist/phaser-types.d.ts.map +1 -1
  32. package/dist/runtime.d.ts +30 -2
  33. package/dist/runtime.d.ts.map +1 -1
  34. package/dist/runtime.js +235 -8
  35. package/dist/runtime.js.map +1 -1
  36. package/dist/tileset-dialog.d.ts +121 -0
  37. package/dist/tileset-dialog.d.ts.map +1 -0
  38. package/dist/tileset-dialog.js +1594 -0
  39. package/dist/tileset-dialog.js.map +1 -0
  40. package/dist/tilesets.d.ts +13 -0
  41. package/dist/tilesets.d.ts.map +1 -0
  42. package/dist/tilesets.js +60 -0
  43. package/dist/tilesets.js.map +1 -0
  44. package/package.json +3 -2
@@ -1,3 +1,4 @@
1
+ import { aiTextureKey } from "./keys.js";
1
2
  const assetFormatOptions = [
2
3
  { value: "png", label: "PNG" },
3
4
  { value: "jpg", label: "JPEG" },
@@ -69,6 +70,19 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
69
70
  const promptInput = document.createElement("textarea");
70
71
  promptInput.className = "ai-game-assets-designer__prompt";
71
72
  promptInput.rows = 6;
73
+ const promptField = labelWrap("Prompt", promptInput);
74
+ const tilesetPromptsField = document.createElement("section");
75
+ tilesetPromptsField.className = "ai-game-assets-designer__tile-prompts";
76
+ tilesetPromptsField.hidden = true;
77
+ const tilesetPromptsTitle = document.createElement("div");
78
+ tilesetPromptsTitle.className = "ai-game-assets-designer__tile-prompts-title";
79
+ tilesetPromptsTitle.textContent = "Tile prompts";
80
+ const tilesetPromptsHint = document.createElement("div");
81
+ tilesetPromptsHint.className = "ai-game-assets-designer__tile-prompts-hint";
82
+ tilesetPromptsHint.textContent = "Describe each tile in exact sheet order.";
83
+ const tilesetPromptsList = document.createElement("div");
84
+ tilesetPromptsList.className = "ai-game-assets-designer__tile-prompts-list";
85
+ tilesetPromptsField.append(tilesetPromptsTitle, tilesetPromptsHint, tilesetPromptsList);
72
86
  const widthInput = numericInput();
73
87
  const heightInput = numericInput();
74
88
  const frameCountInput = numericInput();
@@ -98,7 +112,9 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
98
112
  }
99
113
  const dimensionGrid = document.createElement("div");
100
114
  dimensionGrid.className = "ai-game-assets-designer__dimensions";
101
- dimensionGrid.append(labelWrap("Width", widthInput), labelWrap("Height", heightInput));
115
+ const widthField = labelWrap("Width", widthInput);
116
+ const heightField = labelWrap("Height", heightInput);
117
+ dimensionGrid.append(widthField, heightField);
102
118
  const frameCountField = labelWrap("Frames", frameCountInput);
103
119
  const formatField = labelWrap("Format", formatSelect);
104
120
  const audioFormatField = labelWrap("Audio format", audioFormatSelect);
@@ -135,6 +151,10 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
135
151
  const regenerateButton = document.createElement("button");
136
152
  regenerateButton.type = "button";
137
153
  regenerateButton.textContent = "Regenerate";
154
+ const mixTilesetButton = document.createElement("button");
155
+ mixTilesetButton.type = "button";
156
+ mixTilesetButton.textContent = "Mix tileset";
157
+ mixTilesetButton.hidden = true;
138
158
  const uploadButton = document.createElement("button");
139
159
  uploadButton.type = "button";
140
160
  uploadButton.textContent = "Upload...";
@@ -148,19 +168,23 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
148
168
  promoteButton.type = "button";
149
169
  promoteButton.textContent = "Promote";
150
170
  promoteButton.disabled = true;
171
+ const promoteAllButton = document.createElement("button");
172
+ promoteAllButton.type = "button";
173
+ promoteAllButton.textContent = "Promote all";
174
+ promoteAllButton.disabled = true;
151
175
  const restartButton = document.createElement("button");
152
176
  restartButton.type = "button";
153
177
  restartButton.textContent = "Restart";
154
178
  const actions = document.createElement("div");
155
179
  actions.className = "ai-game-assets-designer__actions";
156
- actions.append(regenerateButton, uploadButton, deriveButton, versionsButton, promoteButton, restartButton);
180
+ actions.append(regenerateButton, mixTilesetButton, uploadButton, deriveButton, versionsButton, promoteButton, promoteAllButton, restartButton);
157
181
  const versionLabel = document.createElement("div");
158
182
  versionLabel.className = "ai-game-assets-designer__meta";
159
183
  const optionsGrid = document.createElement("div");
160
184
  optionsGrid.className = "ai-game-assets-designer__options";
161
185
  const status = document.createElement("div");
162
186
  status.className = "ai-game-assets-designer__status";
163
- panel.append(header, targetField, labelWrap("Asset", assetBrowser), targetVariantLabel, animationField, dimensionGrid, frameCountField, formatField, audioFormatField, audioDurationField, audioLoopField, voiceTextField, labelWrap("Current", currentPreview), labelWrap("Prompt", promptInput), actions, versionLabel, optionsGrid, status);
187
+ panel.append(header, targetField, labelWrap("Asset", assetBrowser), targetVariantLabel, animationField, dimensionGrid, frameCountField, formatField, audioFormatField, audioDurationField, audioLoopField, voiceTextField, labelWrap("Current", currentPreview), promptField, tilesetPromptsField, actions, versionLabel, optionsGrid, status);
164
188
  root.append(toggle, panel);
165
189
  return {
166
190
  root,
@@ -176,7 +200,9 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
176
200
  animationSelect,
177
201
  animationField,
178
202
  widthInput,
203
+ widthField,
179
204
  heightInput,
205
+ heightField,
180
206
  dimensionGrid,
181
207
  frameCountInput,
182
208
  frameCountField,
@@ -191,6 +217,9 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
191
217
  voiceTextInput,
192
218
  voiceTextField,
193
219
  promptInput,
220
+ promptField,
221
+ tilesetPromptsField,
222
+ tilesetPromptsList,
194
223
  currentImage,
195
224
  currentAudio,
196
225
  currentAnimation,
@@ -201,8 +230,10 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
201
230
  uploadButton,
202
231
  deriveButton,
203
232
  regenerateButton,
233
+ mixTilesetButton,
204
234
  versionsButton,
205
235
  promoteButton,
236
+ promoteAllButton,
206
237
  restartButton,
207
238
  versionLabel,
208
239
  options: optionsGrid,
@@ -406,6 +437,8 @@ export function renderOptions(options) {
406
437
  options.elements.options.append(card);
407
438
  }
408
439
  }
440
+ const pendingImagePreviews = new WeakMap();
441
+ let promotedTextureInstallId = 0;
409
442
  export function previewOption(options) {
410
443
  const textureKey = `ai-preview:${options.assetId}:${options.option.index}:${Date.now()}`;
411
444
  previewImageSource({
@@ -425,25 +458,80 @@ export function previewCurrentAsset(options) {
425
458
  textureKey
426
459
  });
427
460
  }
461
+ export function installPromotedImageTexture(options) {
462
+ const textureKey = aiTextureKey({ assetId: options.assetId });
463
+ promotedTextureInstallId += 1;
464
+ const requestKey = `promote:${options.assetId}:${promotedTextureInstallId}`;
465
+ return new Promise((resolve, reject) => {
466
+ previewImageSource({
467
+ ...options,
468
+ textureKey,
469
+ requestKey,
470
+ onPreview(assetId, installedTextureKey, asset) {
471
+ resolve({ assetId, textureKey: installedTextureKey, asset });
472
+ },
473
+ onError: reject
474
+ });
475
+ });
476
+ }
428
477
  export function previewImageSource(options) {
429
478
  const image = new Image();
479
+ const pendingForScene = pendingImagePreviews.get(options.scene) ?? new Map();
480
+ const requestKey = options.requestKey ?? options.assetId;
481
+ const previousImage = pendingForScene.get(requestKey);
482
+ if (previousImage) {
483
+ previousImage.onload = null;
484
+ previousImage.onerror = null;
485
+ }
486
+ pendingForScene.set(requestKey, image);
487
+ pendingImagePreviews.set(options.scene, pendingForScene);
488
+ if (shouldUseAnonymousCrossOrigin(options.src)) {
489
+ image.crossOrigin = "anonymous";
490
+ }
430
491
  image.onload = () => {
431
- if (options.scene.textures.exists(options.textureKey)) {
432
- options.scene.textures.remove(options.textureKey);
433
- }
434
- const asset = options.assetOverride ?? options.manifest.assets[options.assetId];
435
- if (asset.frameGrid && options.scene.textures.addSpriteSheet) {
436
- options.scene.textures.addSpriteSheet(options.textureKey, image, {
437
- frameWidth: asset.frameGrid.frameWidth,
438
- frameHeight: asset.frameGrid.frameHeight,
439
- margin: asset.frameGrid.margin,
440
- spacing: asset.frameGrid.spacing
441
- });
492
+ if (pendingForScene.get(requestKey) !== image)
493
+ return;
494
+ pendingForScene.delete(requestKey);
495
+ try {
496
+ if (options.scene.textures.exists(options.textureKey)) {
497
+ options.scene.textures.remove(options.textureKey);
498
+ }
499
+ const asset = options.assetOverride ?? options.manifest.assets[options.assetId];
500
+ const tileset = designerTilesetMetadata(asset);
501
+ if (tileset && options.scene.textures.addSpriteSheet) {
502
+ options.scene.textures.addSpriteSheet(options.textureKey, image, {
503
+ frameWidth: tileset.tileWidth,
504
+ frameHeight: tileset.tileHeight,
505
+ margin: tileset.margin,
506
+ spacing: tileset.spacing
507
+ });
508
+ }
509
+ else if (asset.frameGrid && options.scene.textures.addSpriteSheet) {
510
+ options.scene.textures.addSpriteSheet(options.textureKey, image, {
511
+ frameWidth: asset.frameGrid.frameWidth,
512
+ frameHeight: asset.frameGrid.frameHeight,
513
+ margin: asset.frameGrid.margin,
514
+ spacing: asset.frameGrid.spacing
515
+ });
516
+ }
517
+ else {
518
+ options.scene.textures.addImage(options.textureKey, image);
519
+ }
520
+ options.onPreview(options.assetId, options.textureKey, asset);
442
521
  }
443
- else {
444
- options.scene.textures.addImage(options.textureKey, image);
522
+ catch (error) {
523
+ const previewError = error instanceof Error ? error : new Error(String(error));
524
+ if (options.onError)
525
+ options.onError(previewError);
526
+ else
527
+ console.error("Could not preview AI asset image.", previewError);
445
528
  }
446
- options.onPreview(options.assetId, options.textureKey, asset);
529
+ };
530
+ image.onerror = () => {
531
+ if (pendingForScene.get(requestKey) === image) {
532
+ pendingForScene.delete(requestKey);
533
+ }
534
+ options.onError?.(new Error(`Could not load preview image for "${options.assetId}".`));
447
535
  };
448
536
  image.src = options.src;
449
537
  }
@@ -471,7 +559,9 @@ export async function uploadedOptionFromFile(options) {
471
559
  };
472
560
  }
473
561
  const imageSize = await imageSizeFromSource(dataUrl);
474
- const geometry = uploadedImageGeometry(options.asset, imageSize);
562
+ const format = normalizeAssetFormatFromMimeType(mimeType, options.file.name);
563
+ const generationGeometry = generationOverridesFromInputs(options.elements, options.asset, format);
564
+ const geometry = uploadedImageGeometry(options.asset, imageSize, generationGeometry.tileset);
475
565
  return {
476
566
  index: 0,
477
567
  dataUrl,
@@ -480,13 +570,38 @@ export async function uploadedOptionFromFile(options) {
480
570
  model: "uploaded",
481
571
  dimensions: geometry.dimensions,
482
572
  frameGrid: geometry.frameGrid,
573
+ tileset: geometry.tileset,
483
574
  settings: {
484
575
  ...options.asset.settings,
485
- format: normalizeAssetFormatFromMimeType(mimeType, options.file.name)
576
+ format
486
577
  }
487
578
  };
488
579
  }
489
- export function uploadedImageGeometry(asset, imageSize) {
580
+ export function uploadedImageGeometry(asset, imageSize, tilesetOverride) {
581
+ const baseTileset = designerTilesetMetadata(asset);
582
+ if (baseTileset) {
583
+ const tileset = {
584
+ ...baseTileset,
585
+ ...tilesetOverride
586
+ };
587
+ const capacity = tileset.columns * tileset.rows;
588
+ const tileCount = tileset.tileCount ?? capacity;
589
+ if (tileCount > capacity) {
590
+ throw new Error(`Tile count must not exceed this tileset's ${capacity}-tile capacity.`);
591
+ }
592
+ const margin = tileset.margin ?? 0;
593
+ const spacing = tileset.spacing ?? 0;
594
+ const dimensions = {
595
+ width: margin * 2 + tileset.columns * tileset.tileWidth +
596
+ Math.max(0, tileset.columns - 1) * spacing,
597
+ height: margin * 2 + tileset.rows * tileset.tileHeight +
598
+ Math.max(0, tileset.rows - 1) * spacing
599
+ };
600
+ if (imageSize.width !== dimensions.width || imageSize.height !== dimensions.height) {
601
+ throw new Error(`Tileset upload must be exactly ${dimensions.width}x${dimensions.height}px for the selected tile geometry; received ${imageSize.width}x${imageSize.height}px.`);
602
+ }
603
+ return { dimensions, tileset };
604
+ }
490
605
  if (!asset.frameGrid) {
491
606
  return {
492
607
  dimensions: imageSize
@@ -588,6 +703,7 @@ const frameTouchUpPreferences = {
588
703
  color: "#f8fafc",
589
704
  tool: "brush"
590
705
  };
706
+ let frameTouchUpClipboard;
591
707
  export async function openFrameTouchUpEditor(options) {
592
708
  const sourceImage = await loadImageElement(options.frameSrc);
593
709
  const dialog = document.createElement("div");
@@ -646,6 +762,7 @@ export async function openFrameTouchUpEditor(options) {
646
762
  const eraserButton = touchUpButton("Eraser");
647
763
  const pickerButton = touchUpButton("Picker");
648
764
  const selectButton = touchUpButton("Select");
765
+ selectButton.title = "Select pixels. Cmd/Ctrl+C copies; Cmd/Ctrl+V pastes.";
649
766
  const fillButton = touchUpButton("Fill");
650
767
  const undoButton = touchUpButton("Undo");
651
768
  const redoButton = touchUpButton("Redo");
@@ -679,6 +796,13 @@ export async function openFrameTouchUpEditor(options) {
679
796
  const selectionBox = document.createElement("div");
680
797
  selectionBox.className = "ai-game-assets-designer__touchup-selection";
681
798
  selectionBox.hidden = true;
799
+ const rotateSelectionButton = document.createElement("button");
800
+ rotateSelectionButton.type = "button";
801
+ rotateSelectionButton.className = "ai-game-assets-designer__touchup-selection-rotate";
802
+ rotateSelectionButton.textContent = "↻";
803
+ rotateSelectionButton.setAttribute("aria-label", "Rotate selection 90 degrees clockwise");
804
+ rotateSelectionButton.title = "Rotate selection 90° clockwise";
805
+ selectionBox.append(rotateSelectionButton);
682
806
  canvasWrap.append(canvas, selectionBox);
683
807
  const side = document.createElement("div");
684
808
  side.className = "ai-game-assets-designer__touchup-side";
@@ -718,6 +842,7 @@ export async function openFrameTouchUpEditor(options) {
718
842
  let drawingPointerId;
719
843
  let selectionStart;
720
844
  let selection;
845
+ let floatingSelection;
721
846
  let animationTimeout;
722
847
  let animationCursor = 0;
723
848
  let disposed = false;
@@ -775,6 +900,63 @@ export async function openFrameTouchUpEditor(options) {
775
900
  redoButton.disabled = redoStack.length === 0;
776
901
  };
777
902
  const snapshot = () => context.getImageData(0, 0, canvas.width, canvas.height);
903
+ const clipboardImageData = () => {
904
+ if (!frameTouchUpClipboard)
905
+ return undefined;
906
+ const width = Math.min(frameTouchUpClipboard.width, canvas.width);
907
+ const height = Math.min(frameTouchUpClipboard.height, canvas.height);
908
+ const imageData = context.createImageData(width, height);
909
+ const sourceStride = frameTouchUpClipboard.width * 4;
910
+ const targetStride = width * 4;
911
+ for (let row = 0; row < height; row += 1) {
912
+ imageData.data.set(frameTouchUpClipboard.pixels.subarray(row * sourceStride, (row * sourceStride) + targetStride), row * targetStride);
913
+ }
914
+ return imageData;
915
+ };
916
+ const drawFloatingSelection = () => {
917
+ if (!selection || !floatingSelection)
918
+ return;
919
+ context.putImageData(floatingSelection.background, 0, 0);
920
+ const floatingCanvas = document.createElement("canvas");
921
+ floatingCanvas.width = floatingSelection.pixels.width;
922
+ floatingCanvas.height = floatingSelection.pixels.height;
923
+ const floatingContext = floatingCanvas.getContext("2d");
924
+ if (!floatingContext)
925
+ return;
926
+ floatingContext.putImageData(floatingSelection.pixels, 0, 0);
927
+ context.drawImage(floatingCanvas, selection.x, selection.y);
928
+ };
929
+ const rotateImageDataWithinBounds = (imageData) => {
930
+ const sourceCanvas = document.createElement("canvas");
931
+ sourceCanvas.width = imageData.width;
932
+ sourceCanvas.height = imageData.height;
933
+ const sourceContext = sourceCanvas.getContext("2d");
934
+ const targetCanvas = document.createElement("canvas");
935
+ targetCanvas.width = imageData.width;
936
+ targetCanvas.height = imageData.height;
937
+ const targetContext = targetCanvas.getContext("2d", { willReadFrequently: true });
938
+ if (!sourceContext || !targetContext)
939
+ return imageData;
940
+ sourceContext.putImageData(imageData, 0, 0);
941
+ targetContext.imageSmoothingEnabled = false;
942
+ targetContext.translate(imageData.width / 2, imageData.height / 2);
943
+ targetContext.scale(imageData.width / imageData.height, imageData.height / imageData.width);
944
+ targetContext.rotate(Math.PI / 2);
945
+ targetContext.drawImage(sourceCanvas, -imageData.width / 2, -imageData.height / 2);
946
+ targetContext.setTransform(1, 0, 0, 1, 0, 0);
947
+ return targetContext.getImageData(0, 0, imageData.width, imageData.height);
948
+ };
949
+ const copySelection = () => {
950
+ if (!selection)
951
+ return false;
952
+ const imageData = floatingSelection?.pixels ?? context.getImageData(selection.x, selection.y, selection.width, selection.height);
953
+ frameTouchUpClipboard = {
954
+ width: imageData.width,
955
+ height: imageData.height,
956
+ pixels: new Uint8ClampedArray(imageData.data)
957
+ };
958
+ return true;
959
+ };
778
960
  const pushUndo = () => {
779
961
  undoStack.push(snapshot());
780
962
  if (undoStack.length > 60)
@@ -785,6 +967,7 @@ export async function openFrameTouchUpEditor(options) {
785
967
  const restore = (imageData) => {
786
968
  context.putImageData(imageData, 0, 0);
787
969
  selection = undefined;
970
+ floatingSelection = undefined;
788
971
  redrawEditor();
789
972
  setDirty(true);
790
973
  };
@@ -814,6 +997,12 @@ export async function openFrameTouchUpEditor(options) {
814
997
  selectionBox.style.top = `${selection.y * zoom}px`;
815
998
  selectionBox.style.width = `${selection.width * zoom}px`;
816
999
  selectionBox.style.height = `${selection.height * zoom}px`;
1000
+ const renderedWidth = selection.width * zoom;
1001
+ const renderedHeight = selection.height * zoom;
1002
+ rotateSelectionButton.hidden = renderedWidth < 12 || renderedHeight < 12;
1003
+ const handleSize = Math.max(0, Math.min(18, renderedWidth - 4, renderedHeight - 4));
1004
+ rotateSelectionButton.style.width = `${handleSize}px`;
1005
+ rotateSelectionButton.style.height = `${handleSize}px`;
817
1006
  };
818
1007
  const refreshPreviews = () => {
819
1008
  stillContext.clearRect(0, 0, stillPreview.width, stillPreview.height);
@@ -997,6 +1186,13 @@ export async function openFrameTouchUpEditor(options) {
997
1186
  pushUndo();
998
1187
  const nextX = clamp(selection.x + deltaX, 0, canvas.width - selection.width);
999
1188
  const nextY = clamp(selection.y + deltaY, 0, canvas.height - selection.height);
1189
+ if (floatingSelection) {
1190
+ selection = { ...selection, x: nextX, y: nextY };
1191
+ drawFloatingSelection();
1192
+ setDirty(true);
1193
+ redrawEditor();
1194
+ return;
1195
+ }
1000
1196
  const imageData = context.getImageData(selection.x, selection.y, selection.width, selection.height);
1001
1197
  context.clearRect(selection.x, selection.y, selection.width, selection.height);
1002
1198
  context.putImageData(imageData, nextX, nextY);
@@ -1004,15 +1200,60 @@ export async function openFrameTouchUpEditor(options) {
1004
1200
  setDirty(true);
1005
1201
  redrawEditor();
1006
1202
  };
1203
+ const rotateSelection = () => {
1204
+ if (!selection)
1205
+ return;
1206
+ pushUndo();
1207
+ const pixels = floatingSelection?.pixels ?? context.getImageData(selection.x, selection.y, selection.width, selection.height);
1208
+ const rotated = rotateImageDataWithinBounds(pixels);
1209
+ if (floatingSelection) {
1210
+ floatingSelection.pixels = rotated;
1211
+ drawFloatingSelection();
1212
+ }
1213
+ else {
1214
+ context.putImageData(rotated, selection.x, selection.y);
1215
+ }
1216
+ setDirty(true);
1217
+ redrawEditor();
1218
+ };
1007
1219
  const deleteSelection = () => {
1008
1220
  if (!selection)
1009
1221
  return;
1010
1222
  pushUndo();
1011
- context.clearRect(selection.x, selection.y, selection.width, selection.height);
1223
+ if (floatingSelection) {
1224
+ context.putImageData(floatingSelection.background, 0, 0);
1225
+ floatingSelection = undefined;
1226
+ }
1227
+ else {
1228
+ context.clearRect(selection.x, selection.y, selection.width, selection.height);
1229
+ }
1012
1230
  selection = undefined;
1013
1231
  setDirty(true);
1014
1232
  redrawEditor();
1015
1233
  };
1234
+ const pasteSelection = () => {
1235
+ const pixels = clipboardImageData();
1236
+ if (!pixels)
1237
+ return false;
1238
+ const x = clamp(selection?.x ?? Math.floor((canvas.width - pixels.width) / 2), 0, canvas.width - pixels.width);
1239
+ const y = clamp(selection?.y ?? Math.floor((canvas.height - pixels.height) / 2), 0, canvas.height - pixels.height);
1240
+ pushUndo();
1241
+ floatingSelection = {
1242
+ background: snapshot(),
1243
+ pixels
1244
+ };
1245
+ selection = {
1246
+ x,
1247
+ y,
1248
+ width: pixels.width,
1249
+ height: pixels.height
1250
+ };
1251
+ selectTool("select");
1252
+ drawFloatingSelection();
1253
+ setDirty(true);
1254
+ redrawEditor();
1255
+ return true;
1256
+ };
1016
1257
  const drawAnimationFrame = () => {
1017
1258
  if (disposed)
1018
1259
  return;
@@ -1054,6 +1295,8 @@ export async function openFrameTouchUpEditor(options) {
1054
1295
  brushMenu.hidden = true;
1055
1296
  };
1056
1297
  const close = () => {
1298
+ if (disposed)
1299
+ return;
1057
1300
  disposed = true;
1058
1301
  if (animationTimeout !== undefined)
1059
1302
  window.clearTimeout(animationTimeout);
@@ -1063,6 +1306,7 @@ export async function openFrameTouchUpEditor(options) {
1063
1306
  window.removeEventListener("blur", cancelDrawing);
1064
1307
  document.removeEventListener("pointerdown", closeBrushMenuOnOutsidePointer);
1065
1308
  dialog.remove();
1309
+ options.onClose?.();
1066
1310
  };
1067
1311
  const requestClose = () => {
1068
1312
  if (dirty && !window.confirm("You have unsaved changes. Close without saving?"))
@@ -1074,6 +1318,23 @@ export async function openFrameTouchUpEditor(options) {
1074
1318
  return;
1075
1319
  const key = event.key.toLowerCase();
1076
1320
  const commandModifier = event.metaKey || event.ctrlKey;
1321
+ const target = event.target;
1322
+ const isEditingInput = target instanceof HTMLInputElement ||
1323
+ target instanceof HTMLTextAreaElement ||
1324
+ target instanceof HTMLSelectElement ||
1325
+ (target instanceof HTMLElement && target.isContentEditable);
1326
+ if (isEditingInput && event.key !== "Escape")
1327
+ return;
1328
+ if (commandModifier && !event.altKey && key === "c" && copySelection()) {
1329
+ event.preventDefault();
1330
+ event.stopPropagation();
1331
+ return;
1332
+ }
1333
+ if (commandModifier && !event.altKey && key === "v" && pasteSelection()) {
1334
+ event.preventDefault();
1335
+ event.stopPropagation();
1336
+ return;
1337
+ }
1077
1338
  if (commandModifier && !event.altKey && (key === "z" || key === "y")) {
1078
1339
  event.preventDefault();
1079
1340
  event.stopPropagation();
@@ -1141,6 +1402,7 @@ export async function openFrameTouchUpEditor(options) {
1141
1402
  pickColorAt(point);
1142
1403
  return;
1143
1404
  }
1405
+ floatingSelection = undefined;
1144
1406
  pushUndo();
1145
1407
  drawing = true;
1146
1408
  drawingPointerId = event.pointerId;
@@ -1246,6 +1508,15 @@ export async function openFrameTouchUpEditor(options) {
1246
1508
  clientY: event.clientY
1247
1509
  });
1248
1510
  }, { passive: false });
1511
+ rotateSelectionButton.addEventListener("pointerdown", (event) => {
1512
+ event.preventDefault();
1513
+ event.stopPropagation();
1514
+ });
1515
+ rotateSelectionButton.addEventListener("click", (event) => {
1516
+ event.preventDefault();
1517
+ event.stopPropagation();
1518
+ rotateSelection();
1519
+ });
1249
1520
  closeButton.addEventListener("click", requestClose);
1250
1521
  zoomOutButton.addEventListener("click", () => {
1251
1522
  setZoom(zoom - 1);
@@ -1747,9 +2018,12 @@ export async function openStyleGuideEditor(options) {
1747
2018
  const promoteButton = document.createElement("button");
1748
2019
  promoteButton.type = "button";
1749
2020
  promoteButton.textContent = "Promote style";
2021
+ const regenerateAllButton = document.createElement("button");
2022
+ regenerateAllButton.type = "button";
2023
+ regenerateAllButton.textContent = "Regenerate all...";
1750
2024
  const actions = document.createElement("div");
1751
2025
  actions.className = "ai-game-assets-designer__modal-actions";
1752
- actions.append(cancelButton, promoteButton, confirmButton);
2026
+ actions.append(cancelButton, promoteButton, regenerateAllButton, confirmButton);
1753
2027
  const draft = () => ({
1754
2028
  prompt: promptInput.value.trim(),
1755
2029
  images: images.map((image) => ({ ...image }))
@@ -1775,6 +2049,12 @@ export async function openStyleGuideEditor(options) {
1775
2049
  promoteButton.disabled = false;
1776
2050
  }
1777
2051
  });
2052
+ regenerateAllButton.addEventListener("click", () => {
2053
+ const current = draft();
2054
+ options.onConfirm(current);
2055
+ close();
2056
+ void options.onRegenerateAll(current);
2057
+ });
1778
2058
  card.append(title, promptField, dropZone, uploadButton, fileInput, imageList, actions);
1779
2059
  dialog.append(card);
1780
2060
  options.root.append(dialog);
@@ -2492,10 +2772,21 @@ export function ensureFramePreviewElement(element) {
2492
2772
  return frameElement;
2493
2773
  }
2494
2774
  export function generationOverridesFromInputs(elements, asset, format) {
2775
+ const tileset = designerTilesetMetadata(asset);
2495
2776
  const dimensions = {
2496
- width: positiveIntegerInput(elements.widthInput, asset.frameGrid?.frameWidth ?? asset.dimensions?.width ?? 1),
2497
- height: positiveIntegerInput(elements.heightInput, asset.frameGrid?.frameHeight ?? asset.dimensions?.height ?? 1)
2777
+ width: positiveIntegerInput(elements.widthInput, asset.frameGrid?.frameWidth ?? tileset?.tileWidth ?? asset.dimensions?.width ?? 1),
2778
+ height: positiveIntegerInput(elements.heightInput, asset.frameGrid?.frameHeight ?? tileset?.tileHeight ?? asset.dimensions?.height ?? 1)
2498
2779
  };
2780
+ if (tileset) {
2781
+ return {
2782
+ tileset: {
2783
+ tileWidth: dimensions.width,
2784
+ tileHeight: dimensions.height,
2785
+ tileCount: positiveIntegerInput(elements.frameCountInput, tileset.tileCount ?? tileset.columns * tileset.rows)
2786
+ },
2787
+ settings: { format }
2788
+ };
2789
+ }
2499
2790
  if (!asset.frameGrid) {
2500
2791
  return {
2501
2792
  dimensions,
@@ -2564,6 +2855,9 @@ export function isAudioAsset(asset) {
2564
2855
  asset?.kind === "voice" ||
2565
2856
  asset?.kind === "voice-line";
2566
2857
  }
2858
+ function designerTilesetMetadata(asset) {
2859
+ return asset?.tileset;
2860
+ }
2567
2861
  export function isVoiceAsset(asset) {
2568
2862
  return asset?.kind === "voice" || asset?.kind === "voice-line";
2569
2863
  }
@@ -2572,6 +2866,7 @@ export function assetWithGeneratedGeometry(asset, option, options = {}) {
2572
2866
  ...asset,
2573
2867
  dimensions: option.dimensions ?? asset.dimensions,
2574
2868
  frameGrid: option.frameGrid ?? asset.frameGrid,
2869
+ tileset: option.tileset ?? asset.tileset,
2575
2870
  animations: option.animations ??
2576
2871
  (options.inheritAnimations
2577
2872
  ? asset.animations
@@ -2593,7 +2888,11 @@ export function positiveIntegerInput(input, fallback) {
2593
2888
  const value = Number(input.value);
2594
2889
  if (!Number.isFinite(value))
2595
2890
  return fallback;
2596
- return Math.max(1, Math.floor(value));
2891
+ const normalized = Math.max(1, Math.floor(value));
2892
+ const maximum = Number(input.max);
2893
+ return input.max && Number.isFinite(maximum)
2894
+ ? Math.min(normalized, Math.max(1, Math.floor(maximum)))
2895
+ : normalized;
2597
2896
  }
2598
2897
  export function positiveNumberInput(input, fallback) {
2599
2898
  const value = Number(input.value);
@@ -2724,7 +3023,7 @@ export async function imageSourceToDataUrl(src) {
2724
3023
  return src;
2725
3024
  const response = await fetch(src);
2726
3025
  if (!response.ok) {
2727
- throw new Error(`Could not load current animation image (${response.status}).`);
3026
+ throw new Error(`Could not load current asset image (${response.status}).`);
2728
3027
  }
2729
3028
  const responseBlob = await response.blob();
2730
3029
  const blob = responseBlob.type === "application/octet-stream"
@@ -2737,11 +3036,11 @@ export async function imageSourceToDataUrl(src) {
2737
3036
  resolve(reader.result);
2738
3037
  }
2739
3038
  else {
2740
- reject(new Error("Could not convert current animation image to a data URL."));
3039
+ reject(new Error("Could not convert current asset image to a data URL."));
2741
3040
  }
2742
3041
  });
2743
3042
  reader.addEventListener("error", () => {
2744
- reject(reader.error ?? new Error("Could not read current animation image."));
3043
+ reject(reader.error ?? new Error("Could not read current asset image."));
2745
3044
  });
2746
3045
  reader.readAsDataURL(blob);
2747
3046
  });
@@ -2947,6 +3246,36 @@ export function ensureDesignerStyles() {
2947
3246
  font: inherit;
2948
3247
  }
2949
3248
  .ai-game-assets-designer__field textarea { resize: vertical; }
3249
+ .ai-game-assets-designer__tile-prompts {
3250
+ display: grid;
3251
+ gap: 7px;
3252
+ margin-bottom: 12px;
3253
+ padding: 10px;
3254
+ border: 1px solid #303949;
3255
+ border-radius: 8px;
3256
+ background: rgba(11, 15, 22, 0.58);
3257
+ }
3258
+ .ai-game-assets-designer__tile-prompts[hidden] { display: none; }
3259
+ .ai-game-assets-designer__tile-prompts-title {
3260
+ color: #e5edf9;
3261
+ font-size: 13px;
3262
+ font-weight: 700;
3263
+ }
3264
+ .ai-game-assets-designer__tile-prompts-hint {
3265
+ color: #8f9cb5;
3266
+ font-size: 11px;
3267
+ line-height: 1.35;
3268
+ }
3269
+ .ai-game-assets-designer__tile-prompts-list {
3270
+ display: grid;
3271
+ gap: 8px;
3272
+ }
3273
+ .ai-game-assets-designer__tile-prompts-list .ai-game-assets-designer__field {
3274
+ margin-bottom: 0;
3275
+ }
3276
+ .ai-game-assets-designer__tile-prompts-list textarea {
3277
+ min-height: 62px;
3278
+ }
2950
3279
  .ai-game-assets-designer__target-variant {
2951
3280
  margin: -4px 0 12px;
2952
3281
  padding: 7px 9px;
@@ -3171,6 +3500,17 @@ export function ensureDesignerStyles() {
3171
3500
  gap: 8px;
3172
3501
  margin-top: 10px;
3173
3502
  }
3503
+ .ai-game-assets-designer__tileset-tile-prompt {
3504
+ margin-top: 10px;
3505
+ padding: 9px 10px;
3506
+ border: 1px solid #354058;
3507
+ border-radius: 6px;
3508
+ background: rgba(37, 49, 73, 0.48);
3509
+ color: #d8e4f7;
3510
+ font-size: 12px;
3511
+ line-height: 1.45;
3512
+ white-space: pre-wrap;
3513
+ }
3174
3514
  .ai-game-assets-designer__options.is-audio {
3175
3515
  grid-template-columns: 1fr;
3176
3516
  }
@@ -3245,6 +3585,159 @@ export function ensureDesignerStyles() {
3245
3585
  box-shadow: 0 22px 70px rgba(0, 0, 0, 0.55);
3246
3586
  padding: 14px;
3247
3587
  }
3588
+ .ai-game-assets-designer__tileset-mixer-card {
3589
+ width: min(980px, calc(100vw - 36px));
3590
+ }
3591
+ .ai-game-assets-designer__tileset-editor-card {
3592
+ width: min(720px, calc(100vw - 36px));
3593
+ }
3594
+ .ai-game-assets-designer__tileset-editor-stage {
3595
+ border: 1px solid #303a49;
3596
+ border-radius: 8px;
3597
+ }
3598
+ .ai-game-assets-designer__tileset-editor-strip {
3599
+ margin-top: 8px;
3600
+ }
3601
+ .ai-game-assets-designer__tileset-transform-fields {
3602
+ margin-bottom: 6px;
3603
+ }
3604
+ .ai-game-assets-designer__tileset-transform-hint {
3605
+ margin: 0 0 10px;
3606
+ color: #aeb8c8;
3607
+ font-size: 12px;
3608
+ line-height: 1.4;
3609
+ }
3610
+ .ai-game-assets-designer__tileset-transform-thumb {
3611
+ width: 48px;
3612
+ height: 48px;
3613
+ display: grid;
3614
+ place-items: center;
3615
+ }
3616
+ .ai-game-assets-designer__tileset-transform-thumb-viewport {
3617
+ position: relative;
3618
+ display: block;
3619
+ overflow: hidden;
3620
+ border-radius: 3px;
3621
+ pointer-events: none;
3622
+ }
3623
+ @media (max-width: 520px) {
3624
+ .ai-game-assets-designer__tileset-transform-fields {
3625
+ grid-template-columns: repeat(2, minmax(0, 1fr));
3626
+ }
3627
+ }
3628
+ .ai-game-assets-designer__tileset-regenerate-card {
3629
+ width: min(620px, calc(100vw - 36px));
3630
+ }
3631
+ .ai-game-assets-designer__tileset-regenerate-card .ai-game-assets-designer__tileset-tile-prompt {
3632
+ margin: 0 0 12px;
3633
+ }
3634
+ .ai-game-assets-designer__tileset-mixer-hint {
3635
+ margin: -5px 0 12px;
3636
+ color: #aeb8c8;
3637
+ font-size: 12px;
3638
+ line-height: 1.4;
3639
+ }
3640
+ .ai-game-assets-designer__tileset-mixer-body {
3641
+ display: grid;
3642
+ grid-template-columns: minmax(160px, 220px) minmax(0, 1fr);
3643
+ gap: 14px;
3644
+ min-height: 300px;
3645
+ }
3646
+ .ai-game-assets-designer__tileset-navigator-panel,
3647
+ .ai-game-assets-designer__tileset-preview-panel {
3648
+ min-width: 0;
3649
+ border: 1px solid #303a49;
3650
+ border-radius: 8px;
3651
+ background: #0f1218;
3652
+ padding: 10px;
3653
+ }
3654
+ .ai-game-assets-designer__tileset-section-title {
3655
+ min-height: 20px;
3656
+ margin-bottom: 8px;
3657
+ color: #dbeafe;
3658
+ font-size: 12px;
3659
+ font-weight: 700;
3660
+ }
3661
+ .ai-game-assets-designer__tileset-navigator {
3662
+ display: grid;
3663
+ grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
3664
+ gap: 7px;
3665
+ max-height: min(58vh, 540px);
3666
+ overflow: auto;
3667
+ padding: 2px;
3668
+ }
3669
+ .ai-game-assets-designer__tileset-tile {
3670
+ position: relative;
3671
+ overflow: hidden;
3672
+ border: 2px solid #384251;
3673
+ border-radius: 6px;
3674
+ background: #111827;
3675
+ cursor: pointer;
3676
+ }
3677
+ .ai-game-assets-designer__tileset-tile::after {
3678
+ content: attr(data-source);
3679
+ position: absolute;
3680
+ right: 2px;
3681
+ bottom: 2px;
3682
+ z-index: 3;
3683
+ border-radius: 999px;
3684
+ background: rgba(8, 12, 20, 0.88);
3685
+ color: #9ca8ba;
3686
+ padding: 1px 4px;
3687
+ font-size: 9px;
3688
+ line-height: 1.4;
3689
+ }
3690
+ .ai-game-assets-designer__tileset-tile.is-selected {
3691
+ border-color: #6ed3ff;
3692
+ box-shadow: 0 0 0 1px rgba(110, 211, 255, 0.35);
3693
+ }
3694
+ .ai-game-assets-designer__tileset-tile.is-mixed::after {
3695
+ background: #155e75;
3696
+ color: #ecfeff;
3697
+ }
3698
+ .ai-game-assets-designer__tileset-choices {
3699
+ display: grid;
3700
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
3701
+ gap: 10px;
3702
+ align-items: start;
3703
+ width: 100%;
3704
+ min-width: 0;
3705
+ }
3706
+ .ai-game-assets-designer__tileset-choice {
3707
+ display: grid;
3708
+ justify-items: center;
3709
+ gap: 7px;
3710
+ min-width: 0;
3711
+ max-width: 100%;
3712
+ overflow: hidden;
3713
+ border: 2px solid #384251;
3714
+ border-radius: 8px;
3715
+ background: #151b25;
3716
+ color: #dbeafe;
3717
+ padding: 9px;
3718
+ font: inherit;
3719
+ font-size: 12px;
3720
+ cursor: pointer;
3721
+ }
3722
+ .ai-game-assets-designer__tileset-choice.is-selected {
3723
+ border-color: #6ed3ff;
3724
+ background: #16283a;
3725
+ }
3726
+ .ai-game-assets-designer__tileset-choice-preview {
3727
+ position: relative;
3728
+ overflow: hidden;
3729
+ max-width: 100%;
3730
+ border-radius: 6px;
3731
+ background: #0a0d12;
3732
+ }
3733
+ @media (max-width: 760px) {
3734
+ .ai-game-assets-designer__tileset-mixer-body {
3735
+ grid-template-columns: 1fr;
3736
+ }
3737
+ .ai-game-assets-designer__tileset-navigator {
3738
+ max-height: 180px;
3739
+ }
3740
+ }
3248
3741
  .ai-game-assets-designer__modal-title {
3249
3742
  margin-bottom: 12px;
3250
3743
  font-weight: 700;
@@ -3268,6 +3761,9 @@ export function ensureDesignerStyles() {
3268
3761
  .ai-game-assets-designer__modal-feedback[data-kind="error"] {
3269
3762
  color: #ffb4b4;
3270
3763
  }
3764
+ .ai-game-assets-designer__modal-feedback[data-kind="success"] {
3765
+ color: #86efac;
3766
+ }
3271
3767
  .ai-game-assets-designer__version-list {
3272
3768
  display: grid;
3273
3769
  gap: 10px;
@@ -3381,6 +3877,9 @@ export function ensureDesignerStyles() {
3381
3877
  .ai-game-assets-designer__frame-thumb.is-selected::after {
3382
3878
  border-color: #6ed3ff;
3383
3879
  }
3880
+ .ai-game-assets-designer__frame-thumb.is-previewing::after {
3881
+ box-shadow: inset 0 -3px 0 #facc15;
3882
+ }
3384
3883
  .ai-game-assets-designer__frame-fields {
3385
3884
  display: grid;
3386
3885
  grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -3530,7 +4029,7 @@ export function ensureDesignerStyles() {
3530
4029
  .ai-game-assets-designer__touchup {
3531
4030
  position: fixed;
3532
4031
  inset: 0;
3533
- z-index: 1;
4032
+ z-index: 2147483647;
3534
4033
  display: grid;
3535
4034
  grid-template-rows: auto auto minmax(0, 1fr);
3536
4035
  gap: 10px;
@@ -3716,6 +4215,30 @@ export function ensureDesignerStyles() {
3716
4215
  box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.9);
3717
4216
  transform: translate(24px, 24px);
3718
4217
  }
4218
+ .ai-game-assets-designer__touchup-selection-rotate {
4219
+ position: absolute;
4220
+ top: 2px;
4221
+ right: 2px;
4222
+ z-index: 2;
4223
+ display: grid;
4224
+ place-items: center;
4225
+ min-width: 0;
4226
+ min-height: 0;
4227
+ border: 1px solid #dbeafe;
4228
+ border-radius: 999px;
4229
+ background: rgba(15, 23, 42, 0.94);
4230
+ color: #f8fafc;
4231
+ padding: 0;
4232
+ font: 700 11px/1 ui-sans-serif, system-ui, sans-serif;
4233
+ cursor: pointer;
4234
+ pointer-events: auto;
4235
+ }
4236
+ .ai-game-assets-designer__touchup-selection-rotate:hover,
4237
+ .ai-game-assets-designer__touchup-selection-rotate:focus-visible {
4238
+ border-color: #6ed3ff;
4239
+ color: #6ed3ff;
4240
+ outline: none;
4241
+ }
3719
4242
  .ai-game-assets-designer__touchup-side {
3720
4243
  min-height: 0;
3721
4244
  display: grid;