@ai-game-assets/phaser 0.6.0 → 0.8.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 (49) 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 +55 -2
  6. package/dist/debug-client.d.ts.map +1 -1
  7. package/dist/debug-client.js +77 -0
  8. package/dist/debug-client.js.map +1 -1
  9. package/dist/designer-support.d.ts +29 -3
  10. package/dist/designer-support.d.ts.map +1 -1
  11. package/dist/designer-support.js +557 -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 +1897 -143
  16. package/dist/designer.js.map +1 -1
  17. package/dist/first-drafts.d.ts.map +1 -1
  18. package/dist/first-drafts.js +68 -40
  19. package/dist/first-drafts.js.map +1 -1
  20. package/dist/index.d.ts +9 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/keys.d.ts +2 -0
  25. package/dist/keys.d.ts.map +1 -1
  26. package/dist/keys.js +6 -0
  27. package/dist/keys.js.map +1 -1
  28. package/dist/loader.d.ts.map +1 -1
  29. package/dist/loader.js +50 -3
  30. package/dist/loader.js.map +1 -1
  31. package/dist/phaser-types.d.ts +1 -0
  32. package/dist/phaser-types.d.ts.map +1 -1
  33. package/dist/runtime.d.ts +30 -2
  34. package/dist/runtime.d.ts.map +1 -1
  35. package/dist/runtime.js +235 -8
  36. package/dist/runtime.js.map +1 -1
  37. package/dist/tileset-dialog.d.ts +121 -0
  38. package/dist/tileset-dialog.d.ts.map +1 -0
  39. package/dist/tileset-dialog.js +1594 -0
  40. package/dist/tileset-dialog.js.map +1 -0
  41. package/dist/tilesets.d.ts +13 -0
  42. package/dist/tilesets.d.ts.map +1 -0
  43. package/dist/tilesets.js +60 -0
  44. package/dist/tilesets.js.map +1 -0
  45. package/dist/voice-line-regeneration.d.ts +47 -0
  46. package/dist/voice-line-regeneration.d.ts.map +1 -0
  47. package/dist/voice-line-regeneration.js +185 -0
  48. package/dist/voice-line-regeneration.js.map +1 -0
  49. 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,14 @@ 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 regenerateAllLinesButton = document.createElement("button");
155
+ regenerateAllLinesButton.type = "button";
156
+ regenerateAllLinesButton.textContent = "Regenerate all lines";
157
+ regenerateAllLinesButton.hidden = true;
158
+ const mixTilesetButton = document.createElement("button");
159
+ mixTilesetButton.type = "button";
160
+ mixTilesetButton.textContent = "Mix tileset";
161
+ mixTilesetButton.hidden = true;
138
162
  const uploadButton = document.createElement("button");
139
163
  uploadButton.type = "button";
140
164
  uploadButton.textContent = "Upload...";
@@ -148,19 +172,23 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
148
172
  promoteButton.type = "button";
149
173
  promoteButton.textContent = "Promote";
150
174
  promoteButton.disabled = true;
175
+ const promoteAllButton = document.createElement("button");
176
+ promoteAllButton.type = "button";
177
+ promoteAllButton.textContent = "Promote all";
178
+ promoteAllButton.disabled = true;
151
179
  const restartButton = document.createElement("button");
152
180
  restartButton.type = "button";
153
181
  restartButton.textContent = "Restart";
154
182
  const actions = document.createElement("div");
155
183
  actions.className = "ai-game-assets-designer__actions";
156
- actions.append(regenerateButton, uploadButton, deriveButton, versionsButton, promoteButton, restartButton);
184
+ actions.append(regenerateButton, regenerateAllLinesButton, mixTilesetButton, uploadButton, deriveButton, versionsButton, promoteButton, promoteAllButton, restartButton);
157
185
  const versionLabel = document.createElement("div");
158
186
  versionLabel.className = "ai-game-assets-designer__meta";
159
187
  const optionsGrid = document.createElement("div");
160
188
  optionsGrid.className = "ai-game-assets-designer__options";
161
189
  const status = document.createElement("div");
162
190
  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);
191
+ 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
192
  root.append(toggle, panel);
165
193
  return {
166
194
  root,
@@ -176,7 +204,9 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
176
204
  animationSelect,
177
205
  animationField,
178
206
  widthInput,
207
+ widthField,
179
208
  heightInput,
209
+ heightField,
180
210
  dimensionGrid,
181
211
  frameCountInput,
182
212
  frameCountField,
@@ -191,6 +221,9 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
191
221
  voiceTextInput,
192
222
  voiceTextField,
193
223
  promptInput,
224
+ promptField,
225
+ tilesetPromptsField,
226
+ tilesetPromptsList,
194
227
  currentImage,
195
228
  currentAudio,
196
229
  currentAnimation,
@@ -201,8 +234,11 @@ export function createDesignerElements(options, manifest, selectedAssetId) {
201
234
  uploadButton,
202
235
  deriveButton,
203
236
  regenerateButton,
237
+ regenerateAllLinesButton,
238
+ mixTilesetButton,
204
239
  versionsButton,
205
240
  promoteButton,
241
+ promoteAllButton,
206
242
  restartButton,
207
243
  versionLabel,
208
244
  options: optionsGrid,
@@ -406,6 +442,8 @@ export function renderOptions(options) {
406
442
  options.elements.options.append(card);
407
443
  }
408
444
  }
445
+ const pendingImagePreviews = new WeakMap();
446
+ let promotedTextureInstallId = 0;
409
447
  export function previewOption(options) {
410
448
  const textureKey = `ai-preview:${options.assetId}:${options.option.index}:${Date.now()}`;
411
449
  previewImageSource({
@@ -425,25 +463,80 @@ export function previewCurrentAsset(options) {
425
463
  textureKey
426
464
  });
427
465
  }
466
+ export function installPromotedImageTexture(options) {
467
+ const textureKey = aiTextureKey({ assetId: options.assetId });
468
+ promotedTextureInstallId += 1;
469
+ const requestKey = `promote:${options.assetId}:${promotedTextureInstallId}`;
470
+ return new Promise((resolve, reject) => {
471
+ previewImageSource({
472
+ ...options,
473
+ textureKey,
474
+ requestKey,
475
+ onPreview(assetId, installedTextureKey, asset) {
476
+ resolve({ assetId, textureKey: installedTextureKey, asset });
477
+ },
478
+ onError: reject
479
+ });
480
+ });
481
+ }
428
482
  export function previewImageSource(options) {
429
483
  const image = new Image();
484
+ const pendingForScene = pendingImagePreviews.get(options.scene) ?? new Map();
485
+ const requestKey = options.requestKey ?? options.assetId;
486
+ const previousImage = pendingForScene.get(requestKey);
487
+ if (previousImage) {
488
+ previousImage.onload = null;
489
+ previousImage.onerror = null;
490
+ }
491
+ pendingForScene.set(requestKey, image);
492
+ pendingImagePreviews.set(options.scene, pendingForScene);
493
+ if (shouldUseAnonymousCrossOrigin(options.src)) {
494
+ image.crossOrigin = "anonymous";
495
+ }
430
496
  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
- });
497
+ if (pendingForScene.get(requestKey) !== image)
498
+ return;
499
+ pendingForScene.delete(requestKey);
500
+ try {
501
+ if (options.scene.textures.exists(options.textureKey)) {
502
+ options.scene.textures.remove(options.textureKey);
503
+ }
504
+ const asset = options.assetOverride ?? options.manifest.assets[options.assetId];
505
+ const tileset = designerTilesetMetadata(asset);
506
+ if (tileset && options.scene.textures.addSpriteSheet) {
507
+ options.scene.textures.addSpriteSheet(options.textureKey, image, {
508
+ frameWidth: tileset.tileWidth,
509
+ frameHeight: tileset.tileHeight,
510
+ margin: tileset.margin,
511
+ spacing: tileset.spacing
512
+ });
513
+ }
514
+ else if (asset.frameGrid && options.scene.textures.addSpriteSheet) {
515
+ options.scene.textures.addSpriteSheet(options.textureKey, image, {
516
+ frameWidth: asset.frameGrid.frameWidth,
517
+ frameHeight: asset.frameGrid.frameHeight,
518
+ margin: asset.frameGrid.margin,
519
+ spacing: asset.frameGrid.spacing
520
+ });
521
+ }
522
+ else {
523
+ options.scene.textures.addImage(options.textureKey, image);
524
+ }
525
+ options.onPreview(options.assetId, options.textureKey, asset);
442
526
  }
443
- else {
444
- options.scene.textures.addImage(options.textureKey, image);
527
+ catch (error) {
528
+ const previewError = error instanceof Error ? error : new Error(String(error));
529
+ if (options.onError)
530
+ options.onError(previewError);
531
+ else
532
+ console.error("Could not preview AI asset image.", previewError);
445
533
  }
446
- options.onPreview(options.assetId, options.textureKey, asset);
534
+ };
535
+ image.onerror = () => {
536
+ if (pendingForScene.get(requestKey) === image) {
537
+ pendingForScene.delete(requestKey);
538
+ }
539
+ options.onError?.(new Error(`Could not load preview image for "${options.assetId}".`));
447
540
  };
448
541
  image.src = options.src;
449
542
  }
@@ -471,7 +564,9 @@ export async function uploadedOptionFromFile(options) {
471
564
  };
472
565
  }
473
566
  const imageSize = await imageSizeFromSource(dataUrl);
474
- const geometry = uploadedImageGeometry(options.asset, imageSize);
567
+ const format = normalizeAssetFormatFromMimeType(mimeType, options.file.name);
568
+ const generationGeometry = generationOverridesFromInputs(options.elements, options.asset, format);
569
+ const geometry = uploadedImageGeometry(options.asset, imageSize, generationGeometry.tileset);
475
570
  return {
476
571
  index: 0,
477
572
  dataUrl,
@@ -480,13 +575,38 @@ export async function uploadedOptionFromFile(options) {
480
575
  model: "uploaded",
481
576
  dimensions: geometry.dimensions,
482
577
  frameGrid: geometry.frameGrid,
578
+ tileset: geometry.tileset,
483
579
  settings: {
484
580
  ...options.asset.settings,
485
- format: normalizeAssetFormatFromMimeType(mimeType, options.file.name)
581
+ format
486
582
  }
487
583
  };
488
584
  }
489
- export function uploadedImageGeometry(asset, imageSize) {
585
+ export function uploadedImageGeometry(asset, imageSize, tilesetOverride) {
586
+ const baseTileset = designerTilesetMetadata(asset);
587
+ if (baseTileset) {
588
+ const tileset = {
589
+ ...baseTileset,
590
+ ...tilesetOverride
591
+ };
592
+ const capacity = tileset.columns * tileset.rows;
593
+ const tileCount = tileset.tileCount ?? capacity;
594
+ if (tileCount > capacity) {
595
+ throw new Error(`Tile count must not exceed this tileset's ${capacity}-tile capacity.`);
596
+ }
597
+ const margin = tileset.margin ?? 0;
598
+ const spacing = tileset.spacing ?? 0;
599
+ const dimensions = {
600
+ width: margin * 2 + tileset.columns * tileset.tileWidth +
601
+ Math.max(0, tileset.columns - 1) * spacing,
602
+ height: margin * 2 + tileset.rows * tileset.tileHeight +
603
+ Math.max(0, tileset.rows - 1) * spacing
604
+ };
605
+ if (imageSize.width !== dimensions.width || imageSize.height !== dimensions.height) {
606
+ 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.`);
607
+ }
608
+ return { dimensions, tileset };
609
+ }
490
610
  if (!asset.frameGrid) {
491
611
  return {
492
612
  dimensions: imageSize
@@ -588,6 +708,7 @@ const frameTouchUpPreferences = {
588
708
  color: "#f8fafc",
589
709
  tool: "brush"
590
710
  };
711
+ let frameTouchUpClipboard;
591
712
  export async function openFrameTouchUpEditor(options) {
592
713
  const sourceImage = await loadImageElement(options.frameSrc);
593
714
  const dialog = document.createElement("div");
@@ -646,6 +767,7 @@ export async function openFrameTouchUpEditor(options) {
646
767
  const eraserButton = touchUpButton("Eraser");
647
768
  const pickerButton = touchUpButton("Picker");
648
769
  const selectButton = touchUpButton("Select");
770
+ selectButton.title = "Select pixels. Cmd/Ctrl+C copies; Cmd/Ctrl+V pastes.";
649
771
  const fillButton = touchUpButton("Fill");
650
772
  const undoButton = touchUpButton("Undo");
651
773
  const redoButton = touchUpButton("Redo");
@@ -679,6 +801,13 @@ export async function openFrameTouchUpEditor(options) {
679
801
  const selectionBox = document.createElement("div");
680
802
  selectionBox.className = "ai-game-assets-designer__touchup-selection";
681
803
  selectionBox.hidden = true;
804
+ const rotateSelectionButton = document.createElement("button");
805
+ rotateSelectionButton.type = "button";
806
+ rotateSelectionButton.className = "ai-game-assets-designer__touchup-selection-rotate";
807
+ rotateSelectionButton.textContent = "↻";
808
+ rotateSelectionButton.setAttribute("aria-label", "Rotate selection 90 degrees clockwise");
809
+ rotateSelectionButton.title = "Rotate selection 90° clockwise";
810
+ selectionBox.append(rotateSelectionButton);
682
811
  canvasWrap.append(canvas, selectionBox);
683
812
  const side = document.createElement("div");
684
813
  side.className = "ai-game-assets-designer__touchup-side";
@@ -718,6 +847,7 @@ export async function openFrameTouchUpEditor(options) {
718
847
  let drawingPointerId;
719
848
  let selectionStart;
720
849
  let selection;
850
+ let floatingSelection;
721
851
  let animationTimeout;
722
852
  let animationCursor = 0;
723
853
  let disposed = false;
@@ -775,6 +905,63 @@ export async function openFrameTouchUpEditor(options) {
775
905
  redoButton.disabled = redoStack.length === 0;
776
906
  };
777
907
  const snapshot = () => context.getImageData(0, 0, canvas.width, canvas.height);
908
+ const clipboardImageData = () => {
909
+ if (!frameTouchUpClipboard)
910
+ return undefined;
911
+ const width = Math.min(frameTouchUpClipboard.width, canvas.width);
912
+ const height = Math.min(frameTouchUpClipboard.height, canvas.height);
913
+ const imageData = context.createImageData(width, height);
914
+ const sourceStride = frameTouchUpClipboard.width * 4;
915
+ const targetStride = width * 4;
916
+ for (let row = 0; row < height; row += 1) {
917
+ imageData.data.set(frameTouchUpClipboard.pixels.subarray(row * sourceStride, (row * sourceStride) + targetStride), row * targetStride);
918
+ }
919
+ return imageData;
920
+ };
921
+ const drawFloatingSelection = () => {
922
+ if (!selection || !floatingSelection)
923
+ return;
924
+ context.putImageData(floatingSelection.background, 0, 0);
925
+ const floatingCanvas = document.createElement("canvas");
926
+ floatingCanvas.width = floatingSelection.pixels.width;
927
+ floatingCanvas.height = floatingSelection.pixels.height;
928
+ const floatingContext = floatingCanvas.getContext("2d");
929
+ if (!floatingContext)
930
+ return;
931
+ floatingContext.putImageData(floatingSelection.pixels, 0, 0);
932
+ context.drawImage(floatingCanvas, selection.x, selection.y);
933
+ };
934
+ const rotateImageDataWithinBounds = (imageData) => {
935
+ const sourceCanvas = document.createElement("canvas");
936
+ sourceCanvas.width = imageData.width;
937
+ sourceCanvas.height = imageData.height;
938
+ const sourceContext = sourceCanvas.getContext("2d");
939
+ const targetCanvas = document.createElement("canvas");
940
+ targetCanvas.width = imageData.width;
941
+ targetCanvas.height = imageData.height;
942
+ const targetContext = targetCanvas.getContext("2d", { willReadFrequently: true });
943
+ if (!sourceContext || !targetContext)
944
+ return imageData;
945
+ sourceContext.putImageData(imageData, 0, 0);
946
+ targetContext.imageSmoothingEnabled = false;
947
+ targetContext.translate(imageData.width / 2, imageData.height / 2);
948
+ targetContext.scale(imageData.width / imageData.height, imageData.height / imageData.width);
949
+ targetContext.rotate(Math.PI / 2);
950
+ targetContext.drawImage(sourceCanvas, -imageData.width / 2, -imageData.height / 2);
951
+ targetContext.setTransform(1, 0, 0, 1, 0, 0);
952
+ return targetContext.getImageData(0, 0, imageData.width, imageData.height);
953
+ };
954
+ const copySelection = () => {
955
+ if (!selection)
956
+ return false;
957
+ const imageData = floatingSelection?.pixels ?? context.getImageData(selection.x, selection.y, selection.width, selection.height);
958
+ frameTouchUpClipboard = {
959
+ width: imageData.width,
960
+ height: imageData.height,
961
+ pixels: new Uint8ClampedArray(imageData.data)
962
+ };
963
+ return true;
964
+ };
778
965
  const pushUndo = () => {
779
966
  undoStack.push(snapshot());
780
967
  if (undoStack.length > 60)
@@ -785,6 +972,7 @@ export async function openFrameTouchUpEditor(options) {
785
972
  const restore = (imageData) => {
786
973
  context.putImageData(imageData, 0, 0);
787
974
  selection = undefined;
975
+ floatingSelection = undefined;
788
976
  redrawEditor();
789
977
  setDirty(true);
790
978
  };
@@ -814,6 +1002,12 @@ export async function openFrameTouchUpEditor(options) {
814
1002
  selectionBox.style.top = `${selection.y * zoom}px`;
815
1003
  selectionBox.style.width = `${selection.width * zoom}px`;
816
1004
  selectionBox.style.height = `${selection.height * zoom}px`;
1005
+ const renderedWidth = selection.width * zoom;
1006
+ const renderedHeight = selection.height * zoom;
1007
+ rotateSelectionButton.hidden = renderedWidth < 12 || renderedHeight < 12;
1008
+ const handleSize = Math.max(0, Math.min(18, renderedWidth - 4, renderedHeight - 4));
1009
+ rotateSelectionButton.style.width = `${handleSize}px`;
1010
+ rotateSelectionButton.style.height = `${handleSize}px`;
817
1011
  };
818
1012
  const refreshPreviews = () => {
819
1013
  stillContext.clearRect(0, 0, stillPreview.width, stillPreview.height);
@@ -997,6 +1191,13 @@ export async function openFrameTouchUpEditor(options) {
997
1191
  pushUndo();
998
1192
  const nextX = clamp(selection.x + deltaX, 0, canvas.width - selection.width);
999
1193
  const nextY = clamp(selection.y + deltaY, 0, canvas.height - selection.height);
1194
+ if (floatingSelection) {
1195
+ selection = { ...selection, x: nextX, y: nextY };
1196
+ drawFloatingSelection();
1197
+ setDirty(true);
1198
+ redrawEditor();
1199
+ return;
1200
+ }
1000
1201
  const imageData = context.getImageData(selection.x, selection.y, selection.width, selection.height);
1001
1202
  context.clearRect(selection.x, selection.y, selection.width, selection.height);
1002
1203
  context.putImageData(imageData, nextX, nextY);
@@ -1004,15 +1205,60 @@ export async function openFrameTouchUpEditor(options) {
1004
1205
  setDirty(true);
1005
1206
  redrawEditor();
1006
1207
  };
1208
+ const rotateSelection = () => {
1209
+ if (!selection)
1210
+ return;
1211
+ pushUndo();
1212
+ const pixels = floatingSelection?.pixels ?? context.getImageData(selection.x, selection.y, selection.width, selection.height);
1213
+ const rotated = rotateImageDataWithinBounds(pixels);
1214
+ if (floatingSelection) {
1215
+ floatingSelection.pixels = rotated;
1216
+ drawFloatingSelection();
1217
+ }
1218
+ else {
1219
+ context.putImageData(rotated, selection.x, selection.y);
1220
+ }
1221
+ setDirty(true);
1222
+ redrawEditor();
1223
+ };
1007
1224
  const deleteSelection = () => {
1008
1225
  if (!selection)
1009
1226
  return;
1010
1227
  pushUndo();
1011
- context.clearRect(selection.x, selection.y, selection.width, selection.height);
1228
+ if (floatingSelection) {
1229
+ context.putImageData(floatingSelection.background, 0, 0);
1230
+ floatingSelection = undefined;
1231
+ }
1232
+ else {
1233
+ context.clearRect(selection.x, selection.y, selection.width, selection.height);
1234
+ }
1012
1235
  selection = undefined;
1013
1236
  setDirty(true);
1014
1237
  redrawEditor();
1015
1238
  };
1239
+ const pasteSelection = () => {
1240
+ const pixels = clipboardImageData();
1241
+ if (!pixels)
1242
+ return false;
1243
+ const x = clamp(selection?.x ?? Math.floor((canvas.width - pixels.width) / 2), 0, canvas.width - pixels.width);
1244
+ const y = clamp(selection?.y ?? Math.floor((canvas.height - pixels.height) / 2), 0, canvas.height - pixels.height);
1245
+ pushUndo();
1246
+ floatingSelection = {
1247
+ background: snapshot(),
1248
+ pixels
1249
+ };
1250
+ selection = {
1251
+ x,
1252
+ y,
1253
+ width: pixels.width,
1254
+ height: pixels.height
1255
+ };
1256
+ selectTool("select");
1257
+ drawFloatingSelection();
1258
+ setDirty(true);
1259
+ redrawEditor();
1260
+ return true;
1261
+ };
1016
1262
  const drawAnimationFrame = () => {
1017
1263
  if (disposed)
1018
1264
  return;
@@ -1054,6 +1300,8 @@ export async function openFrameTouchUpEditor(options) {
1054
1300
  brushMenu.hidden = true;
1055
1301
  };
1056
1302
  const close = () => {
1303
+ if (disposed)
1304
+ return;
1057
1305
  disposed = true;
1058
1306
  if (animationTimeout !== undefined)
1059
1307
  window.clearTimeout(animationTimeout);
@@ -1063,6 +1311,7 @@ export async function openFrameTouchUpEditor(options) {
1063
1311
  window.removeEventListener("blur", cancelDrawing);
1064
1312
  document.removeEventListener("pointerdown", closeBrushMenuOnOutsidePointer);
1065
1313
  dialog.remove();
1314
+ options.onClose?.();
1066
1315
  };
1067
1316
  const requestClose = () => {
1068
1317
  if (dirty && !window.confirm("You have unsaved changes. Close without saving?"))
@@ -1074,6 +1323,23 @@ export async function openFrameTouchUpEditor(options) {
1074
1323
  return;
1075
1324
  const key = event.key.toLowerCase();
1076
1325
  const commandModifier = event.metaKey || event.ctrlKey;
1326
+ const target = event.target;
1327
+ const isEditingInput = target instanceof HTMLInputElement ||
1328
+ target instanceof HTMLTextAreaElement ||
1329
+ target instanceof HTMLSelectElement ||
1330
+ (target instanceof HTMLElement && target.isContentEditable);
1331
+ if (isEditingInput && event.key !== "Escape")
1332
+ return;
1333
+ if (commandModifier && !event.altKey && key === "c" && copySelection()) {
1334
+ event.preventDefault();
1335
+ event.stopPropagation();
1336
+ return;
1337
+ }
1338
+ if (commandModifier && !event.altKey && key === "v" && pasteSelection()) {
1339
+ event.preventDefault();
1340
+ event.stopPropagation();
1341
+ return;
1342
+ }
1077
1343
  if (commandModifier && !event.altKey && (key === "z" || key === "y")) {
1078
1344
  event.preventDefault();
1079
1345
  event.stopPropagation();
@@ -1141,6 +1407,7 @@ export async function openFrameTouchUpEditor(options) {
1141
1407
  pickColorAt(point);
1142
1408
  return;
1143
1409
  }
1410
+ floatingSelection = undefined;
1144
1411
  pushUndo();
1145
1412
  drawing = true;
1146
1413
  drawingPointerId = event.pointerId;
@@ -1246,6 +1513,15 @@ export async function openFrameTouchUpEditor(options) {
1246
1513
  clientY: event.clientY
1247
1514
  });
1248
1515
  }, { passive: false });
1516
+ rotateSelectionButton.addEventListener("pointerdown", (event) => {
1517
+ event.preventDefault();
1518
+ event.stopPropagation();
1519
+ });
1520
+ rotateSelectionButton.addEventListener("click", (event) => {
1521
+ event.preventDefault();
1522
+ event.stopPropagation();
1523
+ rotateSelection();
1524
+ });
1249
1525
  closeButton.addEventListener("click", requestClose);
1250
1526
  zoomOutButton.addEventListener("click", () => {
1251
1527
  setZoom(zoom - 1);
@@ -1747,9 +2023,12 @@ export async function openStyleGuideEditor(options) {
1747
2023
  const promoteButton = document.createElement("button");
1748
2024
  promoteButton.type = "button";
1749
2025
  promoteButton.textContent = "Promote style";
2026
+ const regenerateAllButton = document.createElement("button");
2027
+ regenerateAllButton.type = "button";
2028
+ regenerateAllButton.textContent = "Regenerate all...";
1750
2029
  const actions = document.createElement("div");
1751
2030
  actions.className = "ai-game-assets-designer__modal-actions";
1752
- actions.append(cancelButton, promoteButton, confirmButton);
2031
+ actions.append(cancelButton, promoteButton, regenerateAllButton, confirmButton);
1753
2032
  const draft = () => ({
1754
2033
  prompt: promptInput.value.trim(),
1755
2034
  images: images.map((image) => ({ ...image }))
@@ -1775,6 +2054,12 @@ export async function openStyleGuideEditor(options) {
1775
2054
  promoteButton.disabled = false;
1776
2055
  }
1777
2056
  });
2057
+ regenerateAllButton.addEventListener("click", () => {
2058
+ const current = draft();
2059
+ options.onConfirm(current);
2060
+ close();
2061
+ void options.onRegenerateAll(current);
2062
+ });
1778
2063
  card.append(title, promptField, dropZone, uploadButton, fileInput, imageList, actions);
1779
2064
  dialog.append(card);
1780
2065
  options.root.append(dialog);
@@ -2492,10 +2777,21 @@ export function ensureFramePreviewElement(element) {
2492
2777
  return frameElement;
2493
2778
  }
2494
2779
  export function generationOverridesFromInputs(elements, asset, format) {
2780
+ const tileset = designerTilesetMetadata(asset);
2495
2781
  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)
2782
+ width: positiveIntegerInput(elements.widthInput, asset.frameGrid?.frameWidth ?? tileset?.tileWidth ?? asset.dimensions?.width ?? 1),
2783
+ height: positiveIntegerInput(elements.heightInput, asset.frameGrid?.frameHeight ?? tileset?.tileHeight ?? asset.dimensions?.height ?? 1)
2498
2784
  };
2785
+ if (tileset) {
2786
+ return {
2787
+ tileset: {
2788
+ tileWidth: dimensions.width,
2789
+ tileHeight: dimensions.height,
2790
+ tileCount: positiveIntegerInput(elements.frameCountInput, tileset.tileCount ?? tileset.columns * tileset.rows)
2791
+ },
2792
+ settings: { format }
2793
+ };
2794
+ }
2499
2795
  if (!asset.frameGrid) {
2500
2796
  return {
2501
2797
  dimensions,
@@ -2564,6 +2860,9 @@ export function isAudioAsset(asset) {
2564
2860
  asset?.kind === "voice" ||
2565
2861
  asset?.kind === "voice-line";
2566
2862
  }
2863
+ function designerTilesetMetadata(asset) {
2864
+ return asset?.tileset;
2865
+ }
2567
2866
  export function isVoiceAsset(asset) {
2568
2867
  return asset?.kind === "voice" || asset?.kind === "voice-line";
2569
2868
  }
@@ -2572,6 +2871,7 @@ export function assetWithGeneratedGeometry(asset, option, options = {}) {
2572
2871
  ...asset,
2573
2872
  dimensions: option.dimensions ?? asset.dimensions,
2574
2873
  frameGrid: option.frameGrid ?? asset.frameGrid,
2874
+ tileset: option.tileset ?? asset.tileset,
2575
2875
  animations: option.animations ??
2576
2876
  (options.inheritAnimations
2577
2877
  ? asset.animations
@@ -2593,7 +2893,11 @@ export function positiveIntegerInput(input, fallback) {
2593
2893
  const value = Number(input.value);
2594
2894
  if (!Number.isFinite(value))
2595
2895
  return fallback;
2596
- return Math.max(1, Math.floor(value));
2896
+ const normalized = Math.max(1, Math.floor(value));
2897
+ const maximum = Number(input.max);
2898
+ return input.max && Number.isFinite(maximum)
2899
+ ? Math.min(normalized, Math.max(1, Math.floor(maximum)))
2900
+ : normalized;
2597
2901
  }
2598
2902
  export function positiveNumberInput(input, fallback) {
2599
2903
  const value = Number(input.value);
@@ -2724,7 +3028,7 @@ export async function imageSourceToDataUrl(src) {
2724
3028
  return src;
2725
3029
  const response = await fetch(src);
2726
3030
  if (!response.ok) {
2727
- throw new Error(`Could not load current animation image (${response.status}).`);
3031
+ throw new Error(`Could not load current asset image (${response.status}).`);
2728
3032
  }
2729
3033
  const responseBlob = await response.blob();
2730
3034
  const blob = responseBlob.type === "application/octet-stream"
@@ -2737,11 +3041,11 @@ export async function imageSourceToDataUrl(src) {
2737
3041
  resolve(reader.result);
2738
3042
  }
2739
3043
  else {
2740
- reject(new Error("Could not convert current animation image to a data URL."));
3044
+ reject(new Error("Could not convert current asset image to a data URL."));
2741
3045
  }
2742
3046
  });
2743
3047
  reader.addEventListener("error", () => {
2744
- reject(reader.error ?? new Error("Could not read current animation image."));
3048
+ reject(reader.error ?? new Error("Could not read current asset image."));
2745
3049
  });
2746
3050
  reader.readAsDataURL(blob);
2747
3051
  });
@@ -2947,6 +3251,36 @@ export function ensureDesignerStyles() {
2947
3251
  font: inherit;
2948
3252
  }
2949
3253
  .ai-game-assets-designer__field textarea { resize: vertical; }
3254
+ .ai-game-assets-designer__tile-prompts {
3255
+ display: grid;
3256
+ gap: 7px;
3257
+ margin-bottom: 12px;
3258
+ padding: 10px;
3259
+ border: 1px solid #303949;
3260
+ border-radius: 8px;
3261
+ background: rgba(11, 15, 22, 0.58);
3262
+ }
3263
+ .ai-game-assets-designer__tile-prompts[hidden] { display: none; }
3264
+ .ai-game-assets-designer__tile-prompts-title {
3265
+ color: #e5edf9;
3266
+ font-size: 13px;
3267
+ font-weight: 700;
3268
+ }
3269
+ .ai-game-assets-designer__tile-prompts-hint {
3270
+ color: #8f9cb5;
3271
+ font-size: 11px;
3272
+ line-height: 1.35;
3273
+ }
3274
+ .ai-game-assets-designer__tile-prompts-list {
3275
+ display: grid;
3276
+ gap: 8px;
3277
+ }
3278
+ .ai-game-assets-designer__tile-prompts-list .ai-game-assets-designer__field {
3279
+ margin-bottom: 0;
3280
+ }
3281
+ .ai-game-assets-designer__tile-prompts-list textarea {
3282
+ min-height: 62px;
3283
+ }
2950
3284
  .ai-game-assets-designer__target-variant {
2951
3285
  margin: -4px 0 12px;
2952
3286
  padding: 7px 9px;
@@ -3171,6 +3505,17 @@ export function ensureDesignerStyles() {
3171
3505
  gap: 8px;
3172
3506
  margin-top: 10px;
3173
3507
  }
3508
+ .ai-game-assets-designer__tileset-tile-prompt {
3509
+ margin-top: 10px;
3510
+ padding: 9px 10px;
3511
+ border: 1px solid #354058;
3512
+ border-radius: 6px;
3513
+ background: rgba(37, 49, 73, 0.48);
3514
+ color: #d8e4f7;
3515
+ font-size: 12px;
3516
+ line-height: 1.45;
3517
+ white-space: pre-wrap;
3518
+ }
3174
3519
  .ai-game-assets-designer__options.is-audio {
3175
3520
  grid-template-columns: 1fr;
3176
3521
  }
@@ -3245,6 +3590,159 @@ export function ensureDesignerStyles() {
3245
3590
  box-shadow: 0 22px 70px rgba(0, 0, 0, 0.55);
3246
3591
  padding: 14px;
3247
3592
  }
3593
+ .ai-game-assets-designer__tileset-mixer-card {
3594
+ width: min(980px, calc(100vw - 36px));
3595
+ }
3596
+ .ai-game-assets-designer__tileset-editor-card {
3597
+ width: min(720px, calc(100vw - 36px));
3598
+ }
3599
+ .ai-game-assets-designer__tileset-editor-stage {
3600
+ border: 1px solid #303a49;
3601
+ border-radius: 8px;
3602
+ }
3603
+ .ai-game-assets-designer__tileset-editor-strip {
3604
+ margin-top: 8px;
3605
+ }
3606
+ .ai-game-assets-designer__tileset-transform-fields {
3607
+ margin-bottom: 6px;
3608
+ }
3609
+ .ai-game-assets-designer__tileset-transform-hint {
3610
+ margin: 0 0 10px;
3611
+ color: #aeb8c8;
3612
+ font-size: 12px;
3613
+ line-height: 1.4;
3614
+ }
3615
+ .ai-game-assets-designer__tileset-transform-thumb {
3616
+ width: 48px;
3617
+ height: 48px;
3618
+ display: grid;
3619
+ place-items: center;
3620
+ }
3621
+ .ai-game-assets-designer__tileset-transform-thumb-viewport {
3622
+ position: relative;
3623
+ display: block;
3624
+ overflow: hidden;
3625
+ border-radius: 3px;
3626
+ pointer-events: none;
3627
+ }
3628
+ @media (max-width: 520px) {
3629
+ .ai-game-assets-designer__tileset-transform-fields {
3630
+ grid-template-columns: repeat(2, minmax(0, 1fr));
3631
+ }
3632
+ }
3633
+ .ai-game-assets-designer__tileset-regenerate-card {
3634
+ width: min(620px, calc(100vw - 36px));
3635
+ }
3636
+ .ai-game-assets-designer__tileset-regenerate-card .ai-game-assets-designer__tileset-tile-prompt {
3637
+ margin: 0 0 12px;
3638
+ }
3639
+ .ai-game-assets-designer__tileset-mixer-hint {
3640
+ margin: -5px 0 12px;
3641
+ color: #aeb8c8;
3642
+ font-size: 12px;
3643
+ line-height: 1.4;
3644
+ }
3645
+ .ai-game-assets-designer__tileset-mixer-body {
3646
+ display: grid;
3647
+ grid-template-columns: minmax(160px, 220px) minmax(0, 1fr);
3648
+ gap: 14px;
3649
+ min-height: 300px;
3650
+ }
3651
+ .ai-game-assets-designer__tileset-navigator-panel,
3652
+ .ai-game-assets-designer__tileset-preview-panel {
3653
+ min-width: 0;
3654
+ border: 1px solid #303a49;
3655
+ border-radius: 8px;
3656
+ background: #0f1218;
3657
+ padding: 10px;
3658
+ }
3659
+ .ai-game-assets-designer__tileset-section-title {
3660
+ min-height: 20px;
3661
+ margin-bottom: 8px;
3662
+ color: #dbeafe;
3663
+ font-size: 12px;
3664
+ font-weight: 700;
3665
+ }
3666
+ .ai-game-assets-designer__tileset-navigator {
3667
+ display: grid;
3668
+ grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
3669
+ gap: 7px;
3670
+ max-height: min(58vh, 540px);
3671
+ overflow: auto;
3672
+ padding: 2px;
3673
+ }
3674
+ .ai-game-assets-designer__tileset-tile {
3675
+ position: relative;
3676
+ overflow: hidden;
3677
+ border: 2px solid #384251;
3678
+ border-radius: 6px;
3679
+ background: #111827;
3680
+ cursor: pointer;
3681
+ }
3682
+ .ai-game-assets-designer__tileset-tile::after {
3683
+ content: attr(data-source);
3684
+ position: absolute;
3685
+ right: 2px;
3686
+ bottom: 2px;
3687
+ z-index: 3;
3688
+ border-radius: 999px;
3689
+ background: rgba(8, 12, 20, 0.88);
3690
+ color: #9ca8ba;
3691
+ padding: 1px 4px;
3692
+ font-size: 9px;
3693
+ line-height: 1.4;
3694
+ }
3695
+ .ai-game-assets-designer__tileset-tile.is-selected {
3696
+ border-color: #6ed3ff;
3697
+ box-shadow: 0 0 0 1px rgba(110, 211, 255, 0.35);
3698
+ }
3699
+ .ai-game-assets-designer__tileset-tile.is-mixed::after {
3700
+ background: #155e75;
3701
+ color: #ecfeff;
3702
+ }
3703
+ .ai-game-assets-designer__tileset-choices {
3704
+ display: grid;
3705
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
3706
+ gap: 10px;
3707
+ align-items: start;
3708
+ width: 100%;
3709
+ min-width: 0;
3710
+ }
3711
+ .ai-game-assets-designer__tileset-choice {
3712
+ display: grid;
3713
+ justify-items: center;
3714
+ gap: 7px;
3715
+ min-width: 0;
3716
+ max-width: 100%;
3717
+ overflow: hidden;
3718
+ border: 2px solid #384251;
3719
+ border-radius: 8px;
3720
+ background: #151b25;
3721
+ color: #dbeafe;
3722
+ padding: 9px;
3723
+ font: inherit;
3724
+ font-size: 12px;
3725
+ cursor: pointer;
3726
+ }
3727
+ .ai-game-assets-designer__tileset-choice.is-selected {
3728
+ border-color: #6ed3ff;
3729
+ background: #16283a;
3730
+ }
3731
+ .ai-game-assets-designer__tileset-choice-preview {
3732
+ position: relative;
3733
+ overflow: hidden;
3734
+ max-width: 100%;
3735
+ border-radius: 6px;
3736
+ background: #0a0d12;
3737
+ }
3738
+ @media (max-width: 760px) {
3739
+ .ai-game-assets-designer__tileset-mixer-body {
3740
+ grid-template-columns: 1fr;
3741
+ }
3742
+ .ai-game-assets-designer__tileset-navigator {
3743
+ max-height: 180px;
3744
+ }
3745
+ }
3248
3746
  .ai-game-assets-designer__modal-title {
3249
3747
  margin-bottom: 12px;
3250
3748
  font-weight: 700;
@@ -3268,6 +3766,9 @@ export function ensureDesignerStyles() {
3268
3766
  .ai-game-assets-designer__modal-feedback[data-kind="error"] {
3269
3767
  color: #ffb4b4;
3270
3768
  }
3769
+ .ai-game-assets-designer__modal-feedback[data-kind="success"] {
3770
+ color: #86efac;
3771
+ }
3271
3772
  .ai-game-assets-designer__version-list {
3272
3773
  display: grid;
3273
3774
  gap: 10px;
@@ -3381,6 +3882,9 @@ export function ensureDesignerStyles() {
3381
3882
  .ai-game-assets-designer__frame-thumb.is-selected::after {
3382
3883
  border-color: #6ed3ff;
3383
3884
  }
3885
+ .ai-game-assets-designer__frame-thumb.is-previewing::after {
3886
+ box-shadow: inset 0 -3px 0 #facc15;
3887
+ }
3384
3888
  .ai-game-assets-designer__frame-fields {
3385
3889
  display: grid;
3386
3890
  grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -3530,7 +4034,7 @@ export function ensureDesignerStyles() {
3530
4034
  .ai-game-assets-designer__touchup {
3531
4035
  position: fixed;
3532
4036
  inset: 0;
3533
- z-index: 1;
4037
+ z-index: 2147483647;
3534
4038
  display: grid;
3535
4039
  grid-template-rows: auto auto minmax(0, 1fr);
3536
4040
  gap: 10px;
@@ -3716,6 +4220,30 @@ export function ensureDesignerStyles() {
3716
4220
  box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.9);
3717
4221
  transform: translate(24px, 24px);
3718
4222
  }
4223
+ .ai-game-assets-designer__touchup-selection-rotate {
4224
+ position: absolute;
4225
+ top: 2px;
4226
+ right: 2px;
4227
+ z-index: 2;
4228
+ display: grid;
4229
+ place-items: center;
4230
+ min-width: 0;
4231
+ min-height: 0;
4232
+ border: 1px solid #dbeafe;
4233
+ border-radius: 999px;
4234
+ background: rgba(15, 23, 42, 0.94);
4235
+ color: #f8fafc;
4236
+ padding: 0;
4237
+ font: 700 11px/1 ui-sans-serif, system-ui, sans-serif;
4238
+ cursor: pointer;
4239
+ pointer-events: auto;
4240
+ }
4241
+ .ai-game-assets-designer__touchup-selection-rotate:hover,
4242
+ .ai-game-assets-designer__touchup-selection-rotate:focus-visible {
4243
+ border-color: #6ed3ff;
4244
+ color: #6ed3ff;
4245
+ outline: none;
4246
+ }
3719
4247
  .ai-game-assets-designer__touchup-side {
3720
4248
  min-height: 0;
3721
4249
  display: grid;